توضیح دوات: برای راهنمایی و اطلاعات بیشتر در مورد نمودار کلاس به این مقاله مراجعه کنید.
پل (Bridge) یک الگوی طراحی ساختاری است که به ما امکان میدهد یک کلاس بزرگ یا مجموعهای از کلاسهای مرتبط را به دو سلسلهمراتب جداگانه تقسیم کنیم: انتزاع (Abstraction) و پیادهسازی (Implementation)، که میتوانند به طور مستقل از یکدیگر توسعه یابند.
الگوی طراحی پل انتزاع را از پیادهسازی جدا میکند. در نتیجه، انتزاع و پیادهسازی میتوانند به صورت مستقل توسعه و تغییر کنند. هدف اصلی این الگو، تسهیل جداسازی مسئولیتها است.
در عمل، پل به هر دو بخش انتزاع و پیادهسازی اشاره دارد اما هیچیک را پیادهسازی نمیکند، و بدین ترتیب جزئیات هر یک در کلاسهای جداگانه خود باقی میمانند.
نمونه کد این پست را میتوانید در گیتهاب پیدا کنید.
مفهومسازی مسئله
تصور کنید که در حال کار روی یک برنامه CAD هستیم. در حال حاضر، این برنامه دارای یک کلاس IShape
است که توابع مربوط به اشکال هندسی مختلف را ارائه میدهد و چند زیرکلاس مانند Circle
، Triangle
و Rectangle
دارد.
اکنون میخواهیم این سلسلهمراتب کلاسی را گسترش دهیم تا شامل رنگها نیز شود، بنابراین باید زیرکلاسهایی مانند Red
, Green
و Blue
ایجاد کنیم. با این حال، از آنجایی که از قبل سه زیرکلاس داریم، نیاز به ۹ ترکیب کلاسی داریم مانند BlueCircle
و GreenTriangle
.
افزودن انواع اشکال جدید و رنگها به این سلسلهمراتب باعث میشود که ساختار به صورت نمایی گسترش یابد. برای مثال، اگر بخواهیم شکل Pentagon
را اضافه کنیم، باید سه زیرکلاس دیگر (یکی برای هر رنگ) ایجاد کنیم. اگر بخواهیم رنگ Magenta
را اضافه کنیم، باید همه اشکال موجود را بهروزرسانی کنیم. در نهایت، اگر بخواهیم هر دو را اضافه کنیم، به ۷ زیرکلاس جدید نیاز داریم تا تمام اشکال و رنگها را پوشش دهیم. همانطور که میبینید، هرچه بیشتر پیش برویم، اوضاع بدتر میشود.
مشکل این است که ما تلاش میکنیم کلاسهای شکل را در دو بعد مستقل گسترش دهیم: از نظر فرم و از نظر رنگ.
الگوی پل این مشکل را با جایگزین کردن وراثت با ترکیب اشیاء حل میکند. به این معنا که یکی از ابعاد را به یک سلسلهمراتب کلاسی جداگانه استخراج میکنیم، بهطوری که کلاسهای اصلی به یک شیء از سلسلهمراتب جدید اشاره کنند، به جای اینکه تمام حالتها و رفتارهای خود را در یک کلاس واحد داشته باشند.
شما میتوانید با تبدیل یک سلسلهمراتب بزرگ به چند سلسلهمراتب مرتبط، از رشد نمایی جلوگیری کنید.
با پیروی از این رویکرد، میتوانیم کد مربوط به رنگ را به یک کلاس جداگانه با سه زیرکلاس (Red
، Green
و Blue
) استخراج کنیم. رابط IShape
یک فیلد مرجع به یکی از اشیاء رنگی اضافه میکند. اکنون شکل میتواند هر کاری که مربوط به رنگ است را به شیء رنگ متصل تفویض کند. این مرجع به عنوان پلی بین کلاسهای IShape
و IColor
عمل خواهد کرد. از این پس، افزودن رنگهای جدید نیازی به تغییر در سلسلهمراتب شکلها ندارد و بالعکس.
حالا که یک مثال ساده با اشکال و رنگها را توضیح دادیم، بیایید معنای اصطلاحات انتزاع و پیادهسازی را تفسیر کنیم.
- انتزاع (که به آن رابط نیز گفته میشود) یک لایه کنترل سطح بالا برای برخی از موجودیتها است. این لایه نباید هیچ کاری واقعی را به تنهایی انجام دهد. وظیفه آن تنها تفویض کارها به لایه پیادهسازی است، که به آن پلتفرم نیز گفته میشود.
- پیادهسازی کدی است که جزئیات عملیاتی خاصی را اجرا میکند.
در یک برنامه کاربردی واقعی، انتزاع و پیادهسازی میتوانند توسط هر نوع سیستمی پیادهسازی شوند. بهعنوان مثال، انتزاع میتواند توسط یک رابط کاربری گرافیکی (GUI) نمایان شود، و پیادهسازی میتواند سیستم بکاند (API) زیرین باشد که لایه GUI در پاسخ به تعاملات کاربر آن را فراخوانی میکند.
برای حل این مشکل، الگوی پل پیشنهاد میکند که کلاسها را به دو سلسلهمراتب تقسیم کنیم:
- انتزاعها: لایه GUI برنامه.
- پیادهسازیها: API سیستمعاملها.
شیء انتزاع ظاهر برنامه را کنترل میکند و کار واقعی را به شیء پیادهسازی متصل تفویض میکند. پیادهسازیهای مختلف قابل تعویض هستند تا زمانی که از یک رابط مشترک پیروی کنند.
اجزای الگوی طراحی پل
این الگو شامل ۵ بخش اصلی است:
- انتزاع: منطق کنترل سطح بالا را ارائه میدهد و برای انجام کارهای سطح پایین به شیء پیادهسازی تکیه میکند.
- پیادهسازی: رابطی را اعلام میکند که برای تمام پیادهسازیهای خاص مشترک است.
- پیادهسازی خاص: شامل کدهای مخصوص پلتفرم است.
- انتزاع پیشرفته: گونههایی از منطق کنترل را ارائه میدهد که با پیادهسازیهای مختلف کار میکند.
- کلاینت: معمولاً تنها با انتزاع کار دارد و مسئولیت پیوند دادن شیء انتزاع به یکی از اشیاء پیادهسازی را بر عهده دارد.
برای نمایش نحوهی عملکرد الگوی Bridge، ما قصد داریم یک سیستم سفارشدهی برای افرادی که نیازهای غذایی خاص دارند ایجاد کنیم. بسیاری از افراد دچار بیماریهایی مانند بیماری سلیاک یا AGS هستند که مصرف برخی مواد غذایی را برای آنها ممنوع میکند. به همین دلیل، ممکن است برای افرادی که با چنین مشکلاتی مواجه هستند، سفارش غذا از رستورانها دشوار باشد، زیرا اغلب آنها غذای مناسب برای نیازهای خاص فرد را ارائه نمیدهند (و حتی اگر این غذا را ارائه دهند، محیطی که غذا در آن تهیه میشود معمولاً بهدرستی تهویه یا استریل نشده است، که احتمال آلودگی متقابل را افزایش میدهد).
ایده به این شکل است: من باید بتوانم یک نوع غذای خاص انتخاب کنم و یک رستوران انتخاب کنم، بدون اینکه نیاز به دانستن دقیقاً چیستی هر یک از آنها داشته باشم (مثلاً یک غذای بدون گوشت از یک داینر یا یک غذای بدون گلوتن از یک رستوران مجلل).
اگر از مدل وراثت سنتی استفاده میکردیم، ممکن بود کلاسهای زیر را داشته باشیم:
public interface IOrder { }
public class MeatFreeOrder : IOrder { }
public class GlutenFreeOrder : IOrder { }
اما اگر لازم باشد نوع رستورانی که سفارش از آن آمده است را نیز پیگیری کنیم چه؟ هیچ رابطه واقعی بین غذا و رستوران وجود ندارد، اما ممکن است همچنان بخشی از مدل باشد.
در این صورت، ممکن است با یک ساختار پیچیده از وراثت مواجه شویم:
public interface IOrder { }
public class MeatFreeOrder : IOrder { }
public class GlutenFreeOrder : IOrder { }
public interface IDinerOrder : IOrder { }
public class DinerMeatFreeOrder : MeatFreeOrder, IDinerOrder { }
public class DinerGlutenFreeOrder : GlutenFreeOrder, IDinerOrder { }
public interface IFineDiningOrder : IOrder { }
public class FineDiningMeatFreeOrder : MeatFreeOrder, IFineDiningOrder { }
public class FineDiningGlutenFreeOrder : GlutenFreeOrder, IFineDiningOrder { }
در اینجا، ما دو ویژگی مستقل (غذای بدون گوشت/بدون گلوتن و رستوران معمولی/رستوران با غذای فاخر) را مدلسازی میکنیم، اما به سه اینترفیس و شش کلاس نیاز داریم. میتوانیم این را با استفاده از الگوی Bridge سادهتر کنیم.
الگوی Bridge سعی میکند مسئولیت این اینترفیسها را تقسیم کند بهطوریکه بسیار بیشتر قابل استفاده مجدد باشند. با این کار، در نهایت چیزی شبیه به موارد زیر خواهیم داشت؛
public interface IOrder { }
public class MeatFreeOrder : IOrder { }
public class GlutenFreeOrder : IOrder { }
public interface IRestaurantOrder : IOrder { }
public class DinerOrder : IRestaurantOrder { }
public class FineDiningOrder : IRestaurantOrder { }
بیا این را به پیادهسازی کامل الگوی Bridge گسترش دهیم. ابتدا به شرکتکنندهی Implementation نیاز داریم که یک روش برای ثبت سفارش تعریف خواهد کرد:
namespace Bridge
{
///
/// Implementation participant which defines an interface for placing an order
///
public interface IOrderingSystem
{
public void Place(string order);
}
}
همچنین به شرکتکننده Abstraction نیاز داریم که یک کلاس انتزاعی خواهد بود و یک متد برای ارسال سفارش تعریف میکند و یک مرجع به Implementation نگه میدارد:
namespace Bridge
{
///
/// Abstraction which represents the sent order
/// and maintains a reference to the restaurant where the order is going.
///
public abstract class OrderHandler
{
public IOrderingSystem _restaurant;
public abstract void SendOrder();
}
}
اکنون میتوانیم شروع به تعریف کلاسهای RefinedAbstraction خود کنیم. بیایید آن دو نوع وعده غذایی خاصی که قبلاً ذکر شد (بدون گوشت و بدون گلوتن) را گرفته و اشیای RefinedAbstraction را برای آنها پیادهسازی کنیم.
namespace Bridge
{
///
/// RefinedAbstraction for a meat-free order
///
public class MeatFreeOrder : OrderHandler
{
public override void SendOrder()
{
_restaurant.Place("Meat-Free Order");
}
}
///
/// RefinedAbstraction for a gluten-free order
///
public class GlutenFreeOrder : OrderHandler
{
public override void SendOrder()
{
_restaurant.Place("Gluten-Free Order");
}
}
}
در نهایت، باید شرکتکنندگان ConcreteImplementation خود را پیادهسازی کنیم که همان رستورانها هستند:
namespace Bridge
{
///
/// ConcreteImplementation for an ordering system at a diner.
///
public class DinerOrders : IOrderingSystem
{
public void Place(string order)
{
Console.WriteLine($"Placing order for {order} at the Diner");
if (order.Equals("Meat-Free Order"))
Console.WriteLine("\tDish: Bean & Halloumi Stew");
if (order.Equals("Gluten-Free Order"))
Console.WriteLine("\tDish: Stuffed Peppers");
}
}
///
/// ConcreteImplementation for an ordering system at a fancy restaurant
///
public class FineDiningRestaurantOrders : IOrderingSystem
{
public void Place(string order)
{
Console.WriteLine($"Placing order for {order} at the Fine Dining Restaurant");
if (order.Equals("Meat-Free Order"))
Console.WriteLine("\tDish: Vegetarian Mushroom Risotto With Truffle Oil");
if (order.Equals("Gluten-Free Order"))
Console.WriteLine("\tDish: Arroz de Bacalhau");
}
}
}
در نهایت، به یک متد Main()
نیاز داریم که از الگوی Bridge استفاده کند تا سفارشهای مختلفی ایجاد کرده و آنها را به رستورانهای مختلف ارسال کند:
using Bridge;
OrderHandler meatFreeOrderHandler = new MeatFreeOrder
{
_restaurant = new DinerOrders()
};
meatFreeOrderHandler.SendOrder();
meatFreeOrderHandler._restaurant = new FineDiningRestaurantOrders();
meatFreeOrderHandler.SendOrder();
Console.WriteLine();
OrderHandler glutenFreeOrderHandler = new GlutenFreeOrder
{
_restaurant = new DinerOrders()
};
glutenFreeOrderHandler.SendOrder();
glutenFreeOrderHandler._restaurant = new FineDiningRestaurantOrders();
glutenFreeOrderHandler.SendOrder();
اگر برنامه را اجرا کنیم، متوجه میشویم که میتوانیم هر سفارش را به هر رستورانی ارسال کنیم. علاوه بر این، اگر هر یک از انتزاعات (سفارشها) تعریف خود را تغییر دهند، پیادهسازها اهمیتی نمیدهند؛ و برعکس، اگر پیادهسازها پیادهسازی خود را تغییر دهند، انتزاعات نیز نیازی به تغییر نخواهند داشت.
در اینجا تصویری از برنامه نمونه در حال اجرا آورده شده است:
کاربرد الگوی Bridge
الگوی Bridge در شرایط زیر بسیار مفید است:
- اجتناب از اتصال بین انتزاع و پیادهسازی.
- داشتن انتزاع و پیادهسازی قابل توسعه.
- جلوگیری از تأثیر تغییرات در پیادهسازی بر روی مشتری.
- سازماندهی و تقسیم یک کلاس یکپارچه با گزینههای متعدد عملکردی.
اجتناب از اتصال بین انتزاع و پیادهسازی
اگرچه اختیاری است، الگوی Bridge به ما امکان میدهد تا شیء پیادهسازی را درون انتزاع جایگزین کنیم. این کار بهسادگی تخصیص یک مقدار جدید به یک فیلد است.
بهعنوان نکته، این دلیل اصلی است که چرا افراد الگوی Bridge را با الگوی Strategy اشتباه میگیرند. به یاد داشته باشید که یک الگو چیزی بیش از یک روش ساختاری برای کلاسها است؛ بلکه میتواند نیت و مشکلی را که در حال حل شدن است، منتقل کند.
داشتن انتزاع و پیادهسازی قابل توسعه
الگوی Bridge پیشنهاد میدهد که یک سلسله مراتب جداگانه برای هر یک از ابعاد استخراج کنیم. کلاس اصلی کار مربوطه را به اشیای متعلق به آن سلسلهمراتبها واگذار میکند، بهجای اینکه همه کارها را خودش انجام دهد.
تغییرات در پیادهسازی میتواند بر مشتری تأثیر بگذارد
الگوی Bridge میتواند به شما کمک کند از هر گونه تغییر در پیادهسازی که بر مشتری تأثیر بگذارد، جلوگیری کنید. همچنین نیازی به کامپایل مجدد کد سمت مشتری پس از تغییرات در پیادهسازی وجود نخواهد داشت.
سازماندهی و تقسیم یک کلاس یکپارچه با گزینههای متعدد عملکردی
هرچه یک کلاس بزرگتر شود، فهمیدن چگونگی عملکرد آن سختتر میشود و زمان بیشتری برای اعمال تغییرات نیاز است. تغییرات انجامشده در یکی از حالتهای عملکردی ممکن است نیاز به تغییرات در سراسر کلاس داشته باشد که اغلب منجر به خطا یا نادیده گرفتن برخی از اثرات جانبی حیاتی میشود.
الگوی Bridge به شما امکان میدهد کلاس یکپارچه را به چند سلسله مراتب کلاسی تقسیم کنید. پس از این، میتوانید کلاسهای هر سلسلهمراتب را مستقل از کلاسهای دیگر تغییر دهید. این رویکرد نگهداری کد را ساده میکند و خطر خراب شدن کد موجود را کاهش میدهد.
مزایا و معایب الگوی Bridge
مزایا
- امکان ایجاد کلاسها و برنامههای مستقل از پلتفرم را فراهم میکند.
- کد مشتری با انتزاعات سطح بالا کار میکند و جزئیات پلتفرم را نمیبیند.
- امکان معرفی انتزاعات و پیادهسازیهای جدید مستقل از یکدیگر را فراهم میکند و اصل باز/بسته را برآورده میسازد.
- تمرکز روی منطق سطح بالا در انتزاع و جزئیات پلتفرم در پیادهسازی، اصل مسئولیتپذیری ساده را برآورده میکند.
معایب
- ممکن است با اعمال الگو روی یک کلاس بهشدت منسجم، کد را پیچیدهتر کند.
روابط با سایر الگوها
- Bridge معمولاً از ابتدا طراحی میشود و به ما امکان میدهد بخشهای مختلف یک برنامه را مستقل از یکدیگر توسعه دهیم. از سوی دیگر، Adapter معمولاً در یک برنامه موجود استفاده میشود تا برخی از کلاسهایی که در غیر این صورت ناسازگار هستند را بهخوبی با یکدیگر کارآمد کند.
- Bridge، State، Strategy، و Adapter ساختارهای بسیار مشابهی دارند. در واقع، این الگوها مبتنی بر ترکیب هستند که کار را به سایر اشیا واگذار میکند. با این حال، آنها مشکلات مختلفی را حل میکنند. یک الگو فقط یک دستورالعمل برای ساختاردهی کد به شیوهای خاص نیست؛ بلکه میتواند مشکل حلشده توسط الگو را نیز به سایر توسعهدهندگان منتقل کند.
- میتوان از Abstract Factory همراه با الگوی Bridge استفاده کرد. این ترکیب زمانی ارزشمند است که برخی انتزاعات تعریفشده توسط الگوی Bridge فقط با پیادهسازیهای خاصی کار کنند. در این حالت، Abstract Factory میتواند این روابط را کپسوله کند و پیچیدگی را از کد مشتری پنهان کند.
- میتوان الگوی Builder را با الگوی Bridge ترکیب کرد: کلاس کارگردان نقش انتزاع را بازی میکند، در حالی که سازندههای مختلف بهعنوان پیادهسازیها عمل میکنند.
نتیجهگیری
در این مقاله، بررسی کردیم که الگوی Bridge چیست، چه زمانی باید از آن استفاده کرد و مزایا و معایب استفاده از این الگو چیست. سپس برخی موارد کاربرد این الگو و روابط آن با سایر الگوهای طراحی کلاسیک را بررسی کردیم.
شایان ذکر است که الگوی طراحی پل، همراه با سایر الگوهای طراحی ارائهشده توسط Gang of Four، یک راهحل همهجانبه یا نهایی برای طراحی برنامه نیست. بار دیگر بر عهده مهندسان است که تصمیم بگیرند چه زمانی از یک الگوی خاص استفاده کنند. در نهایت، این الگوها زمانی مفید هستند که بهعنوان ابزاری دقیق استفاده شوند، نه بهعنوان یک پتک سنگین.