در این آموزش از سری آموزش الگوهای طراحی نرمافزار، قصد داریم به الگوی طراحی Decorator بپردازیم. الگوی طراحی Decorator یک الگوی طراحی ساختاری(Structural) است که به شما این امکان را میدهد تا با قرار دادن Objectها در پوششی مخصوص، رفتارهای جدیدی را به آنها متصل کنید.
[divider]
مشکل
تصور کنید که شما در حال کار بر روی یک کتابخانه اطلاع رسانی هستید که به سایر برنامه ها اجازه می دهد تا رویدادهای مهم را به کاربرانشان اطلاع دهند.
نسخه اولیه کتابخانه براساس کلاس Notifier است که فقط دارای چند فیلد ، یک Constructor و یک متد ارسال است. این متد ارسال می تواند یک پیام را به عنوان آرگومان ورودی از مشتری بپذیرد و این پیام را به لیستی از ایمیلهایی که از طریق Constructor این کلاس در اختیارش قرار گرفته، ارسال کند. یک برنامه شخص ثالث که به عنوان مشتری عمل می کند قرار است یکبار Object ای از این کتابخانه اعلان را ایجاد و پیکربندی کند، و هر بار که اتفاق مهمی رخ میدهد، از این Object استفاده کند.
در بعضی از مواقع، می فهمید که کاربران کتابخانه بیش از اعلان های ایمیلی انتظار دارند. بسیاری از آنها مایل به دریافت پیام کوتاه درباره موضوعات مهم هستند. دیگران دوست دارند در فیس بوک به آنها اطلاع داده شود و البته کاربران شرکت ها دوست دارند اعلان ها را در Slack دریافت کنند.
این تغییرات چقدر میتواند سخت باشد؟ شما کلاس اعلان را گسترش داده و روشهای اعلان اضافی را در زیر کلاسهای جدید قرار داده اید. حال مشتری که قرار بود کلاس اعلان مورد نظر را فورا مورد استفاده قرار دهد و از آن برای همه اعلان ها استفاده کند، میتواند از روشهای مختلف این اعلانها را به کاربران برساند.
اما پس از آن کسی به طور منطقی از شما سؤال خواهد کرد که ، “چرا نمی توانید به طور همزمان چندین نوع اعلان استفاده کنید؟ اگر خانه شما در آتش است، احتمالاً می خواهید از طریق هر کانال ممکنی مطلع شوید. ”
شما سعی کردید با ایجاد زیر کلاسهای خاص که چندین روش اطلاع رسانی را در یک کلاس با هم ترکیب می کند، به آن مشکل رسیدگی کنید اما با این حال، به سرعت آشکار شد که این روش نه تنها کد کتابخانه بلکه کد سرویس گیرنده را نیز بسیار زیاد و حجیم خواهد کرد.
شما باید روش دیگری برای ساخت کلاسهای اعلان پیدا کنید تا تعداد آنها به طور اتفاقی رکورد گینس را نشکند.
[divider]
راهحل
گسترش یک کلاس با کمک وراثت، اولین چیزی است که وقتی باید رفتار یک شی را تغییر دهید، به ذهن خطور می کند. اما، وراثت دارای چندین نکته جدی است که باید از آنها آگاه باشید.
وراثت ایستا است. شما نمی توانید رفتار یک شی موجود را در زمان اجرا تغییر دهید. فقط می توانید کل شیء را با یک مورد دیگر که از زیر کلاس های مختلف ایجاد شده است جایگزین کنید.
زیر کلاس ها می توانند فقط یک کلاس والد داشته باشند. در اکثر زبانها ، وراثت اجازه نمی دهد که یک کلاس رفتارهای چند کلاس را به طور هم زمان به ارث ببرد.
یکی از راه های غلبه بر این محدودیتها ، استفاده از Agregregation یا Composition است.
به جای وراثت هر دو روش جایگزین تقریباً به یک شکل کار می کنند: یک شیء به دیگری ارجاع می دهد و برخی از قسمتهای کار را به آن واگذار می کند ، در حالی که با ارثبری، تنها خود شیء قادر به انجام آن کار است، و رفتار را از کلاس والد خود به ارث می برد. با استفاده از این رویکرد جدید می توانید به راحتی شیء helper “مرتبط” را با دیگری جایگزین کرده و رفتار را در زمان اجرا تغییر دهید. یک شی می تواند از رفتار طبقات مختلف استفاده کند، به منابع مختلف مراجعه کرده و انواع کارها را به آنها واگذار کند.
Agregregation / Composition یک اصل مهم در پشت پرده بسیاری از الگوهای طراحی از جمله Decorator است. در این یادداشت ، اجازه دهید به بحث الگوی طراحی Decorator برگردیم.
Wrapper نام مستعار جایگزین برای الگوی طرحی Decorator است که به وضوح ایده اصلی این الگو را بیان می کند. “wrapper” پوششی است که می تواند با دربرگرفتن اشیاء “هدف” با آنها در ارتباط باشد. wrapper شامل مجموعه ای از متدهای مشابه شیء هدف است و تمام درخواست هایی که دریافت میکند، به آن شیء هدف واگذار می کند. با این حال، ممکن است wrapper نتیجه کار را با انجام کاری، قبل یا بعد از انتقال درخواست به شیء هدف تغییر دهد.
چه زمانی یک wrapper ساده به الگوی طراحی Decorator واقعی تبدیل می شود؟ همانطور که اشاره کردم، wrapper از همان interface ای پیروی میکند که شیء هدف(دربر گرفته شده) از آن پیروی میکند. به همین دلیل از منظر مشتری این دو(شی هدف و wrapper) یکسان هستند. قسمت ارجاع به شیء هدف در wrapper را باید طوری تغییر دهید که هر شیئی که از رابط پیروی کند، امکان قرار گرفتن در این مکان را داشته باشد. با این کار می توانید یک شیء را در چندین wrapper قرار دهید زیرا wrapper هم از همان رابط پیروی میکند. در نتیجه میتوان رفتار ترکیبی همه wrapper ها را به شی مربوطه اضافه کرد.
در مثال اعلان های ما، اجازه دهید رفتار ساده اعلان نامه الکترونیکی را در داخل کلاس Notification base بگذاریم، اما سایر روش های اعلان را به Decoratorها تبدیل کنیم.
کد مشتری باید یک شیء اعلان اولیه را به مجموعه ای از decorator ها مطابق با ترجیحات مشتری بپیوند دهد. اشیاء حاصل به صورت پشته(stack) ساختار خواهند یافت. به دیاگرام زیر دقت کنید. میتوانید در هنگام اجرای برنامه، بسته به این که کدام سرویسهای ارسال در دسترس هستند، پشتهی مناسبی ایجاد کرد. مقایسه کنید با حالتی که تلاش داشتیم با وراثت این پشته را ایجاد کنیم. ایستا بودن وراثت اینجا نمایان میشود. وقتی از وراثت استفاده کنید، نمیتوانید هر ترکیب دلخواهی از روش های ارسال را بسته به شرایط موجود در زمان اجرای برنامه ایجاد کنید. زیرا پیادهسازی زنجیرهای از روشهای ارسال با وراثت، همیشه ترتیبی ایستا خواهد داشت.
آخرین decorator در پشته یک شی خواهد بود که مشتری در واقع با آن کار می کند. از آنجا که همه decorator ها همان رابط را به عنوان Notifier پیاده سازی می کنند، کد مشتری دیگر اهمیتی نمی دهند که آیا دارد با شیء اعلان “خالص” کار میکند یا با یک شیء تزئین شده.
ما می توانیم همین رفتار را برای سایر رفتارها مانند قالب بندی پیام یا ترکیب لیست گیرنده اعمال کنیم. مشتری می تواند با هر دکوراسیون سفارشی ، شی را تزیین کند ، مادامی که decorator ها همان رابط را دنبال کند.
[divider]
مثالی از الگوی Decorator در دنیای واقعی
پوشیدن لباس نمونه ای از استفادهی دکوراتورها است. وقتی سردتان شد، خود را در لباس گرم میپیچید. اگر هنوز هم با لباس گرم سردتان است، می توانید یک ژاکت روی لباسهایتان بپوشید. اگر باران ببارد ، می توانید یک پالتوی بارانی بپوشید. همه این لباس ها رفتار اصلی شما را “گسترش می دهند” اما بخشی از شما نیستند و هر زمان که نیازی به هر کدام از آنها ندارید، می توانید به راحتی آن لباس ها را از تن دربیاورید.
[divider]
ساختار الگوی طراحی Decorator
۱- رابط Component همان رابط مشترک میان wrapper ها و اشیای خالصی است که قرار است در داخل wrapperها دربرگرفته شوند.
۲- کلاس Concrete Component همان کلاسی است که قرار است اشیای ساخته شده از آن در داخل wrapperها دربر گرفته شوند.
۳- کلاس Base Decorator زمینه ای برای ارجاع به یک شیء پیچیده شده دارد. نوع فیلد باید از نوع رابط Component تعریف شود تا بتواند حاوی ConcreteComponent و ConcreteDecorator ها باشد. Base Decorator تمام عملیات را به شیء بسته بندی شده واگذار می کند.
۴- Concrete Decorator ها رفتارهای اضافی را تعریف می کنند که می توان به صورت پویا به اشیای خالص یا تزیین شده، افزود. Concrete decorator ها متدهای دکوراتور پایه را بازنویسی کرده و رفتار خود را قبل یا بعد از فراخوانی متد والدین انجام می دهند.
۵- client می تواند componentها را در چند لایه از دکوراتورها بپیچاند، تا زمانی که با تمام اشیاء از طریق رابط Component تعامل کند.
[divider]
موفق و پیروز باشید…