زمان تخمینی مطالعه: 14 دقیقه
الگوی Decorator به کاربر اجازه میدهد تا عملکرد جدیدی را به یک شیء موجود بدون تغییر ساختار آن اضافه کند. این نوع الگوی طراحی تحت الگوهای ساختاری قرار میگیرد زیرا این الگو به عنوان یک پوشش برای یک کلاس موجود عمل میکند. این الگو یک کلاس دکوراتور ایجاد میکند که کلاس اصلی را میپوشاند و عملکرد اضافی را ارائه میکند تا امضای متدهای کلاس را دست نخورده نگه دارد.
بیان مسئله: تصور کنید که روی یک کتابخانه notification(اعلان) کار میکنید که به برنامههای دیگر اجازه میدهد تا کاربران خود را در مورد رویدادهای مهم مطلع کنند. نسخه اولیه کتابخانه مبتنی بر کلاس Notifier است که فقط چند فیلد، سازنده و یک متد send واحد دارد. این متد میتواند یک آرگومان پیام را از یک کلاینت بپذیرد و پیام را به لیستی از ایمیلهایی که از طریق سازنده آن به اطلاعدهنده(notifier) ارسال شدهاند ارسال کند. یک برنامه شخص ثالث که به عنوان یک کلاینت عمل میکرد قرار بود یک بار شی اطلاعدهنده(notifier) را ایجاد و پیکربندی کند و سپس هر بار که اتفاق مهمی رخ میداد از آن استفاده کند.
در برخی مواقع متوجه میشوید که کاربران کتابخانه انتظار بیشتری از اعلانهای ایمیلی دارند. بسیاری از آنها مایلند پیامکی در مورد مسائل مهم دریافت کنند. دیگران مایلند در فیس بوک مطلع شوند و البته، کاربران شرکتی دوست دارند اعلان های Slack را دریافت کنند.
چقدر میتواند این کار دشوار باشد؟ شما کلاس Notifier را گسترش دادید و متدهای اعلان اضافی را در زیر کلاسهای جدید قرار دادید. حال قرار بود کلاینت کلاس اعلان مورد نظر را نمونه سازی کند و از آن برای تمام اعلانهای بعدی استفاده کند. اما اینجا یک سوال منطقی وجود دارد: «چرا نمی توان از چندین نوع اعلان به طور همزمان استفاده کرد؟»
برای حل این مشکل میتوان با ایجاد زیر کلاسهای ویژه که چندین روش اعلان را در یک کلاس ترکیب میکنند، آن مشکل را برطرف کرد. ولی با این حال، به سرعت آشکار میگردد که این رویکرد نه تنها کد کتابخانه بلکه کد مشتری را نیز به شدت مخدوش میکند.
برای حل مشکل ایجاد شده شما باید راه دیگری برای ساختاربندی کلاسهای اعلان پیدا کنید تا تعداد آنها به طور تصادفی بسیار زیاد نشوند.
زمانی که باید رفتار یک شی را تغییر دهید، گسترش یک کلاس اولین چیزی است که به ذهن خطور میکند. با این حال، مفهوم وراثت چندین اخطار جدی دارد که باید از آنها آگاه باشید.
- وراثت ثابت است. شما نمیتوانید رفتار یک شی موجود را در زمان اجرا تغییر دهید. شما فقط میتوانید کل شی را با شی دیگری که از یک زیر کلاس متفاوت ایجاد شده است جایگزین کنید.
- زیر کلاسها میتوانند فقط یک کلاس والد داشته باشند. در بیشتر زبانها، وراثت به یک کلاس اجازه نمیدهد رفتارهای چند کلاس را همزمان به ارث ببرد.
یکی از راههای غلبه بر این اخطارها استفاده از Aggregation یا Composition به جای Inheritance است. هر دو گزینه تقریباً به یک شکل عمل میکنند: یک شی ارجاع به شی دیگر دارد و کاری را به آن واگذار میکند، در حالی که با وراثت، شی خود قادر به انجام آن کار است و رفتار را از ابرکلاس خود به ارث میبرد. با این رویکرد جدید میتوانید به راحتی شی «helper» مرتبط را با دیگری جایگزین کنید و رفتار کانتینر را در زمان اجرا تغییر دهید. یک شی میتواند از رفتار کلاسهای مختلف استفاده کند، به چندین شیء ارجاع داشته باشد و انواع کارها را به آنها واگذار کند. تجمع/ترکیب، اصل کلیدی پشت بسیاری از الگوهای طراحی، از جمله الگوی Decorator است.
“Wrapper” نام مستعار جایگزین برای الگوی Decorator است که به وضوح ایده اصلی الگو را بیان میکند. Wrapper یک شی است که میتواند با یک شی هدف مرتبط شود. Wrapper شامل همان مجموعهای از متدها به عنوان هدف است و تمام درخواستهایی را که دریافت میکند به آن تفویض میکند. با این حال، wrapper ممکن است با انجام کاری قبل یا بعد از ارسال درخواست به هدف، نتیجه را تغییر دهد.
چه زمانی یک پوشاننده(wrapper) ساده تبدیل به دکوراتور واقعی میشود؟ همانطور که اشاره کردم، wrapper همان رابط کاربری شی پوشیده شده را اجرا میکند. به همین دلیل است که از دیدگاه مشتری، این اشیاء یکسان هستند. کاری کنید که فیلد مرجع wrapper هر شیئی را که از آن اینترفیس پیروی میکند بپذیرد. این موضوع به شما امکان میدهد یک شی را در چند پوشش بپوشانید و رفتار ترکیبی همه پوششها را به آن اضافه کنید.
در مثال نوتیفیکیشن، بیایید رفتار اعلان ایمیل ساده را در کلاس Notifier پایه بگذاریم، اما سایر روشهای اعلان را به دکوراتور تبدیل کنیم.
کد مشتری باید یک شی notifier اصلی را در مجموعهای از دکوراتورها بپیچد که با ترجیحات مشتری مطابقت دارد. اشیاء به دست آمده به عنوان یک پشته ساختار خواهند داشت.
آخرین دکوراتور در پشته، شیئی است که مشتری در واقع با آن کار میکند. از آنجایی که همه دکوراتورها اینترفیس یکسانی را با اعلانکننده پایه پیادهسازی میکنند، بقیه کد کلاینت اهمیتی نمیدهد که با شی اعلانکننده «خالص» یا تزئین شده(decorate) کار کند. ما میتوانیم همین رویکرد را برای متدهای دیگر مانند قالببندی پیامها یا تهیه فهرست گیرندگان اعمال کنیم. مشتری میتواند شی را با هر دکوراتور سفارشی تزئین کند، به شرطی که از رابط کاربری مشابه دیگران پیروی کند.
نمونه قابل قیاس در دنیای واقعی
پوشیدن لباس نمونهای از استفاده از دکوراتورها است. وقتی سردمان میشود، خودمان را در یک پلیور میپیچیم. اگر با ژاکت هنوز سردتان است، میتوانید یک ژاکت روی آن بپوشید. اگر باران میبارد، میتوانید یک کت بارانی بپوشید. همه این لباسها رفتارهای اصلی شما را «توسعه» میدهند، اما بخشی از شما نیستند، و میتوانید هر زمان که به آن نیاز ندارید، به راحتی هر لباسی را در بیاورید.
ساختار الگوی Decorator
در این بخش به بررسی ساختار الگوی دکوراتور(Decorator) میپردازیم و پیادهسازی آن را به صورت UML خواهیم دید.
- گام 1: کامپوننت، اینترفیس مشترک را هم برای wrapperها و هم برای اشیاء پیچیده شده تعریف میکند.
- گام 2: کامپوننتهای Concrete دستهای از اشیاء هستند که پوشانده میشوند. این رفتار اساسی را تعریف میکند که میتواند توسط دکوراتورها تغییر یابد.
- گام 3: کلاس Base Decorator یک فیلد برای ارجاع به یک شی پیچیده دارد. نوع فیلد باید به عنوان اینترفیس کامپوننت تعریف شود تا بتواند هم اجزای concrete و هم دکوراتورها را داشته باشد. دکوراتور پایه تمام عملیات را به شی پوشیده شده واگذار میکند.
- گام 4: دکوراتورهای concrete رفتارهای اضافی را تعریف میکنند که میتوانند به صورت پویا به اجزا اضافه شوند. دکوراتورهای concrete متدهای دکوراتور پایه را نادیده میگیرند و رفتار خود را قبل یا بعد از فراخوانی متد والد اجرا میکنند.
- گام 5: مشتری میتواند اجزاء را در چندین لایه دکوراتور بپیچد، تا زمانی که با همه اشیا از طریق اینترفیس کامپوننت کار کند.
شبه کد(Pseudocode)
در این مثال، الگوی Decorator به شما امکان میدهد دادههای حساس را مستقل از کدی که در واقع از این دادهها استفاده میکند فشرده و رمزگذاری کنید.
برنامه، شی منبع داده را با یک جفت دکوراتور میپوشاند. هر دو wrapper نحوه نوشتن و خواندن دادهها از دیسک را تغییر میدهند:
- درست قبل از اینکه دادهها روی دیسک نوشته شوند، دکوراتورها آن دادهها را رمزگذاری و فشرده میکنند. کلاس اصلی، دادههای رمزگذاری شده و محافظت شده را بدون اطلاع از تغییر در فایل مینویسد.
- درست پس از خواندن دادهها از روی دیسک، دادهها از همان دکوراتورها عبور میکند که آن را از حالت فشرده خارج کرده و رمزگشایی میکنند.
دکوراتورها و کلاس منبع داده، یک اینترفیس را پیادهسازی میکنند که همه آنها را در کد مشتری قابل تعویض میکند.
// The component interface defines operations that can be
// altered by decorators.
interface DataSource is
method writeData(data)
method readData():data
// Concrete components provide default implementations for the
// operations. There might be several variations of these
// classes in a program.
class FileDataSource implements DataSource is
constructor FileDataSource(filename) { ... }
method writeData(data) is
// Write data to file.
method readData():data is
// Read data from file.
// The base decorator class follows the same interface as the
// other components. The primary purpose of this class is to
// define the wrapping interface for all concrete decorators.
// The default implementation of the wrapping code might include
// a field for storing a wrapped component and the means to
// initialize it.
class DataSourceDecorator implements DataSource is
protected field wrappee: DataSource
constructor DataSourceDecorator(source: DataSource) is
wrappee = source
// The base decorator simply delegates all work to the
// wrapped component. Extra behaviors can be added in
// concrete decorators.
method writeData(data) is
wrappee.writeData(data)
// Concrete decorators may call the parent implementation of
// the operation instead of calling the wrapped object
// directly. This approach simplifies extension of decorator
// classes.
method readData():data is
return wrappee.readData()
// Concrete decorators must call methods on the wrapped object,
// but may add something of their own to the result. Decorators
// can execute the added behavior either before or after the
// call to a wrapped object.
class EncryptionDecorator extends DataSourceDecorator is
method writeData(data) is
// 1. Encrypt passed data.
// 2. Pass encrypted data to the wrappee's writeData
// method.
method readData():data is
// 1. Get data from the wrappee's readData method.
// 2. Try to decrypt it if it's encrypted.
// 3. Return the result.
// You can wrap objects in several layers of decorators.
class CompressionDecorator extends DataSourceDecorator is
method writeData(data) is
// 1. Compress passed data.
// 2. Pass compressed data to the wrappee's writeData
// method.
method readData():data is
// 1. Get data from the wrappee's readData method.
// 2. Try to decompress it if it's compressed.
// 3. Return the result.
// Option 1. A simple example of a decorator assembly.
class Application is
method dumbUsageExample() is
source = new FileDataSource("somefile.dat")
source.writeData(salaryRecords)
// The target file has been written with plain data.
source = new CompressionDecorator(source)
source.writeData(salaryRecords)
// The target file has been written with compressed
// data.
source = new EncryptionDecorator(source)
// The source variable now contains this:
// Encryption > Compression > FileDataSource
source.writeData(salaryRecords)
// The file has been written with compressed and
// encrypted data.
// Option 2. Client code that uses an external data source.
// SalaryManager objects neither know nor care about data
// storage specifics. They work with a pre-configured data
// source received from the app configurator.
class SalaryManager is
field source: DataSource
constructor SalaryManager(source: DataSource) { ... }
method load() is
return source.readData()
method save() is
source.writeData(salaryRecords)
// ...Other useful methods...
// The app can assemble different stacks of decorators at
// runtime, depending on the configuration or environment.
class ApplicationConfigurator is
method configurationExample() is
source = new FileDataSource("salary.dat")
if (enabledEncryption)
source = new EncryptionDecorator(source)
if (enabledCompression)
source = new CompressionDecorator(source)
logger = new SalaryManager(source)
salary = logger.load()
// ...
قابلیتها و کاربردها
- زمانی که باید بتوانید رفتارهای اضافی را به اشیا در زمان اجرا بدون شکستن کدی که از این اشیاء استفاده میکند، اختصاص دهید، از الگوی Decorator استفاده کنید.الگوی Decorator به شما امکان میدهد منطق کسب و کار خود را به صورت لایهای ساختاربندی کنید، برای هر لایه یک دکوراتور ایجاد کنید و در زمان اجرا، اشیاء را با ترکیبهای مختلف این منطق بسازید. کد کلاینت میتواند با همه این اشیاء به یک شکل رفتار کند، زیرا همه آنها از یک اینترفیس مشترک پیروی میکنند.
- هنگامی که گسترش رفتار یک شی با استفاده از وراثت ممکن نیست یا ناخوشایند است از الگوی Decorator استفاده کنید. بسیاری از زبانهای برنامه نویسی کلیدواژه final را دارند که میتوان از آن برای جلوگیری از گسترش بیشتر کلاس استفاده کرد. برای کلاس final، تنها راه استفاده مجدد از رفتار موجود این است که کلاس را با پوشش مخصوص خود، با استفاده از الگوی Decorator بپوشانید.
نحوه پیادهسازی
- مطمئن شوید که دامنه کسب و کار شما میتواند به عنوان یک مؤلفه اصلی با چندین لایه اختیاری روی آن نمایش داده شود.
- دریابید که چه متدهایی برای کامپوننت اصلی و لایههای اختیاری مشترک است. یک اینترفیس کامپوننت ایجاد کنید و آن متدها را در آنجا تعریف کنید.
- یک کلاس جزء concrete ایجاد کنید و رفتار پایه را در آن تعریف کنید.
- یک کلاس دکوراتور پایه ایجاد کنید. باید یک فیلد برای ذخیره ارجاع به یک شی پیچیده داشته باشد. فیلد باید با نوع اینترفیس مؤلفه تعریف شود تا امکان اتصال به اجزای concrete و همچنین decorator وجود داشته باشد. دکوراتور پایه باید تمام کارها را به شی پوشیده شده واگذار کند.
- مطمئن شوید که همه کلاسها اینترفیس کامپوننت را پیادهسازی میکنند.
- دکوراتورهای concrete را با گسترش آنها از دکوراتور پایه ایجاد کنید. یک دکوراتور concrete باید رفتار خود را قبل یا بعد از فراخوانی روش والد (که همیشه به شی پوشیده شده واگذار میکند) اجرا کند.
- کد مشتری باید مسئول ایجاد دکوراتورها و ترکیب آنها به روش مورد نیاز مشتری باشد.
مزایا و معایب الگوی Decorator
این الگوی طراحی دارای مزایا و معایبی به شرح زیر است:
– مزایا
- شما میتوانید رفتار یک شی را بدون ایجاد یک زیر کلاس جدید گسترش دهید.
- میتوانید در زمان اجرا مسئولیتهایی را از یک شی اضافه یا حذف کنید.
- شما میتوانید چندین رفتار را با قرار دادن یک شی در چند دکوراتور ترکیب کنید.
- اصل مسئولیت واحد: شما میتوانید یک کلاس یکپارچه را که بسیاری از انواع احتمالی رفتار را پیادهسازی میکند به چندین کلاس کوچکتر تقسیم کنید.
– معایب
- حذف یک پوشش خاص از پشته پوششها سخت است.
- اجرای دکوراتور به گونهای که رفتار آن به ترتیب در پشته دکوراتورها بستگی نداشته باشد دشوار است.
- ممکن است کد پیکربندی اولیه لایهها بسیار زشت به نظر برسد.
ارتباط با الگوهای دیگر
- الگوی Adapter یک اینترفیس کاملا متفاوت برای دسترسی به یک شی موجود فراهم میکند. از طرف دیگر، با الگوی Decorator اینترفیس یا ثابت میماند یا گسترش مییابد. علاوه بر این، Decorator از ترکیب بازگشتی پشتیبانی میکند، که با استفاده از الگوی آداپتور امکانپذیر نیست.
- با الگوی آداپتور شما از طریق اینترفیسهای مختلف به یک شی موجود دسترسی پیدا میکنید. با الگوی Proxy، اینترفیس ثابت میماند. با Decorator شما از طریق یک اینترفیس پیشرفته به شی دسترسی دارید.
- الگوی Chain of Responsibility(CoR) و Decorator ساختارهای کلاسی بسیار مشابهی دارند. هر دو الگو بر ترکیب بازگشتی تکیه دارند تا اجرا را از طریق یک سری اشیاء عبور دهند. با این حال، چندین تفاوت اساسی وجود دارد.
کنترل کنندههای CoR میتوانند عملیات دلخواه را مستقل از یکدیگر اجرا کنند. آنها همچنین میتوانند ارسال درخواست را در هر نقطهای متوقف کنند. از سوی دیگر، دکوراتورهای مختلف میتوانند رفتار شی را گسترش دهند و در عین حال آن را با اینترفیس پایه سازگار نگه دارند. علاوه بر این، دکوراتورها مجاز به شکستن جریان درخواست نیستند.
- الگوی Composite و Decorator نمودارهای ساختاری مشابهی دارند زیرا هر دو به ترکیب بازگشتی برای سازماندهی تعداد باز از اشیا متکی هستند.
الگوی Decorator مانند الگوی Composite است اما فقط یک جزء فرزند دارد. یک تفاوت مهم دیگر وجود دارد: دکوراتور مسئولیتهای بیشتری را به شی پوشیده شده اضافه میکند، در حالی که Composite فقط نتایج فرزندان خود را “خلاصه” میکند. با این حال، الگوها همچنین میتوانند همکاری کنند: میتوانید از Decorator برای گسترش رفتار یک شی خاص در درخت ترکیبی استفاده کنید.
- طرحهایی که به شدت از Composite و Decorator استفاده میکنند اغلب میتوانند از استفاده از الگوی Prototype بهره ببرند. اعمال الگو به شما این امکان را میدهد که ساختارهای پیچیده را به جای بازسازی مجدد از ابتدا شبیهسازی کنید.
- الگوی Decorator به شما امکان میدهد پوسته یک شی را تغییر دهید، در حالی که الگوی Strategy به شما امکان میدهد همه چیز را تغییر دهید.
- الگویDecorator و Proxy ساختارهای مشابهی دارند، اما اهداف بسیار متفاوتی دارند. هر دو الگو بر اساس اصل ترکیببندی ساخته شدهاند، جایی که یک شی قرار است بخشی از کار را به دیگری واگذار کند. تفاوت این است که یک Proxy معمولاً چرخه عمر شیء سرویس خود را به تنهایی مدیریت میکند، در حالی که ترکیب Decorators همیشه توسط مشتری کنترل میشود.