توضیح دوات: برای راهنمایی و اطلاعات بیشتر در مورد نمودار کلاس به این مقاله مراجعه کنید.
الگوی طراحی Decorator یک الگوی ساختاری است که به ما امکان میدهد رفتارهای جدیدی را به اشیاء اضافه کنیم، بدون اینکه کد اصلی را تغییر دهیم. این کار با قرار دادن این اشیاء درون بستهبندیهای خاص انجام میشود. این بستهبندیها رفتار موردنظر را اضافه میکنند، بدون اینکه تغییری در کد اصلی ایجاد شود.
الگوی طراحی Decorator یک ابزار کاربردی است وقتی که میخواهیم شیء خاصی را با رفتارهای اضافی ارتقا دهیم، اما نمیخواهیم یا نمیتوانیم به ساختار داخلی آن دست بزنیم. با استفاده از این الگو، میتوانیم شیء را در یک بستهبندی تخصصی قرار دهیم و عملکرد موردنظر را در همان بستهبندی پیادهسازی کنیم.
میتوانید کد نمونه این مطلب را در GitHub پیدا کنید.
مفهومسازی مسئله
تصور کنید که یک کتابخانه اعلان (Notification) داریم که به ما امکان میدهد کاربران را در مورد رویدادهای مرتبط با استقرار (Deployment) مطلع کنیم.
نسخه اولیه این کتابخانه بر اساس یک کلاس Notifier
طراحی شده که شامل چند فیلد مانند لیست آدرسهای ایمیل، یک سازنده و یک متد Notify
است. متد Notify
یک آرگومان پیام از کلاینت میگیرد و آن را به لیستی از ایمیلها که از طریق سازنده Notifier
ارائه شده، ارسال میکند. یک برنامه دیگر بهعنوان کلاینت عمل میکند و Notifier
را یک بار تنظیم کرده و هر بار که اتفاق مهمی در جریان کاری استقرار رخ دهد، از آن استفاده میکند.
این کتابخانه برای مدتی بدون مشکل کار میکرد تا اینکه یک مشکل پیش آمد. در جریان یک انتشار شبانه، ساختار شکست خورد و ایمیلها ارسال شدند اما هیچکس در دسترس نبود تا به مشکل رسیدگی کند. بههرحال، بیشتر افراد نیمهشب ایمیل خود را بررسی نمیکنند.
اکنون کاربران چیزی فراتر از اعلانهای ایمیلی میخواهند. بسیاری از آنها دوست دارند اعلانهای SMS درباره مسائل بحرانی دریافت کنند. برخی دیگر مایل به دریافت اعلانهای Microsoft Teams هستند و تیمهای DevOps هم به دلایل مختلف اعلانهای Slack را ترجیح میدهند.
مشکلی نیست! میتوانیم کلاس Notifier
را گسترش دهیم و متدهای اعلان اضافی را به زیردستههای جدید اضافه کنیم. اکنون کلاینت میتواند کلاس اعلان مناسب را ایجاد کرده و برای اعلانهای بعدی از آن استفاده کند.
اما سپس کسی سؤال حیاتی را مطرح کرد: “چرا نمیتوان از چندین اعلان بهطور همزمان استفاده کرد؟ اگر کشتی در حال غرق شدن است، احتمالاً دوست دارید از طریق همه کانالها مطلع شوید.”
با استفاده از راهحل قبلی، میتوانیم با ایجاد زیردستههای خاصی که چندین متد Notifier
را در یک کلاس ترکیب میکنند، به این مسئله پاسخ دهیم. حالا زمان یک محاسبه ساده است. هر متد اعلان میتواند وجود داشته باشد یا نداشته باشد. بنابراین برای وجود یک متد اعلان دو حالت وجود دارد. در حال حاضر ۳ متد اعلان مختلف داریم. پس ۲ به توان ۳ ترکیب مختلف از زیردستهها خواهیم داشت یا ۸ کلاس. اگر متد اعلان دیگری اضافه کنیم، ۲ به توان ۴ زیردسته مختلف یا ۱۶ کلاس خواهیم داشت. واضح است که این رویکرد باعث افزایش نمایی کدها خواهد شد.
گسترش یک کلاس اولین چیزی است که وقتی نیاز به تغییر رفتار یک شیء داریم به ذهن میرسد. اما ارثبری (Inheritance) مشکلات خاصی دارد:
- ارثبری ایستا است. نمیتوانیم رفتار یک شیء موجود را در زمان اجرا تغییر دهیم. تنها کاری که میتوان انجام داد جایگزین کردن نمونه با یک نمونه دیگر است که از یک زیردسته متفاوت ایجاد شده است.
- زیرکلاسها فقط میتوانند یک کلاس والد داشته باشند. در C#، ارثبری به کلاس اجازه نمیدهد که رفتارها را از چند کلاس بهطور همزمان به ارث ببرد.
یکی از راههای غلبه بر این محدودیتها استفاده از ترکیب (Composition) یا تجمیع (Aggregation) بهجای ارثبری است. این دو جایگزین تقریباً مشابه هستند. در ترکیب و تجمیع، یک نمونه به نمونه دیگری ارجاع داده و بخشی از کار را به آن واگذار میکند، درحالیکه در ارثبری، خود نمونه کار را اجرا میکند و رفتار را از والد خود به ارث میبرد.
با این رویکرد جدید، میتوان بهراحتی شیء متصلشده را با یک شیء دیگر جایگزین کرد و رفتار ظرف را در زمان اجرا تغییر داد. ترکیب و تجمیع تکنیکهای کلیدی بسیاری از الگوهای طراحی، از جمله Decorator هستند.
یک Decorator، که بهعنوان Wrapper نیز شناخته میشود، میتواند به یک شیء هدف متصل شود. این Wrapper میتواند تمام درخواستهایی را که دریافت میکند به هدف ارسال کند. بااینحال، Wrapper میتواند نتیجه را با پردازش درخواست پیش از ارسال به هدف یا تغییر پاسخ پس از بازگشت نتیجه از هدف تغییر دهد.
ساختار الگوی طراحی Decorator
نمودار زیر نحوه کار الگوی Decorator را نشان میدهد.
- برنامه یک درخواست ارسال میکند و کلاس Decorator آن را دریافت میکند.
- کلاس Decorator میتواند درخواست را پیش از ارسال به کلاس بستهبندیشده پردازش کند.
- کلاس بستهبندیشده عملکرد خود را بهطور معمول انجام میدهد، بدون اطلاع از کلاس Decorator.
- کلاس Decorator میتواند پاسخ را پیش از ارسال به برنامه پردازش کند.
- Decorator نتیجه را به فراخواننده اصلی بازمیگرداند.
در پیادهسازی پایه، الگوی Decorator دارای چهار شرکتکننده است:
- Component: Component رابطهای مشترک برای هر دو بستهبندیها و اشیاء بستهبندیشده را تعریف میکند.
- Concrete Components: Concrete Component کلاسی از اشیاء است که بستهبندی خواهند شد. این کلاس رفتار اصلی را تعریف میکند که میتواند توسط Decorators تغییر کند.
- Base Decorator: Base Decorator به شیء بستهبندیشده ارجاع میدهد. این Decorator تمام عملیات را به شیء بستهبندیشده واگذار میکند.
- Concrete Decorators: Concrete Decorators رفتارهای اضافی را تعریف میکنند که میتوانند بهصورت پویا به Concrete Components اضافه شوند.
- Client: Client میتواند اجزاء را در لایههای متعدد Decorators بستهبندی کند، مادامی که با اشیاء از طریق یک رابط مشترک کار کند.
برای نشان دادن نحوه عملکرد الگوی Decorator، یک رستوران شیک به سبک مزرعه تا میز باز خواهیم کرد.
ایده پشت این رستوران این است که غذاها از موادی تهیه شوند که مستقیماً از تولیدکننده به دست میآیند. این موضوع به این معناست که ممکن است برخی از غذاها بهعنوان ناموجود علامتگذاری شوند، زیرا مزرعه میتواند تنها مقدار محدودی از مواد اولیه تولید کند.
برای شروع، شرکتکننده Component خود را پیادهسازی خواهیم کرد، که همان کلاس انتزاعی Dish
است:
namespace Decorator.Components
{
///
/// The abstract Component class
///
public abstract class Dish
{
public abstract void Display();
}
}
همچنین به چند کلاس شرکتکننده ConcreteComponent نیاز داریم که نماینده غذاهای خاصی هستند که رستوران ما میتواند ارائه دهد. این کلاسها فقط به مواد اولیه یک غذا اهمیت میدهند و نه به تعداد غذاهای موجود. این مسئولیت بر عهده Decorator است.
namespace Decorator.Components
{
///
/// A ConcreteComponent class
///
public class Salad : Dish
{
private readonly string _veggies;
private readonly string? _cheeses;
private readonly string? _dressing;
public Salad(string veggies, string? cheeses, string? dressing)
{
_veggies = veggies;
_cheeses = cheeses;
_dressing = dressing;
}
public override void Display()
{
Console.WriteLine("\nSalad:");
Console.WriteLine($" Veggies: {_veggies}");
Console.WriteLine($" Cheeses: {_cheeses}");
Console.WriteLine($" Dressing: {_dressing}");
}
}
}
namespace Decorator.Components
{
///
/// A ConcreteComponent class
///
public class Pasta : Dish
{
private readonly string _pasta;
private readonly string _sauce;
public Pasta(string pasta, string sauce)
{
_pasta = pasta;
_sauce = sauce;
}
public override void Display()
{
Console.WriteLine("\nPasta: ");
Console.WriteLine($" Pasta: {_pasta}");
Console.WriteLine($" Sauce: {_sauce}");
}
}
}
اکنون، باید پیگیری کنیم که آیا یک غذا موجود است یا خیر. برای این کار ابتدا یک کلاس AbstractDecorator
پیادهسازی میکنیم که همان شرکتکننده Decorator ما است.
namespace Decorator.Decorators
{
///
/// The Abstract Base Decorator
///
public abstract class AbstractDecorator : Dish
{
protected Dish _dish;
protected AbstractDecorator(Dish dish)
{
_dish = dish;
}
public override void Display()
{
_dish.Display();
}
}
}
در نهایت، به یک شرکتکننده ConcreteDecorator نیاز داریم تا پیگیری کند چند عدد از هر غذا سفارش داده شده است. این نقش AvailabilityDecorator
است.
namespace Decorator.Decorators
{
public class AvailabilityDecorator : AbstractDecorator
{
public int AvailableItems { get; set; }
protected List customers = new();
public AvailabilityDecorator(Dish dish, int available) : base(dish)
{
AvailableItems = available;
}
public void OrderItem(string name)
{
if (AvailableItems > 0)
{
customers.Add(name);
AvailableItems--;
}
else
Console.WriteLine($"\nNot enough ingredients for {name}'s dish");
}
public override void Display()
{
base.Display();
foreach(string customer in customers)
Console.WriteLine($"Ordered by {customer}");
}
}
}
آخرین مرحله، تنظیم متد Main
است. ابتدا مجموعهای از غذاها را تعریف میکنیم و سپس آنها را تزئین میکنیم تا بتوانند موجودی خود را پیگیری کنند. در نهایت، غذاها را سفارش میدهیم.
Salad caesarSalad = new("Crisp Romaine Lettuce", "Parmesan Cheese", "Homemade Caesar Dressing");
caesarSalad.Display();
Pasta fetuccine = new("Homemade Fetuccine", "Creamy Garlic Alfredo Sauce");
fetuccine.Display();
Console.WriteLine("\nChanging availability of the dishes");
AvailabilityDecorator caesarAvailability = new(caesarSalad, 3);
AvailabilityDecorator pastaAvailability = new(fetuccine, 4);
caesarAvailability.OrderItem("Marion");
caesarAvailability.OrderItem("Thomas");
caesarAvailability.OrderItem("Imogen");
caesarAvailability.OrderItem("Jude");
pastaAvailability.OrderItem("Marion");
pastaAvailability.OrderItem("Thomas");
pastaAvailability.OrderItem("Imogen");
pastaAvailability.OrderItem("Jude");
pastaAvailability.OrderItem("Jacinth");
caesarAvailability.Display();
pastaAvailability.Display();
Console.ReadLine();
مشکل Decorator های سنگین
الگوی Decorator بر اساس قابلیت ترکیبپذیری طراحی شده است. به این معنا که کلاس مورد استفاده دوباره به ارث برده نمیشود، بلکه توسط کلاسهای Decorator بستهبندی میشود. این ویژگی با یک چالش همراه است: Decorator رابط عمومی کلاس استفادهشده را به ارث نمیبرد و باید بهصورت صریح هر متد رابط را پیادهسازی کند. ایجاد و نگهداری تمامی این متدها میتواند بار کاری زیادی ایجاد کند.
اگرچه ابزارهای توسعهای مانند ReSharper میتوانند در مدیریت این متدها کمک کنند، اما مسئولیت نگهداری این موارد همچنان بر عهده برنامهنویس است.
مزایا و معایب الگوی طراحی Decorator
مزایا
- میتوان رفتار یک شیء را بدون تغییر در کد منبع آن گسترش داد.
- میتوان رفتار یک شیء را بدون ایجاد زیردسته جدید گسترش داد.
- میتوان چندین Decorator را ترکیب کرد تا رفتارهای متعددی به یک شیء اضافه شود.
- میتوان یک کلاس یکپارچه را به چند کلاس تقسیم کرد که هرکدام رفتار خاص خود را دارند و به این ترتیب اصل مسئولیت واحد را رعایت کرد.
معایب
- حذف یک بستهبندی از میان یک پشته بستهبندی دشوار است.
- پیادهسازی Decoratorها بهگونهای که به ترتیب قرارگیری آنها در پشته وابسته نباشد، سخت است.
- Decorator های سنگین ممکن است نیاز به پیادهسازی متدهایی داشته باشند که استفاده نمیشوند.
- پیکربندی اولیه لایهها ممکن است بسیار ناخوشایند به نظر برسد و نگهداری آن دشوار باشد.
روابط با سایر الگوها
Adapter و Decorator: هر دو الگو عملکردی مشابه دارند. با این حال، Adapter رابط یک شیء موجود را تغییر میدهد، درحالیکه Decorator یک شیء را بدون تغییر رابط آن ارتقا میدهد. همچنین Decorator از ترکیب بازگشتی پشتیبانی میکند، درحالیکه این امکان در Adapter وجود ندارد. در نهایت، Adapter رابط متفاوتی برای شیء بستهبندیشده فراهم میکند، اما Decorator رابط بهبودیافتهای ارائه میدهد.
Chain of Responsibility و Decorator: این دو الگو ساختارهای کلاسی مشابهی دارند و هر دو به ترکیب بازگشتی متکی هستند. بااینحال، تفاوتهایی وجود دارد. Handler در CoR میتواند عملیات دلخواهی را بدون وابستگی به سایر Handlerها در زنجیره اجرا کند. اما Decorator میتواند رفتار شیء را گسترش دهد و در عین حال آن را با رابط پایه سازگار نگه دارد. همچنین، Decoratorها اجازه ندارند جریان درخواست را مختل کنند.
Composite و Decorator: این دو الگو ساختار مشابهی دارند، زیرا هر دو بر ترکیب بازگشتی متکی هستند. Decorator مانند Composite است اما فقط یک مؤلفه فرزند دارد. علاوه بر این، Decorator مسئولیتهای اضافی به شیء بستهبندیشده اضافه میکند، درحالیکه Composite فقط نتایج فرزندان خود را جمعبندی میکند.
Decorator و Proxy: این دو الگو ساختارهای مشابهی دارند اما اهداف بسیار متفاوتی را دنبال میکنند. هر دو الگو بر اصل ترکیب بنا شدهاند. بااینحال، یک Proxy معمولاً چرخه عمر شیء سرویس خود را بهطور مستقل مدیریت میکند، درحالیکه ترکیب Decorator همیشه توسط کلاینت کنترل میشود.
جمعبندی
در این مقاله بررسی کردیم که الگوی طراحی Decorator چیست، چه زمانی باید از آن استفاده کرد و مزایا و معایب استفاده از این الگو چیست. سپس مفهوم Decorator سنگین را بررسی کرده و رابطه الگوی Decorator با سایر الگوهای طراحی کلاسیک را تحلیل کردیم.
شایان ذکر است که الگوی طراحی Decorator، همانند سایر الگوهای طراحی ارائهشده توسط Gang of Four، یک راهحل همهجانبه یا معجزهآسا برای طراحی نرمافزار نیست. انتخاب زمان استفاده از این الگوها به عهده مهندسان است. در نهایت، این الگوها زمانی مفید هستند که بهعنوان ابزاری دقیق استفاده شوند، نه یک ابزار سنگیندست.