درود دوستان! میخوایم یکی از کاربردیترین الگوها یعنی الگوی Decorator رو بررسی کنیم که با اون میتونیم به صورت داینامیک و در زمان Runtime به آبجکتها ویژگی اضافه کنیم و رفتار اونها رو تغییر بدیم.
میخوایم یاد بگیریم که:
- مشکل کجاست
- الگوی Decorator چیه
- چرا به این الگو میگن Wrapper
- چطوری الگوی Decorator رو پیادهسازی کنیم
- مزایای الگوی Decorator
- معایب الگوی Decorator
مشکل کجاست؟ 🤔
فرض کنیم یک هتل داریم و میخوایم قابلیتی رو اضافه کنیم که کاربرها بتونن به صورت اینترنتی اتاق رزرو کنن. توی ورژن اولیه برنامه، کاربر فقط میتونه اتاق رو بدون ویژگیهای اضافی سفارش بده:
class SimpleRoom { getDescription() { return "Base room"; } getPrice() { return 2.00; } } const order = new SimpleRoom();
حالا میخوایم برنامه رو توسعه بدیم و کاری کنیم که کاربر بتونه مشخص کنه سفارش چه ویژگیهایی داشته باشه (غذا، وایفای، استخر، منظره رو به دریا و ...). شاید راهی که به ذهنمون برسه این باشه که برای هر ویژگی، یک زیرکلاس از SimpleRoom بسازیم:
class RoomWithWifi { ... } class RoomWithBreakfast { ... } class RoomWithWifiAndBreakfast { ... } class RoomWithExtraBedAndWifi { ... } // ...
همونطور که میبینیم با اضافه کردن هر ویژگی، باید کلی زیرکلاس بسازیم که با این کار تعداد کلاسها به صورت تصاعدی بالا میره، مدیریت اونها سخت میشه و بنابراین اصلاً راه منطقی به حساب نمیاد. باید بتونیم ویژگیها رو در لحظه و طبق درخواست کاربر به همون آبجکت SimpleRoom اضافه کنیم. توی چنین شرایطی الگوی Decorator به کمک ما میاد 👋
الگوی Decorator چیه؟
این الگو به ما کمک میکنه تا به صورت داینامیک و در زمان اجرای کد (Run-time) ویژگیهایی رو به آبجکتها اضافه کنیم، بدون اینکه نیاز باشه کلاسها رو دستکاری کنیم و برای هر ویژگی زیرکلاس بسازیم. با این الگو میتونیم طبق درخواست کاربر، ویژگیها رو لایه به لایه و مرحله به مرحله به یک آبجکت اضافه کنیم.
ساختاری که این الگو میسازه شبیه به استک (پشته هست):
WiFi(Breakfast(TV(<<Room>>)));
اون Room آبجکت اصلی و پایهای ما هست که میخوایم به اون ویژگی اضافه کنیم و TV و Breakfast و WiFi ویژگیهایی هستن که روی این آبجکت بهترتیب و لایه به لایه اعمال میشن. به هر یک از این ویژگیها میگن Decorator. توی ادامه میخوایم چنین ساختاری رو پیادهسازی کنیم.
الگوی Decorator زیر مجموعه الگوهای Structural - الگوهایی که کمک میکنن تا آبجکتهای موجود طوری با هم در تعامل باشن تا بتونیم ساختارهای بزرگتری طراحی کنیم - هست. به این الگو Wrapper هم گفته میشه و پیادهسازی خیلی راحتی داره.
چرا به این الگو میگن Wrapper؟
به این دلیل که آبجکتهای موجود رو میگیره، به اونها ویژگی اضافه میکنه و نهایتاً ورژن بهبودیافته همون آبجکت رو برمیگردونه.
پیادهسازی الگوی Decorator
این الگو پیادهسازی راحتی داره. هرچند کد کامل این برنامه توی ادامه قرار گرفته، میخوایم مرحله به مرحله این الگو رو پیادهسازی کنیم.
مرحله اول: ساختن اینترفیس
ابتدا باید یک اینترفیس بسازیم و توی اون متدهایی رو تعریف کنیم که هم برای آبجکت اصلی و هم برای Decorator ها مشترک هستن. اسم این اینترفیس رو میذاریم RoomInterface:
interface RoomInterface { getDescription(); getPrice(); }
مرحله دوم: ساختن کلاسِ آبجکت اصلی
این کلاس، همون کلاسی هست که توی مثال ابتدایی داشتیم. یعنی کلاس SimpleRoom که سادهترین حالت آبجکت هست. اما باید از این اینترفیس تبعیت کنه:
class SimpleRoom implements RoomInterface { getDescription() { return "Base room"; } getPrice() { return 2.00; } }
مرحله سوم: ساختن کلاس Decorator اصلی
این کلاس، والدی هست برای Decorator ها و قراره آبجکت اصلی و یا بقیه Decorator ها رو توی خودش نگه داره. این کار رو با استفاده از فیلد خط ۲ و تابع constructor انجام میدیم. این کلاس هم باید از RoomInterface تبعیت کنه:
abstract class BaseDecorator implements RoomInterface { >> protected room: RoomInterface; constructor(room: RoomInterface) { this.room = room; } public getDescription() { return this.room.getDescription(); } public getPrice() { return this.room.getPrice(); } }
به این کلاس میگن Base Decorator. حالا برای هر ویژگی که قراره به آبجکت اصلی اضافه کنیم، باید یک زیرکلاس از این کلاس بسازیم. این کلاس به خودی خودش کاری انجام نمیده. فقط آبجکتی که میخوایم اون رو Wrap کنیم (ویژگی اضافه کنیم) رو توی خودش نگه میداره. کار اصلی، یعنی تغییر دادن و اضافه کردن ویژگی به آبجکت اصلی رو توی زیرکلاسها انجام میدیم.
مرحله چهارم: ساختن کلاسهای Decorator
گفتیم که این کلاسها، همون ویژگیهایی هستن که میخوایم به آبجکت اصلی اضافه کنیم. این کلاسها باید از کلاس BaseDecorator ارثبری کنن:
// Wifi class WiFiDecorator extends BaseDecorator { public getDescription() { return `${super.getDescription()} + WiFi`; } public getPrice() { return super.getPrice() + 0.2; } } // Breakfast class BreakfastDecorator extends BaseDecorator { public getDescription() { return `${super.getDescription()} + Breakfast`; } public getPrice() { return super.getPrice() + 2.00; } }
هر یک از این Decorator ها ویژگیهای مخصوص به خودشون رو به آبجکت اصلی اضافه میکنن. برای مثال توی خط ۸ میخواستیم قیمت آبجکت اصلی رو افزایش بدیم. ابتدا با super.getPrice() قیمت آبجکت اصلی رو کلاس والد گرفتیم و بعد با مقدار دلخواه اون رو افزایش دادیم.
ما اینجا ۲ تا کلاس (ویژگی) ساختیم. وایفای و صبحانه. هرچند میتونیم بینهایت دیگه کلاس (ویژگی) درست کنیم.
مرحله آخر: ساختن آبجکت اصلی و اضافهکردن ویژگیها
بعد از اینکه ساختار برنامه رو طراحی کردیم، حالا میتونیم از اونها توی قسمت کلاینت برنامه (قسمتی که از این کدها و کلاسها استفاده میکنه) استفاده کنیم.
ابتدا میخوایم سفارش یک اتاق ساده بدیم. آبجکت اصلی (اتاق) رو میسازیم:
var room = new SimpleRoom(); console.log('Description:', room.getDescription()); console.log('Price:', room.getPrice()); // Description: Base room // Price: 2
حالا میخوایم به این اتاق سرویس وایفای اضافه کنیم:
var room = new SimpleRoom(); >> room = new WiFiDecorator(room); console.log('Description:', room.getDescription()); console.log('Price:', room.getPrice()); // Description: Base room + WiFi // Price: 2.2
همونطور که میبینیم باید آبجکت اصلی رو به Decorator پاس میدادیم. با این کار متد constructor والد یعنی کلاس BaseDecorator اجرا میشه و آبجکت room نسبت داده میشه به فیلد room توی کلاس BaseDecorator.
مرحله بعد میخوایم صبحانه رو هم اضافه کنیم:
var room = new SimpleRoom(); room = new WiFiDecorator(room); >> room = new BreakfastDecorator(room); console.log('Description:', room.getDescription()); console.log('Price:', room.getPrice()); // Description: Base room + WiFi + Breakfast // Price: 4.2
به این شکل میتونیم بینهایت دیگه ویژگی به آبجکت اصلی اضافه کنیم:
var room = new SimpleRoom(); room = new WiFiDecorator(room); room = new ExtraBedDecorator(room); room = new AirConditionerDecorator(room); room = new ParkingDecorator(room);
همونطور که میبینیم، هنگام پیادهسازی شدن هر Decorator باید به اون آبجکتی از نوع RoomInterface پاس بدیم. هم آبجکت اصلی SimpleRoom و هم Decorator ها از نوع RoomInterface هستن. با این کار، یک ساختار تو در تو که به اصطلاح به اون میگن Recursive Composition شکل میگیره.
خب این الگو به همین سادگی بود. برای درک بهتر این الگو، پیشنهاد میکنم اون رو به زبان خودتون پیادهسازی و اتفاقها رو بررسی کنین.
(Star هم بزنین 👋)
درست مثل الگوی Composition، توی این الگو هم آبجکتها ساختاری به صورت تو در تو یا Recursive دارن. اما توی این الگو، هر Decorator میتونه فقط یک عضو فرزند داشته باشه. اما توی الگوی Composition هر عضو میتونه چندین زیر عضو داشته باشه. همچنین الگوی Decorator رفتار و خروجی زیر عضوها رو تغییر میده. اما الگوی Composition فقط با نتیجه و خروجی زیرعضوها کار داره.
الگوی Decorator شباهت زیادی به الگوی Adapter داره. اما وظیفهٔ الگوی Adapter اینه که یک اینترفیس ناسازگار رو برای برنامه سازگار کنه. همچنین ساختار تو در تو برای الگوی Adapter ممکن نیست.
مزایای الگوی Decorator
- همونطور که دیدیم، با این الگو تونستیم ویژگیها رو در زمان اجرای کد (Run-time) به آبجکت اضافه کنیم و یا از اون حذف کنیم. بدون اینکه نیاز باشه کلاسها رو دستکاری کنیم و یا زیرکلاس بسازیم
- ویژگیهای متنوع خیلی راحت میتونن به آبجکت اضافه بشن
- به جای اینکه همه ویژگیها رو به صورت یکجا توی یک کلاس بنویسیم، برای هر ویژگی یک کلاس جدا ساختیم تا توسعه کدها راحتتر بشه (اصل اول SOLID)
معایب الگوی Decorator
- جدا کردن یک ویژگی از بین همه ویژگیها ممکنه کار دشواری باشه
خب دوستان این قسمت هم به پایان رسید. درست مثل الگوهای دیگه بهتره که ابتدا نیازها به درستی شناسایی و بعد الگوی مناسب استفاده بشه. حتی گاهی اوقات لازمه الگوها رو با هم ترکیب کنیم تا مشکلمون رو حل کنیم. امیدوارم از این پست هم استفاده کرده باشین. روزتون خوش 😉✌️
