Decorator Pattern در برنامه نویسی چیست

Decorator Pattern چیست

Decorator Pattern چیست؟ آموزش سریع الگوی دکوراتور با مثال واقعی در .NET 8

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

Decorator Pattern چیست؟

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

برای مثال، یک سرویس ارسال اعلان را در نظر بگیرید. وظیفه اصلی سرویس، ارسال پیام است؛ اما ممکن است بعداً بخواهیم قابلیت‌های زیر را نیز به آن اضافه کنیم:

  • ثبت اطلاعات عملیات در لاگ؛
  • اندازه‌گیری مدت زمان ارسال پیام؛
  • تلاش مجدد در صورت شکست عملیات؛
  • ذخیره نتیجه در پایگاه داده؛
  • بررسی دسترسی کاربر پیش از ارسال.

با Decorator لازم نیست تمام این رفتارها را داخل کلاس اصلی قرار دهیم. هر قابلیت در یک کلاس مستقل نوشته می‌شود و هنگام ساخت شیء، لایه‌های موردنیاز را با یکدیگر ترکیب می‌کنیم.

تعریف کوتاه: Decorator یعنی افزودن مسئولیت یا رفتار جدید به یک شیء از طریق قرار دادن آن داخل یک شیء دیگر، بدون دست‌کاری کد کلاس اصلی.

Decorator چه مشکلی را حل می‌کند؟

بدون استفاده از Decorator معمولاً دو راه پیش روی توسعه‌دهنده قرار دارد: افزودن تمام قابلیت‌ها به کلاس اصلی یا ساخت زیرکلاس‌های متعدد. راه اول باعث بزرگ شدن کلاس و نقض اصل مسئولیت واحد می‌شود. راه دوم نیز با افزایش ترکیب قابلیت‌ها، تعداد کلاس‌ها را به‌شدت زیاد می‌کند.

نمونه انفجار کلاس‌ها

فرض کنید سه نوع ارسال‌کننده شامل پیامک، ایمیل و اعلان پوش داریم. همچنین سه قابلیت ثبت لاگ، تلاش مجدد و اندازه‌گیری عملکرد نیز داریم. استفاده از ارث‌بری ممکن است ما را به کلاس‌هایی شبیه موارد زیر برساند:

SmsSenderWithLogging SmsSenderWithRetry SmsSenderWithLoggingAndRetry EmailSenderWithLogging EmailSenderWithRetry PushSenderWithLoggingAndMetrics ...

هر بار که قابلیت جدیدی اضافه شود، تعداد ترکیب‌ها بیشتر می‌شود. Decorator این قابلیت‌ها را به اجزای مستقل تبدیل می‌کند تا بتوانیم آن‌ها را آزادانه روی سرویس‌های مختلف قرار دهیم.

Decorator Pattern یکی از الگوهای ساختاری (Structural Design Patterns) در کتاب Gang of Four (GoF) است که اجازه می‌دهد بدون تغییر دادن کلاس اصلی، رفتارهای جدیدی به یک شیء اضافه کنیم.

ساختار اصلی الگوی Decorator

این الگو معمولاً از چهار بخش اصلی تشکیل می‌شود:

  1. Component: رابط یا کلاس انتزاعی مشترکی که عملیات اصلی را تعریف می‌کند.
  2. Concrete Component: پیاده‌سازی واقعی و پایه‌ای عملیات.
  3. Base Decorator: کلاسی که یک نمونه از Component را نگه می‌دارد و درخواست را به آن منتقل می‌کند.
  4. Concrete Decorator: لایه‌ای که رفتار جدید را قبل یا بعد از اجرای سرویس داخلی اضافه می‌کند.
LoggingDecorator → PerformanceDecorator → EmailNotificationService

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

مثال سریع Decorator Pattern در .NET 8

در این مثال یک برنامه کنسولی ساده می‌سازیم. سرویس اصلی، اعلان را از طریق ایمیل ارسال می‌کند. سپس دو Decorator برای ثبت لاگ و اندازه‌گیری مدت زمان اجرا به آن اضافه می‌کنیم.

ساختار پیشنهادی پروژه

DecoratorPatternDemo/ ├── Program.cs ├── INotificationService.cs ├── EmailNotificationService.cs ├── NotificationDecorator.cs ├── LoggingNotificationDecorator.cs └── PerformanceNotificationDecorator.cs

مرحله اول: تعریف قرارداد مشترک

ابتدا رابطی تعریف می‌کنیم که هم سرویس اصلی و هم تمام Decoratorها آن را پیاده‌سازی کنند.

public interface INotificationService {     Task SendAsync(string recipient, string message); }

مرحله دوم: ساخت سرویس اصلی

کلاس زیر همان Concrete Component است و وظیفه واقعی ارسال اعلان را بر عهده دارد.

public sealed class EmailNotificationService : INotificationService {     public async Task SendAsync(string recipient, string message)     {         await Task.Delay(500);          Console.WriteLine(             $"Email sent to {recipient}: {message}");     } }

مرحله سوم: ساخت Decorator پایه

Decorator پایه یک نمونه از رابط INotificationService را نگه می‌دارد. این نمونه می‌تواند سرویس اصلی یا یک Decorator دیگر باشد.

public abstract class NotificationDecorator : INotificationService {     protected readonly INotificationService InnerService;      protected NotificationDecorator(         INotificationService innerService)     {         InnerService = innerService;     }      public virtual Task SendAsync(         string recipient,         string message)     {         return InnerService.SendAsync(recipient, message);     } }

مرحله چهارم: افزودن قابلیت Logging

این Decorator پیش از ارسال پیام یک لاگ شروع و پس از پایان عملیات یک لاگ موفقیت ثبت می‌کند.

public sealed class LoggingNotificationDecorator     : NotificationDecorator {     public LoggingNotificationDecorator(         INotificationService innerService)         : base(innerService)     {     }      public override async Task SendAsync(         string recipient,         string message)     {         Console.WriteLine(             $"[Log] Sending notification to {recipient}...");          try         {             await InnerService.SendAsync(recipient, message);             Console.WriteLine("[Log] Notification sent successfully.");         }         catch (Exception ex)         {             Console.WriteLine($"[Log] Error: {ex.Message}");             throw;         }     } }

مرحله پنجم: افزودن اندازه‌گیری زمان اجرا

Decorator دوم با استفاده از کلاس Stopwatch مدت زمان اجرای عملیات را محاسبه می‌کند.

using System.Diagnostics;  public sealed class PerformanceNotificationDecorator     : NotificationDecorator {     public PerformanceNotificationDecorator(         INotificationService innerService)         : base(innerService)     {     }      public override async Task SendAsync(         string recipient,         string message)     {         var stopwatch = Stopwatch.StartNew();          await InnerService.SendAsync(recipient, message);          stopwatch.Stop();          Console.WriteLine(             $"[Performance] Duration: {stopwatch.ElapsedMilliseconds} ms");     } }

مرحله ششم: ساخت زنجیره Decoratorها

در فایل Program.cs سرویس اصلی را ایجاد می‌کنیم و سپس Decoratorها را به ترتیب دور آن قرار می‌دهیم.

INotificationService notificationService =     new EmailNotificationService();  notificationService =     new PerformanceNotificationDecorator(notificationService);  notificationService =     new LoggingNotificationDecorator(notificationService);  await notificationService.SendAsync(     "user@example.com",     "Your order has been registered.");

خروجی تقریبی برنامه به شکل زیر خواهد بود:

[Log] Sending notification to user@example.com... Email sent to user@example.com: Your order has been registered. [Performance] Duration: 506 ms [Log] Notification sent successfully.

سرویس اصلی فقط وظیفه ارسال ایمیل را انجام می‌دهد. ثبت لاگ و محاسبه زمان اجرا در کلاس‌های مستقل قرار دارند و بدون تغییر دادن EmailNotificationService به آن اضافه شده‌اند.

اهمیت ترتیب Decoratorها

ترتیب قرار گرفتن Decoratorها روی نتیجه اجرای برنامه اثر دارد. در مثال قبلی، Logging بیرونی‌ترین لایه بود؛ بنابراین قبل از تمام عملیات اجرا شد و پس از پایان تمام لایه‌ها نیز پیام موفقیت را ثبت کرد.

اگر ترتیب را تغییر دهیم، محدوده اندازه‌گیری یا مدیریت خطا نیز تغییر می‌کند:

new LoggingNotificationDecorator(     new PerformanceNotificationDecorator(         new EmailNotificationService()));

در این حالت، Performance فقط زمان اجرای سرویس داخلی خود را اندازه‌گیری می‌کند. اما در ساختار زیر، Performance مدت زمان اجرای Logging و سرویس اصلی را با هم محاسبه می‌کند:

new PerformanceNotificationDecorator(     new LoggingNotificationDecorator(         new EmailNotificationService()));

نکته مهم: زنجیره Decoratorها را مانند Pipeline در نظر بگیرید. بیرونی‌ترین لایه نخست اجرا می‌شود، درخواست را به لایه بعدی می‌فرستد و سپس کنترل به همان لایه بیرونی بازمی‌گردد.

ثبت Decorator در Dependency Injection دات‌نت

در پروژه‌های واقعی ASP.NET Core معمولاً سرویس‌ها توسط کانتینر تزریق وابستگی ساخته می‌شوند. بدون کتابخانه جانبی نیز می‌توان زنجیره ساده Decorator را با Factory ثبت کرد:

builder.Services.AddScoped<EmailNotificationService>();  builder.Services.AddScoped<INotificationService>(serviceProvider => {     INotificationService service =         serviceProvider.GetRequiredService<EmailNotificationService>();      service = new PerformanceNotificationDecorator(service);     service = new LoggingNotificationDecorator(service);      return service; });

اکنون هر کلاسی که INotificationService را دریافت کند، زنجیره کامل را در اختیار خواهد داشت:

public sealed class OrderService {     private readonly INotificationService _notificationService;      public OrderService(INotificationService notificationService)     {         _notificationService = notificationService;     }      public Task ConfirmOrderAsync(string email)     {         return _notificationService.SendAsync(             email,             "Order confirmed.");     } }

کاربردهای واقعی Decorator Pattern در پروژه‌های دات‌نت

Decorator فقط یک الگوی آموزشی نیست و در بسیاری از بخش‌های نرم‌افزارهای تجاری کاربرد دارد:

  • Logging: ثبت ورودی، خروجی، خطا و مدت زمان اجرای سرویس‌ها؛
  • Caching: بازگرداندن پاسخ از حافظه کش قبل از مراجعه به پایگاه داده؛
  • Validation: بررسی ورودی‌ها پیش از اجرای عملیات اصلی؛
  • Authorization: کنترل مجوز کاربر پیش از اجرای سرویس؛
  • Retry: تلاش مجدد برای عملیات شبکه‌ای یا سرویس‌های خارجی؛
  • Metrics: ثبت تعداد فراخوانی، مدت زمان و نرخ شکست؛
  • Transaction: قرار دادن عملیات سرویس داخل تراکنش؛
  • Audit: ثبت تاریخچه تغییرات حساس و اطلاعات کاربر اجراکننده.

نمونه مشهور Decorator در خود .NET

کلاس‌های خانواده Stream یکی از شناخته‌شده‌ترین نمونه‌های Decorator در کتابخانه استاندارد دات‌نت هستند. یک Stream می‌تواند داخل Stream دیگری قرار بگیرد و قابلیت جدیدی دریافت کند.

await using var fileStream =     File.Create("report.gz");  await using var gzipStream =     new System.IO.Compression.GZipStream(         fileStream,         System.IO.Compression.CompressionMode.Compress);  await using var writer =     new StreamWriter(gzipStream);  await writer.WriteAsync("Decorator Pattern in .NET");

در این زنجیره، FileStream نوشتن در فایل را انجام می‌دهد، GZipStream قابلیت فشرده‌سازی را اضافه می‌کند و StreamWriter امکان نوشتن متن را فراهم می‌سازد.

مقایسه Decorator با روش‌های مشابه

روش هدف اصلی مزیت محدودیت
Decorator افزودن رفتار به یک شیء در زمان اجرا ترکیب‌پذیری بالا و عدم تغییر کلاس اصلی افزایش تعداد اشیای کوچک و پیچیدگی زنجیره
Inheritance اشتراک یا گسترش رفتار در سطح نوع ساده برای روابط واقعی «یک نوع از» اتصال شدید و احتمال انفجار زیرکلاس‌ها
Proxy کنترل دسترسی به یک شیء مناسب برای Lazy Loading، امنیت یا ارتباط راه دور هدف اصلی آن افزودن قابلیت‌های ترکیبی نیست
Middleware پردازش درخواست در یک Pipeline عمومی مناسب برای نگرانی‌های سراسری HTTP معمولاً روی کل Pipeline درخواست اعمال می‌شود

Decorator و اصل Open/Closed

اصل Open/Closed می‌گوید اجزای نرم‌افزار باید برای توسعه باز و برای تغییر بسته باشند. Decorator این اصل را به‌خوبی پیاده می‌کند؛ زیرا می‌توان رفتار جدیدی ساخت و آن را به زنجیره افزود، بدون اینکه کد سرویس اصلی را تغییر دهیم.

برای نمونه، اگر بعداً به قابلیت Retry نیاز داشته باشیم، کافی است کلاس RetryNotificationDecorator را بنویسیم و آن را به زنجیره اضافه کنیم. کلاس ارسال ایمیل و Decoratorهای قبلی بدون تغییر باقی می‌مانند.

مزایای Decorator Pattern

  • افزودن یا حذف قابلیت‌ها بدون تغییر کلاس اصلی؛
  • رعایت اصل مسئولیت واحد و جداسازی نگرانی‌ها؛
  • جلوگیری از ساخت زیرکلاس‌های فراوان؛
  • امکان ترکیب رفتارها در ترتیب‌های مختلف؛
  • آزمایش‌پذیری بهتر هر قابلیت به‌صورت مستقل؛
  • سازگاری مناسب با Interface و Dependency Injection؛
  • استفاده مجدد از یک Decorator برای چند پیاده‌سازی متفاوت.

معایب و محدودیت‌های Decorator Pattern

  • تعداد کلاس‌های کوچک پروژه افزایش پیدا می‌کند.
  • تشخیص ترتیب واقعی اجرا برای توسعه‌دهنده تازه‌وارد ممکن است دشوار باشد.
  • اشکال‌زدایی زنجیره‌های بسیار طولانی پیچیده‌تر می‌شود.
  • ترتیب نادرست Decoratorها می‌تواند نتیجه متفاوت یا خطای منطقی ایجاد کند.
  • برای رفتارهای بسیار ساده، استفاده از Decorator ممکن است طراحی را بیش از حد پیچیده کند.

اشتباهات رایج در پیاده‌سازی Decorator

تغییر دادن قرارداد اصلی

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

قرار دادن منطق اصلی کسب‌وکار داخل Decorator

Decorator برای نگرانی‌های جانبی یا رفتارهای قابل ترکیب مناسب است. منطق اصلی دامنه بهتر است در سرویس اصلی یا Domain Service باقی بماند.

نادیده گرفتن ترتیب اجرا

برای مثال، Validation معمولاً باید پیش از Transaction یا اجرای سرویس اصلی باشد. همچنین Cache ممکن است پیش از Logging تفصیلی قرار گیرد یا بالعکس؛ انتخاب ترتیب باید آگاهانه باشد.

زنجیره بسیار طولانی

اگر یک سرویس با تعداد زیادی Decorator پوشانده شود، درک و نگهداری آن دشوار خواهد شد. در چنین شرایطی باید بررسی شود که آیا برخی نگرانی‌ها بهتر است در Pipeline، Middleware یا لایه زیرساخت قرار گیرند.

بهترین روش‌ها

  1. برای Component از Interface کوچک و هدفمند استفاده کنید.
  2. هر Decorator را فقط مسئول یک قابلیت مشخص قرار دهید.
  3. ترتیب Decoratorها را در یک نقطه مرکزی مانند تنظیمات Dependency Injection نگه دارید.
  4. برای هر Decorator تست واحد مستقل بنویسید.
  5. در نام کلاس‌ها از پسوند واضح Decorator استفاده کنید.
  6. خطاها را بی‌دلیل نبلعید؛ پس از ثبت لاگ، Exception را دوباره پرتاب کنید مگر اینکه سیاست مشخصی برای مدیریت آن وجود داشته باشد.
  7. زنجیره را کوتاه و قابل فهم نگه دارید.

چه زمانی از Decorator استفاده کنیم؟

استفاده از Decorator زمانی انتخاب مناسبی است که چند مورد از شرایط زیر برقرار باشد:

  • قابلیت‌های جانبی باید قابل فعال یا غیرفعال شدن باشند.
  • نمی‌خواهید یا نمی‌توانید کلاس اصلی را تغییر دهید.
  • چند پیاده‌سازی مختلف باید از یک قابلیت مشترک استفاده کنند.
  • ترکیب قابلیت‌ها در سناریوهای مختلف متفاوت است.
  • استفاده از ارث‌بری باعث افزایش زیاد زیرکلاس‌ها می‌شود.

چه زمانی Decorator انتخاب مناسبی نیست؟

برای یک عملیات ساده که فقط یک رفتار ثابت دارد، ساخت چند لایه Decorator ارزش چندانی ایجاد نمی‌کند. همچنین اگر رفتار موردنظر به کل چرخه درخواست HTTP مربوط است، Middleware ممکن است گزینه مناسب‌تری باشد. اگر هدف تنها کنترل دسترسی یا ایجاد Lazy Loading باشد، Proxy می‌تواند مفهوم دقیق‌تری ارائه دهد.

پرسش‌های متداول

آیا Decorator فقط با Interface قابل پیاده‌سازی است؟

خیر. می‌توان از کلاس انتزاعی نیز استفاده کرد، اما Interface معمولاً وابستگی کمتر و انعطاف‌پذیری بیشتری ایجاد می‌کند.

آیا Middleware در ASP.NET Core همان Decorator است؟

Middleware از نظر زنجیره کردن رفتارها شباهت زیادی به Decorator دارد، اما دقیقاً همان الگو نیست. Middleware روی Pipeline درخواست و پاسخ HTTP تمرکز دارد، در حالی که Decorator معمولاً یک سرویس یا شیء مشخص را پوشش می‌دهد.

آیا می‌توان چند Decorator را هم‌زمان استفاده کرد؟

بله. مهم‌ترین مزیت این الگو همین ترکیب چند Decorator است. هر لایه باید همان قرارداد مشترک را پیاده‌سازی کند.

تفاوت Decorator با ارث‌بری چیست؟

ارث‌بری رفتار را در سطح کلاس و معمولاً در زمان طراحی گسترش می‌دهد؛ اما Decorator با استفاده از Composition رفتار را روی یک نمونه شیء و در زمان ساخت زنجیره اضافه می‌کند.

آیا Decorator روی کارایی اثر می‌گذارد؟

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

جمع‌بندی

Decorator Pattern روشی منعطف برای افزودن قابلیت‌های جدید به سرویس‌هاست. در این الگو، سرویس اصلی روی مسئولیت واقعی خود تمرکز می‌کند و رفتارهایی مانند Logging، Caching، Validation، Retry و Metrics در لایه‌های مستقل قرار می‌گیرند. این طراحی از انفجار زیرکلاس‌ها جلوگیری می‌کند، اصل Open/Closed را تقویت می‌کند و با ساختار Dependency Injection در دات‌نت سازگاری بسیار خوبی دارد.

چک‌لیست سریع پیاده‌سازی

  1. یک Interface برای عملیات اصلی تعریف کنید.
  2. سرویس اصلی را بدون نگرانی‌های جانبی پیاده‌سازی کنید.
  3. یک Decorator پایه بسازید که نمونه داخلی Interface را نگه دارد.
  4. برای هر قابلیت جانبی یک Decorator مستقل ایجاد کنید.
  5. ترتیب اجرای لایه‌ها را مشخص کنید.
  6. زنجیره را به‌صورت دستی یا با Dependency Injection بسازید.
  7. رفتار هر Decorator و کل زنجیره را آزمایش کنید.

 

0 نظر

نظر محترم شما در مورد مقاله های وب سایت برنامه نویسی و پایگاه داده

نظرات محترم شما در خدمات رسانی بهتر ما را یاری می نمایند. لطفا اگر مایل بودید یک نظر ما را مهمان فرمائید. آدرس ایمیل و وب سایت شما نمایش داده نخواهد شد.

حرف 500 حداکثر