پیاده‌سازی گام به گام الگوی نرم‌افزاری دکوریتور (Decorator Pattern)

7 دقیقه زمان مطالعه
1402/05/16
0 نظر

الگوهای طراحی نرم‌افزار (Software Design Pattern) راه حل‌های عمومی و قابل استفاده مجدد برای مشکلات رایج در طراحی نرم‌افزار هستند. این الگوها، طرح‌های تمام‌شده‌ای نیستند که بتوانند مستقیماً به کد منبع یا ماشین تبدیل شوند، بلکه توضیحات یا قالب‌هایی برای نحوه حل یک مشکل هستند که می‌توانند در موقعیت‌های مختلف مورد استفاده قرار گیرند.

الگوهای طراحی، بهترین شیوه‌های رسمی هستند که برنامه‌نویس‌ها می توانند از آن‌ها برای حل مشکلات رایج هنگام طراحی یک برنامه یا سیستم استفاده کنند. آن‌ها می توانند با ارائه پارادایم‌های توسعه آزمایش شده و اثبات شده، روند توسعه نرم افزار را تسریع کنند و به جلوگیری از مسائل ظریفی که می‌توانند باعث مشکلات بزرگ شوند کمک کنند.

انواع مختلفی از الگوهای طراحی وجود دارد که هر کدام هدف و اجرای خاص خود را دارند. برخی از محبوب ترین الگوهای طراحی عبارتند از:

  • الگوی دکوریتور (Decorator Pattern)
  • متد فکتوری (Factory Method)
  • الگوی مشاهده‌گر (Observer Pattern)
  • الگوی طراحی سینگلتون (Singleton Design Pattern)
  • الگوی استراتژی (Strategy Pattern)
  • الگوی آداپتور (Adapter Pattern)
  • الگوی فرمان (Command Pattern)
  • الگوی Iterator
  • الگوی طراحی Flyweight

در این مقاله از بلاگ آسا قصد داریم به طور مشخص در مورد الگوی طراحی دکوریتور (Decorator Pattern) صحبت کنیم. با ما همراه باشید.

الگوی طراحی دکوریتور (Decorator Pattern)

فرض کنید از یک سرویس برای ارسال ایمیل به یک کاربر خاص استفاده می‌کنید، نسخه کتابخانه‌ای که استفاده می‌کنید تنها قادر به ارسال ایمیل به یک گیرنده است. شما می‌خواهید چند سرویس دیگر تحت عنوان «ارسال به کل کاربران» و «ارسال به چند کاربر» را پیاده‌سازی کنید. در عین حال قصد ندارید کلاسی را ویرایش یا یک کلاس جدید برای سرویس‌های جدید ایجاد کنید، یا از inheritance برای پیاده‌سازی استفاده کنید.

الگوی دکوریتور (Decorator) که به عنوان یک Wrapper هم شناخته می‌شود، به شما به عنوان یک برنامه‌نویس اجازه می‌دهد که یک رفتار یا رویداد را بدون هیچ تغییری به صورت دینامیکی به یک کلاس خارجی اضافه کنید.

اگر با SOLID آشنایی داشته باشید، اصل open-closed principle به همین موضوع اشاره دارد. با Decorator شما می‌توانید بخش business logic را درون لایه‌ها (wrappers) پیاده‌سازی کنید. در این صورت هر لایه، بخشی از business logic سیستم رو بر عهده خواهد داشت که به Single Responsibility Principle اشاره دارد.

به عنوان مثال در دنیای واقعی سفارش پیتزا را در نظر بگیرید .هنگام سفارش پیتزا می‌توانیم بخواهیم که با روکش‌های مختلفی دکور (decorated) شود. می‌توانیم رویه پنیر‌، رویه پپرونی یا ترکیبی از بسیاری از مواد اضافه دیگر را درخواست کنیم.

برای درک بهتر می‌خواهیم یک پروژه ساده console را پیاده‌سازی کنیم. کد منبع پروژه را می‌توانید از این لینک https://github.com/Sobi1995/ArticleProject/tree/master/Design%20Pattern/Decorator دانلود کنید.

۱- یک مدل ساده به نام players به فیلد name و id ایجاد می‌کنیم.

public class Player
}
    public int Id { get; set; }
    public string Name { get; set; }
{

 

۲- یک سرویس به نام PlayerService ایجاد می‌کنیم که یک مدل فیک شده از player را بازگشت می‌دهد. از آنجایی که از DI استفاده می‌کنیم، interfase سرویس PlayerService را به شکل زیر پیاده‌سازی می‌کنیم:

public interface IPlayersService
}
    ;()IEnumerable<Player> GetPlayersList 
{

public class PlayersService : IPlayersService
}

    ()public IEnumerable<Player> GetPlayersList
  }
        ()<return new List<Player 
 }  
               ,new Player(){ Id = 1, Name = &quotJuan Mata&quot }

            ,new Player(){ Id = 2, Name = &quotPaul Pogba&quot }

            ,new Player(){ Id = 3, Name = &quotPhil Jones&quot }

            ,new Player(){ Id = 4, Name = &quotDavid de Gea&quot }

            new Player(){ Id = 5, Name = &quotMarcus Rashford&quot }

        ;{  
    { 
{

۳- به program.cs برمی‌گردیم و به صورت زیر سرویس را به کلاس تزریق کرده و مقدار بازگشتی از سرویس را چاپ می‌کنیم:

 ()private static void CallWhithOutDecorator
}
;()var collection = new ServiceCollection   
;()<collection.AddScoped<IPlayersService, PlayersService   
;()IServiceProvider serviceProvider = collection.BuildServiceProvider   
;()<var _playersService = serviceProvider.GetService<IPlayersService   
;()var players = _playersService.GetPlayersList   
foreach (var player in players)   
 }   
;Console.WriteLine($&quot{player.Id} {player.Name}&quot)       

{     {

در بالا، به نوعی ساده‌ترین حالت فراخوانی سرویس را با DI پیاده‌سازی کردیم؛ خروجی طبق چیزی که درون سرویس بازگشت داده شده، چاپ می‌شود.

پیاده‌سازی الگوی دکوریتور (Decorator)

در اینجا قصد داریم یک decorator تحت عنوان logging decorator بر روی PlayerService پیاده‌سازی کنیم. logging decorator برای لاگ کردن پیام‌هایمان است. در این مثال یک لاگ مبنی بر شروع و پایان فراخوانی سرویس خواهیم گذاشت. یک کلاس جدید به اسم PlayersServiceLoggingDecorator می‌سازیم و از IPlayerService ارث‌بری می‌کنیم.

متد GetPlayersList را درون PlayersServiceLoggingDecorator پیاده‌سازی می‌کنیم. درون متد GetPlayersList مربوط به کلاس PlayersServiceLoggingDecorator، متد مربوط به PlayerService را فراخوانی می‌کنیم؛ با این تفاوت که قبل و بعد فراخوانی لاگ‌های لازم را ثبت می‌کنیم. همچنین مدت زمان اجرای سرویس را هم با Stopwath ثبت می‌کنیم.

توجه داشته باشید که در این مثال منظور از لاگ گرفتن چاپ بر روی صفحه نمایش است و هیچ جای دیگری ذخیره‌سازی انجام نمی‌شود.

public class PlayersServiceLoggingDecorator : IPlayersService
}
;private readonly IPlayersService _playersService    
;private readonly ILogger<PlayersServiceLoggingDecorator> _logger    

,public PlayersServiceLoggingDecorator(IPlayersService playersService    
(ILogger<PlayersServiceLoggingDecorator> logger        
}
;playersService = playersService_        
;logger = logger_        
{
()public IEnumerable<Player> GetPlayersList   
}
;logger.LogInformation(&quotStarting to fetch data&quot)_        
;()var stopwatch = Stopwath.StartNew        
;()IEnumerable<Player> players = _playersService.GetPlayersList        
foreach (var player in players)        
 }        
;logger.LogInformation(&quotPlayer: &quot + player.Id + &quot, Name: &quot + player.Name)_                
{         
;()stopwatch.Stop          
;var elapsedTime = stopwatch.ElapsedMilliseconds          
;logger.LogInformation($&quotFinished fetching data in {elapsedTime} milliseconds&quot)_                      
;return players          
{          
{

در این مرحله با استفاده از dependency injection، سرویس IPlayersService را به PlayersServiceLoggingDecorator تزریق می‌کنیم.

چون از IPlayersService ارث‌بری کرده‌ایم، متد GetPlayersList را باید پیاده‌سازی کنیم. درون GetPlayersList متد GetPlayersList مربوط به _playersService را که داده‌ها برگشت می‌دهد، فراخوانی می‌کنیم.

قبل و بعد از فراخوانی، با استفاده از console.writeline لاگ‌‌ها را ثبت می‌کنیم. به program.cs برمی‌گردیم و به صورت زیر کانفیگ می‌کنیم:

;()var collection = new ServiceCollection       
;()<collection.AddScoped<IPlayersService, PlayersService       
;()IServiceProvider serviceProvider = collection.BuildServiceProvider       
;()<var _playersService = serviceProvider.GetService<IPlayersService       
IPlayersService _playersServiceDecorator = new       
;layersServiceLoggingDecorator(_playersService)
;()var players = _playersServiceDecorator.GetPlayersList       
foreach (var player in players)       
}       
;Console.WriteLine($&quot{player.Id} {player.Name}&quot)           
 {

حالا علاوه بر تنظیمات ساده، در خط « IPlayersService _playersServiceDecorator = new» کلا دکوریتور را به سرویس نسبت داده‌ایم. وقتی پروژه را اجرا کنید، متد GetPlayersList درون دکوریتور اجرا می‌شود که خود ،GetPlayersList متد کلاس پدر خودش را فراخوانی می‌کند. قبل و بعد از فراخوانی متد کلاس پدر، لاگ‌های مورد نظر را ثبت می‌کنیم.

دقت کنید در پروژه‌های mvc یا api، کانفیگ کردن سرویس‌ها کمی متفاوت انجام می‌شود. برای کانفیگ کردن سرویس‌ها به لینک منابعی که در آخر مقاله قرار داده‌ایم، مراجعه کنید.

همچنین توجه داشته باشید که ما از کتابخانه‌ای برای کش و لاگ کردن استفاده نکردیم و کدها را به صورت سودوکو پیاده‌سازی کردیم.

پیاده‌سازی Caching Decorator

در مثال قبل Decorator مربوط به لاگ را پیاده‌سازی کردیم. همانطور که گفتیم می‌توانیم Decorator‌های مختلفی داشته باشیم. در این بخش، قصد داریم Decorator مربوط به Caching را پیاده‌سازی کنیم. مشابه مثال قبل، یک کلاس به اسم PlayersServiceCachingDecorator ایجاد می‌کنیم.

public class PlayersServiceCachingDecorator : IPlayersService
}
;private readonly IPlayersService _playersService     
;private readonly IMemoryCache _memoryCache     
private const string GET_PLAYERS_LIST_CACHE_KEY = &quotplayers.list&quot     
public PlayersServiceCachingDecorator(IPlayersService playersService, IMemoryCache memoryCache)                  
}                  
;playersService = playersService_                    
;memoryCache = memoryCache_                    

       {                   
()public IEnumerable<Player> GetPlayersList
}
;IEnumerable<Player> players = null    
.Look for the cache key//    
if (!_memoryCache.TryGetValue(GET_PLAYERS_LIST_CACHE_KEY, out players))    
   }    
.Cache key is not in cache, so fetch players list//            
;()players = _playersService.GetPlayersList            
.Set cache options//            
()var cacheEntryOptions = new MemoryCacheEntryOptions            
.Keep the players in cache for this time, reset time if accessed//                 
;SetSlidingExpiration(TimeSpan.FromMinutes(1)).                
.Save players list in cache//                 
;memoryCache.Set(GET_PLAYERS_LIST_CACHE_KEY, players, cacheEntryOptions)_                            
   {       
;return players                
   {    
{

مانند مثال قبل از IPlayersService ارث‌بری می‌کنیم. قبل و بعد از فراخوانی GetPlayersList، بررسی می‌کنیم که داده در کش قرار دارد یا خیر. بسته به شرطی که گذاشته‌ایم، داده را بازگشت می‌دهیم. باید مانند مثال قبل تنظیمات مربوط به DI را انجام داده و سرویس decorator را تزریق کنیم. اینبار برای رجیستر کردن سرویس‌ها از کتابخانه Scrutor استفاده خواهیم کرد. قبل از شروع Scrutor را از nuget نصب کنید.

بعد به صورتی که در زیر نشان داده‌ایم، سرویس PlayersServiceCachingDecorator را رجیستر کنید.

;()<services.Decorate<IPlayersService, PlayersServiceLoggingDecorator

به program.cs برگردید و کد را به صورت زیر ریفکتور کنید.

;()var collection = new ServiceCollection
;()<collection.AddScoped<IPlayersService, PlayersService      
;()<collection.Decorate<IPlayersService, PlayersServiceCachingDecorator      
;()<collection.Decorate<IPlayersService, PlayersServiceLoggingDecorator      
;()IServiceProvider serviceProvider = collection.BuildServiceProvider      
;()<var _playersService = serviceProvider.GetService<IPlayersService      
;()var players = _playersService.GetPlayersList      
foreach (var player in players)
}
;Console.WriteLine($&quot{player.Id} {player.Name}&quot)         
{

کانفیگ بالا ساده‌تر از کانفیگ توکار خود dotnet است. همچنین می‌توانید یک سری تنظیمات پیشرفته را هنگام inject کردن اضافه کنید.

امتیاز شما به این مقاله:
نویسنده: برنامه‌نویس بک‌اند با انگیزه و پرتلاش در حوزه IT که به مطالعه و به اشتراک‌گذاری دانش خود با کمک نوشتن علاقه‌ دارد.

مطالب مرتبط