امین احسان
امین احسان
خواندن ۶ دقیقه·۲ سال پیش

الگوی طراحی دکوراتور - Decorator Design Pattern

Decorator Design Pattern
Decorator Design Pattern


بررسی

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


مشکل

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

نسخه اولیه کتابخانه بر اساس کلاس Notifier است و فقط دارای چند فیلد (یک متد سازنده و یک متد ارسال) می‌باشد.

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

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

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

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

اما پس از مدتی، از شما سوال خواهد شد که (چرا نمی‌توان به‌طور همزمان از چندین روش اعلان استفاده کرد) اگر خانه شما آتش‌گرفته است احتمالاً می‌خواهید از طریق هر کانال مطلع شوید.

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

بنابراین شما باید روش دیگری برای ساخت کلاس‌های Notifier پیدا کنید.


راه حل

گسترش یک کلاس با کمک وراثت اولین چیزی است که وقتی باید رفتار یک شی را تغییر دهید به ذهن خطور می‌کند. اما وراثت دارای چندین نکته است که باید از آنها آگاه باشید:

  • وراثت ایستا است شما نمی‌توانید رفتار یک شی موجود را در زمان اجرا تغییر دهید. فقط می‌توانید کل شی را با مورد دیگری که از یک زیر کلاس متفاوت ایجاد شده است جایگزین کنید.
  • زیر کلاس‌ها در (اکثر زبانها) می‌توانند فقط یک کلاس والد داشته باشند. وراثت اجازه نمی‌دهد که یک کلاس رفتارهای چند کلاس را به طور همزمان به ارث ببرد.

یکی از راه‌های غلبه بر این محدودیت‌ها استفاده از Aggregation یا Composition است.

  • تجمع - Aggregation: شی A شامل اشیا B است B می‌تواند بدون A زندگی کند.
  • ترکیب - Composition: شی A از اشیا B تشکیل شده است A چرخه زندگی B را مدیریت می‌کند B نمی‌تواند بدون A زندگی کند.

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

یک شی ارجاع به دیگری دارد و کاری را به آن محول می‌کند، در حالی که (با وراثت شی خود قادر به انجام آن کار است و رفتار را از کلاس والد خود به ارث می‌برد)

با این رویکرد جدید می‌توانید به راحتی شی مرتبط را با دیگری جایگزین کنید و رفتار را در زمان اجرا تغییر دهید. یک شی می‌تواند از رفتار کلاس‌های مختلف استفاده کند و به چندین شی ارجاع داشته باشد و انواع کارها را به آنها واگذار کند.

(تجمع / ترکیب اصل کلیدی پشت بسیاری از الگوهای طراحی از جمله Decorator است)

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

چه زمانی یک Wrapper ساده به الگوی Decorator واقعی تبدیل می‌شود؟

همانطور که اشاره شد. Wrapper از همان Interface پیروی می‌کند که شی هدف (دربرگرفته شده) از آن پیروی می‌کند. به همین دلیل است که از منظر کلاینت این اشیا یکسان هستند (پس فیلد ارجاع Wrapper به شی هدف را باید طوری تغییر دهید که هر شی با پیروی از Interface مربوطه پذیرفته شود) با این کار می‌توان یک شی را در چندین Wrapper قرار داد و رفتار ترکیبی همه Wrapper را به آن اضافه نمود.

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

کد کلاینت باید شی Notifier کننده اصلی را در مجموعه ای از Decorator ها قرار دهد که با ترجیحات کلاینت مطابقت دارد، سپس اشیا به دست آمده ساختار پشته خواهند داشت.

آخرین Decorator در پشته، شیی است که در واقع کلاینت با آن کار می‌کند. از آنجایی که همه، Decorator Interface یکسانی را با Notifier اجرا می‌کنند، بقیه کد کلاینت اهمیتی نمی‌دهد که با شی Notifier که pure است کار میکند یا Decorat شده.

ما می‌توانیم همین رویکرد را برای رفتارهای دیگر مانند قالب‌بندی پیام‌ها یا تهیه فهرست گیرندگان اعمال کنیم. کلاینت می‌تواند شی را با هر Decorator سفارشی Decorat کند. (به شرطی که از Interface مشابه پیروی داشته باشد)


ساختار

  • بخش Component همان Interface مشترک را برای (در برگیرنده) و (اشیا در برگرفته شده) مشخص می‌کند.
  • بخش Concrete Component دسته‌ای از اشیا است که در برگرفته می‌شوند. (رفتار پایه را تعریف می‌کند که می‌تواند توسط Decorator ها تغییر یابد)
  • بخش Base Decorator یک فیلد برای ارجاع به یک شی در برگرفته شده دارد. نوع فیلد باید نوع Interface تعریف شود تا بتواند حاوی Concrete Component و Decorator ها باشد. Base Decorator هم تمام عملیات را به شی در برگرفته شده واگذار می کند.
  • بخش Concrete Component ها رفتارهای بیشتر را تعریف می‌کنند که می‌توانند به صورت پویا اضافه شوند. آن‌ها روش‌های Base Decorator را نادیده می‌گیرند و رفتار خود را قبل یا بعد از فراخوانی اجرا می‌کنند.
  • کلاینت می‌تواند Component ها را در چندین لایه Decorat کننده، در برگیرد (تا زمانی که با همه اشیا از طریق Interface تعامل دارند)


نمونه

پوشیدن لباس نمونه‌ای از استفاده‌ Decorator ها می‌باشد.

وقتی سردتان می‌شود لباس گرم می‌پوشید. اگر هنوز هم سردمان باشد می‌توانید یک ژاکت هم روی لباس‌هایتان بپوشید. یا اگر باران ببارد می‌توانید یک پالتوی بارانی بپوشید. همه این لباس‌ها رفتار اصلی شما را (گسترش می‌دهند) اما بخشی از شما نیستند، و هر زمان که نیازی به هر کدام از آن‌‌ها ندارید می‌توانید به راحتی هر لباس را از تن دربیاورید.


نتیجه

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

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

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

بسیاری از زبان‌های برنامه‌نویسی کلیدواژه Final را دارند، که می‌توان از آن برای جلوگیری از گسترش بیشتر کلاس‌ها استفاده کرد. اما برای استفاده مجدد از رفتارهای این کلاس‌ها تنها راه، این است که آن‌ها را با در برگیرنده مخصوص با استفاده از الگوی Decorator در برگیریم.


linkedin.com/in/AminEhsan

github.com/AminEhsan

مهندسی نرم افزاربرنامه نویسیالگوی طراحیdesign patterndecorator
مجموعه مقالات با هدف تحلیل و بررسی علوم کامپیوتر
شاید از این پست‌ها خوشتان بیاید