درود دوستان. فرض کنیم توی برنامه میخوایم به کاربرها پیام ارسال کنیم. یک تابع داریم به صورت زیر که کار ارسال پیام رو انجام میده:
function notifyUsers(notifier: Notification) { notifier.send(); }
به این تابع فقط آبجکتهایی رو میشه پاس داد که از نوع Notification باشن.
ارسال پیام در حال حاضر فقط به صورت ایمیلی هست. کلاسی ساختیم به اسم EmailNotification که فرض کنیم از نوع Notification هست و از اون به صورت زیر استفاده میکنیم:
// class EmailNotification implements Notification ... mailer = new EmailNotification(); notifyUsers(mailer);
مدتی بعد میخوایم به برنامه یک روش دیگه برای ارسال پیام اضافه کنیم. مثلاً اطلاعرسانی از طریق یک سرویس بیرونی SMS که برای ارسال پیام، کتابخونه و کدهای مخصوص به خودش رو ارائه میده. مشکل اینجا به وجود میاد. این کتابخونه اینترفیسی (نوع) متفاوت داره و از اون نمیتونیم توی تابع notifyUsers استفاده کنیم:
notifyUsers(new External_SMS_Library()); // Fatal error: // Argument 1 passed to notifyUsers() must // implement interface Notification
مشکل اینجاست: در شرایطی که تابع notifyUsers به آبجکتهایی احتیاج داره که از نوع Notification باشن، کلاس External_SMS_Library نوع کاملاً متفاوتی داره و بنابراین تابع notifyUsers نمیتونه از اون استفاده کنه. به قول معروف کلاس External_SMS_Library یک اینترفیس ناسازگار داره.
یک راه برای حل این مشکل اینه که تابع notifyUsers رو دستکاری کنیم:
function notifyUsers(notifier) { if (notifier instanceof EmailNotification) { notifier.sendEmail(); } else if (notifier instanceof External_SMS_Library) { notifier.login(); notifier.setPort(); notifier.sendSms(); } }
خب ظاهراً مشکل حل شد. اما مشکلات دیگهای سبز شدن:
۱. تابع notifyUsers باید اطلاعات زیادی درباره نحوه پیادهسازی و استفاده کلاس External_SMS_Library داشته باشه (اصل اول SOLID هم نقض میشه)
۲. در آینده با اضافه شدن یک سرویس ارسال پیام دیگه، تابع notifyUsers مدام در معرض تغییر هست (نقض شدن اصل دوم SOLID)
۳. ما نمیخوایم کدهامون وابسته به یک کتابخونه خارجی باشه (نقض شدن اصل پنجم SOLID)
۴. گاهی اوقات امکان تغییر دادن کدهای اون کتابخونه خارجی امکانپذیر نیست
خب الگویی که برای حل این مشکل به کمک ما میاد، الگوی طراحی Adapter هست 👌
الگوی Adapter چیه؟ 🤔
این الگو این امکان رو میده تا آبجکتهایی که اینترفیسهای متفاوتی دارن، بتونن بدون مشکل مورد استفاده قرار بگیرن
الگوی Adapter زیرمجموعه الگوهای Structural - الگوهایی که کمک میکنن تا آبجکتهای موجود طوری با هم در تعامل باشن تا بتونیم ساختارهای بزرگتری طراحی کنیم - هست. این الگوی پرطرفدار که به اون Wrapper هم گفته میشه، پیادهسازی کاملاً راحتی داره.
پیادهسازی الگوی Adapter
برای حل مشکلی که توی مثالمون داشتیم، ما به اداپتر (Adapter) نیاز داریم. اداپتر به کلاسی گفته میشه که آبجکت ناسازگار رو میگیره و آبجکتی سازگار با برنامه تحویل میده تا برنامه بتونه با خیال راحت با کتابخونه خارجی ما کار کنه. اداپتر در واقع یک واسط بین کدهای برنامه و اون کلاس ناسازگار (کتابخونه خارجی توی مثال) هست. بجای اینکه به طور مستقیم از اون کلاس کتابخونه خارجی توی کد استفاده کنیم، از اداپتر استفاده میکنیم.
قبل از شروع، با مفهوم کلاینت (client) آشنا بشیم. کلاینت به قسمتی از برنامه گفته میشه که کار اصلی و مورد نظرمون رو انجام میده. توی مثال ما تابع notifyUsers قسمت کلاینت هست.
مرحله ۱: ساختن یک اینترفیس برای همکاری کلاینت و بقیه سرویسها
ابتدا یک اینترفیس میسازیم به اسم Notification:
interface Notification { send(); }
این اینترفیس متدی داره به اسم send که همه سرویسها (ایمیل، SMS ،Push و ...) باید اون رو پیادهسازی کنن.
مرحله ۲: ساختن Adapter برای هر سرویس
برای هر کلاس کتابخونه خارجی یک کلاس اداپتر اختصاصی میسازیم که باید از اینترفیس بالا تبعیت کنن:
class XyzSmsAdapter implements Notification { }
حالا باید این اداپتر رو یکم شخصیسازی کنیم. قبلتر گفتیم که اداپتر یک واسط بین کلاس کتابخونه خارجی و قسمت کلاینت هست. پس اداپتر باید توی خودش یک نمونه از کلاس کتابخونه خارجی رو داشته باشه. به این صورت این کار رو با constrcutor ها انجام میدیم:
class XyzSmsAdapter implements Notification { private service: External_SMS_Library; constructor(service: External_SMS_Library) { this.service = service; } }
حالا باید متدهایی که توی اینترفیسِ Notification وجود داره رو پیادهسازی کنیم:
class XyzSmsAdapter implements Notification { // ... public send() { this.service.login(); this.service.setPort(); this.service.sendSms(); } }
توی متد send همه کارهایی که مربوط به ارسال SMS از طریق این سرویس هست رو نوشتیم.
و تمام! کافیه از اداپتر یک نمونه بسازیم و از اون به جای کلاس کتابخونه خارجی استفاده کنیم:
const SmsNotifier = new XyzSmsAdapter(new External_SMS_Library); notifyUsers(SmsNotifier); function notifyUsers(notifier: Notification) { notifier.send(); }
اگه بخوایم با هزاران کتابخونه خارجی دیگه کار کنیم کافیه برای اونها یک اداپتر اختصاصی بسازیم، منطق کارشون رو توی متد send تعریف کنیم و نهایتاً اداپتر رو پاس بدیم به کلاینت:
class SlackAdapter implements Notification { public send() { this.slackApi.login(); this.slackApi.sendMessage(); } } const slackAdapter = new SlackAdapter(new SlackApi); notifyUsers(slackAdapter);
همونطور که میبینیم، اداپترها خودشون معمولاً کار خاصی انجام نمیدن. بلکه فقط درخواست رو از کلاینت منتقل میکنن به سرویس. همچنین سرویس و کلاینت اصلاً از وجود اداپتر هیچ اطلاعی ندارن. برای کلاینت مهم نیست که چه آبجکتی داره پاس داده میشه و فقط آبجکتهایی رو قبول میکنه از اینترفیس Notification تبعیت میکنن.
درست مثل دنیای واقعی. همونطور که میدونیم برای بعضی از شارژرهای گوشی نیاز به اداپتر (تبدیل) داریم. خود شارژر و سوکت برق هیچ اطلاعی از وجود اداپتر ندارن. کار اداپتر اینه که شارژر بتونه به درستی از سوکت برق بگیره. اداپتر به کلاینت میگه «به من بگو میخوای چکار کنی، انجام دادنش با من»
(اگه خوشتون اومد Star بزنین 😉)
مزایای الگوی Adapter
۱. با استفاده از این الگو میتونیم فرآیند سازگار کردن کتابخونه خارجی رو منتقل کنیم به یک قسمت دیگه (اصل اول SOLID)
۲. میتونیم با بینهایت سرویس کار کنیم بدون اینکه قسمت کلاینت رو تغییر بدیم (اصل دوم SOLID)
۳. وابستگی قسمت کلاینت به کتابخونههای خارجی حذف میشه (اصل پنجم SOLID)
معایب الگوی Adapter
گاهی میتونیم با تغییر دادن کدهای کتابخونه خارجی به هدفمون برسیم و اضافه کردن این الگو ممکنه باعث پیچیدگی بیش از حد برنامه بشه.
خب دوستان این هم از این الگو. امیدوارم استفاده کرده باشید. روزتون خوش 😉❄️
منبعی که برای این قسمت استفاده کردم:
