پوریا سیفی
پوریا سیفی
خواندن ۷ دقیقه·۱ سال پیش

دیزاین پترن Bridge به زبان ساده ?


قبل از اینکه وارد توضیحات اصلی بشیم بیاید یه مسئله ساده رو حل کنیم. فرض کنید دو مجموعه ساده A و B داریم.

A = {x, y, z} B = {1, 2, 3}

فک میکنید ترکیبات دوتایی ممکن، از اعضای این دو مجموعه چه تعداد هست؟ جواب خیلی ساده ست اما تو تصویر زیر بهتر متوجه میشیم :

راه حل کلی این مسئله تو دنیای ریاضیات ضرب دکارتی هست که میشه ۹ حالت ممکن. اما چرا مقاله رو با این مسئله شروع کردیم؟

بیاید فرض کنیم هر کدوم از اعضای مجموعه هایی که مطرح شد یه کلاس باشند:

  • مجموعه اشکال هندسی (کلاس مربع و کلاس دایره)
  • مجموعه رنگ ها (کلاس آبی و قرمز )

پس ما با یه سری کلاس در دو دسته بندی متفاوت سر و کار داریم ینی شکل ها و رنگ ها. خب حالا اگر بخوایم با این کلاس ها اشکال هندسی رنگی بسازیم. چند کلاس مجزا میتونیم برای هر شکل رنگی داشته باشیم :

شبیه به همون ضرب دکارتی هست که میشه ۴ کلاس :



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

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

حالا به تصویر زیر دقت کنید :



تو این نمودار میبینیم که کلاس هایی که توی دسته بندی های متفاوتی قرار دارند (اشکال هندسی و رنگ) به این صورت از هم تفکیک شدند. طبیعیه که ما قرار نیست به ازای هر ترکیبی از کلاس ها، یک کلاس جدید بسازیم. میتونیم با ترکیب (compose) کردن این کلاس ها تو کد هامون به همون خروجی که میخوایم برسیم :

$red = new Red(); new Square($red);

اون فلش قرمز (contains) در تصویر هم رو همین موضوع تاکید داره. درواقع این تصویر داره به ما میگه که هر شکل هندسی میتونه در خودش (متد سازنده ش) نیازمندی خودش به رنگ رو هندل بکنه .پس میبینیم که ما به جای inheritance از object composition استفاده کردیم. تو مقاله Adapter Design Pattern هم به این موضوع اشاره کرده بودیم.




خیلی خب تا اینجا یکی نگرانی های ما با این الگوی ساده برطرف شد. حالا بریم ببینیم دیزاین پترن Bridge چی هست و کجا ها به کارمون میاد. تعریف اصلی دیزاین پترن این هست :

Decouple an abstraction from its implementation so that the two can vary independently.

ترجمه : لایه abstraction از implementation جدا بشه که این دو بتونن مستقل از هم تغییر پیدا کنند.

هیچ جوره نمیشه جمله رو هضم کرد واقعا ??? به نظر جمله خیلی آکادمیک هست و باعث شده از چیزی که واقعا هست پیچیده تر به نظر بیاد. طبق معمول بریم که مسئله رو با مثال واقعی درک کنیم :

پیاده سازی ارسال اعلان (Notification) :

فرض کنیم از ما خواسته شده که اپلیکیشن ما ارسال اعلان داشته باشه و اعلان ها حالت دو عادی و فوری داشته باشند. با توجه به فوری یا عادی بودن اعلان انتهای پیام شامل متن خاصی میشه یا مثلا فوری باید سریع تر ارسال بشه و مطمئن شیم کاربر اونا رو تحویل گرفته و... خیلی ماهیت مثال مهم نیست.

مسئله ای که الان مطرحه اینه که ما اساسا یک انتزاع به اسم Notification داریم که قراره رفتار فوری و عادی داشته باشه. از طرفی میدونیم که اعلان ها از طریق کانال های مختلفی قابل ارسال هستند. مثلا پیامک و ایمیل گزینه های خوبی هستند.

خب اگر خوب دقت کنیم میبینیم که تو این مثال هم ما با دو مجموعه متفاوت از کلاس ها سر و کار داریم.

  • اولین مجموعه : عادی یا فوری اعلان
  • دومین مجموعه : کانال ارسال اعلان (ایمیل یا پیامک)

شبیه به همون مثال اشکال هندسی و رنگ ها شد که ابتدای مقاله صحبت کردیم :


پیاده سازی کد ها :


حالا ما برای اینکه یه اعلان از نوع فوری و از طریق کانال پیامک ارسال کنیم به صورت زیر عمل میکنیم :

$smsChannel = new SMSChannel(); $urgentSMS = new UrgentNotification($smsChannel); $urgentSMS->send(&quotUrgent alert: Please take immediate action!&quot);


حالا فرض کنید شرایطی پیش میاد که ما باید یک کانال جدید (مثلا ارسال از طریق تلگرام) اضافه کنیم یا مثلا برای کانال پیامک مجبور باشیم تغییراتی در ساختار بدیم. آیا این اضافه شدن و یا تغییراتی که قرار به ما تحمیل بشه روی عادی بودن و فوری بودن اعلان ها تاثیری میذاره ؟ قطعا خیر.

بیاید دوباره تعریفی که برای دیزاین پترن ارائه شد رو مرور کنیم :

Decouple an abstraction from its implementation.
انتزاع (abstraction) ما تو این اپلیکیشن ارسال اعلان و فوری و عادی بودنش بود و پیاده سازی (implementation) ما کانال های پیامک و ایمیل. و ما موفق شدیم این دو لایه رو از هم تفکیک کنیم که تغییرات احتمالی رو هر کدوم، تاثیری بر اون یکی لایه نداشته باشه.

حالا واقعا تفاوت Abstraction و Implementation چیه ؟

به نظرم تا اینجای کار شرایط بسیار خوبی فراهم شده که واقعا به این سوال خیلی مهم پاسخ بدیم. تو این مثال خیلی شفاف میشه فهمید اساسا تفاوت این دو چی هست.

ما میدونیم که Abstract کلاسی هست که میتونه بدنه داشته باشه و نمیشه ازش ابجکت ساخت ولی تو عمل چی ؟ واقعا کجا باید از Abstract استفاده کنیم و کجا باید به Interface ها فکر کنیم ؟

خب برگردیم به مثالی که مطرح کرده بودیم . ما میتونیم پیاده سازی های متفاوتی برای یک انتزاع داشته باشیم. یعنی چی؟

بیاید مثال رو ادامه بدیم تا ایده implement کردن برامون شفاف تر بشه :

فرض کنیم تا اینجای کار ما اعلان رو برای یک سری افراد به صورت تکی ارسال میکردیم. حالا شرایطی پیش اومده که نیاز داریم یک اعلان برای گروهی از افراد ارسال بشه. مثلا افرادی که روز تولدشون هست یا افرادی که سفارشی تو سایت ما ثبت کردن و حالا میخوایم بهشون ناتیف بدیم که بیان و نظر خودشون در مورد سفارشی که دریافت کردن تو سایت وارد کنند و...

اتفاقی که اینجا داره میفته اینه که انتزاع ما (Notification) یه پیاده سازی دیگه لازم داره که بتونیم دریافت کنندگان مختلف رو هندل کنیم. چون دریافت کنندگان یه مقدار منطقش پیچیده شده و قطعا وظیفه فوری و عادی بودن نیست که بدون دریافت کنندگان چه افرادی هستند. همینطور وظیفه کانال های پیامک و ایمیل هم نیست.

پس ما نیاز به یک اینترفیس جدید داریم .

حالا میتونیم پیاده سازی های متفاوتی از این اینترفیس داشته باشیم :


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

باز هم میبینیم که حتی با ایجاد Implementation های جدید لایه های دیگه درگیر تغییرات نمیشن . اینکه ما دریافت کنندگان رو چطوری به دست بیاریم هیچ ربطی به فوری و عادی اعلان (Abstraction) بودن .

بریم ببینیم کارهایی که تاحالا کردیم چقدر داره کمک میکنه اصول سالید رو رعایت کنیم :

  • اصل اول (single responsibility)

هر کلاسی که ایجاد میشه یک مسئولیت مشخص داره و نه بیشتر.

  • اصل دوم (open closed) :

با ایجاد کانال جدید اتفاقی برای کلاس های دیگه نمیفته.

  • اصل چهارم (interface segregation) :

اینترفیس دریافت کننده ها و کانال های اعلان ربطی به هم ندارند پس به صورت مستقل از هم عمل میکنند.

  • اصل پنجم (dependency inversion)

ماژول های سطح بالا مثل RegularNotification و UrgentNotification به کلاس های سطح پایین مثل SMSChannel وابسته نیستند بلکه به مفاهیم انتزاعی (NotificationChannel) وابستگی دارند هستند.

در آخر شاید براتون جالب باشه که چرا اسم این دیزاین پترن bridge یا پل هست. تو تصویر زیر خیلی زیبا این ایده مشخص شده :



تو این پترن ، طراحی کد ها به نحوی هست که انگار از implementation هامون یک پلی به Abstraction زده شده و تغییراتی که به اپلیکیشن تحمیل میشه به صورت مجزا در هر لایه هندل میشن و تاثیری روی اون طرف پل ندارن :)

امیدوارم مقاله براتون مفید بوده باشه.

دیزاین پترنoopsolid
شاید از این پست‌ها خوشتان بیاید