زمان تخمینی مطالعه: 13 دقیقه
الگوی Bridge یک الگوی طراحی ساختاری(Structural) است که به شما امکان میدهد که یک کلاس بزرگ یا مجموعهای از کلاسهای نزدیک به هم را به دو سلسله مراتب مجزا تقسیم کنید که شامل کلاسهای انتزاعی و پیادهسازی است که میتوانند مستقل از یکدیگر توسعه یابند. در واقع الگوی Bridge زمانی استفاده میشود که باید یک انتزاع را از اجرای آن جدا کنیم تا این دو بتوانند به طور مستقل متفاوت باشند. این نوع الگوی طراحی تحت الگوی ساختاری قرار میگیرد زیرا این الگو کلاس پیادهسازی و کلاس انتزاعی را با ایجاد یک ساختار پل بین آنها جدا میکند. این الگو شامل یک اینترفیس است که به عنوان یک پل عمل میکند که عملکرد کلاسهای Concrete را از کلاسهای پیاده کننده اینترفیس مستقل میکند. هر دو نوع کلاس را میتوان از نظر ساختاری بدون تأثیر بر یکدیگر تغییر داد.
بیان مسئله: فرض کنید یک کلاس Shape هندسی با یک جفت زیر کلاس دارید که شامل Circle و Square میباشد. در این ساختار شما قصد دارید تا سلسله مراتب کلاس را برای ترکیب رنگها توسعه دهید، و زیر کلاسهای شکل Red و Blue را ایجاد کنید. با این حال، از آنجایی که شما در حال حاضر دو کلاس فرعی دارید، باید چهار ترکیب کلاس مانند BlueCircle و RedSquare ایجاد کنید.
افزودن انواع شکلها و رنگهای جدید به سلسله مراتب موجود در بالا اندازه آن را به صورت تصاعدی رشد میدهد. به عنوان مثال، برای اضافه کردن یک شکل مثلث، باید دو کلاس فرعی را معرفی کنید، که در واقع یک عدد برای هر رنگ است. پس از آن، افزودن یک رنگ جدید مستلزم ایجاد سه زیر کلاس، یکی برای هر نوع شکل است. در واقع در این ساختار هر چه جلوتر میرویم اوضاع بدتر میشود.
این مشکل به این دلیل رخ میدهد که ما سعی میکنیم کلاسهای شکل را در دو بعد مستقل یکی بر اساس فرم و دیگری بر اساس رنگ گسترش دهیم. این موضوع یک مشکل بسیار رایج در ارث بری کلاس است. الگوی Bridge سعی میکند این مشکل را با تغییر ساختار از وراثت به ترکیب شی، حل کند. این به این معنی است که شما یکی از ابعاد را در یک سلسله مراتب کلاس جداگانه استخراج کرده قرار میدهید، به طوری که کلاسهای اصلی به جای اینکه همه حالتها و رفتارها را در یک کلاس داشته باشند، به یک شی از سلسله مراتب جدید ارجاع میدهند.
با پیروی از این رویکرد، میتوانیم کد مربوط به رنگ را با دو زیر کلاس: Red و Blue در کلاس خودش استخراج کنیم. سپس کلاس Shape یک فیلد مرجع دریافت میکند که به یکی از اشیاء رنگی اشاره میکند. اکنون شکل میتواند هر کار مرتبط با رنگ را به شیء رنگی مرتبط واگذار کند. آن مرجع به عنوان پلی بین کلاسهای Shape و Color عمل میکند. از این پس، افزودن رنگهای جدید نیازی به تغییر سلسله مراتب شکل نخواهد داشت و بالعکس.
انتزاع و پیادهسازی
کتاب GoF اصطلاحات Abstraction و Implementation را به عنوان بخشی از تعریف Bridge معرفی میکند. به نظر این اصطلاحات در این کتاب بیش از حد آکادمیک به نظر میرسند و باعث میشوند که الگو پیچیدهتر از آنچه هست به نظر برسد. پس از خواندن مثال ساده ذکر شده در بالا که با اشکال و رنگها مطرح شده است، بیایید معنای پشت کلمات کتاب GoF را رمزگشایی کنیم.
Abstraction (همچنین اینترفیس نامیده میشود) یک لایه کنترل سطح بالا برای برخی موجودیتها است. این لایه قرار نیست به تنهایی کار واقعی انجام دهد. باید کار را به لایه پیادهسازی (که پلتفرم نیز نامیده میشود) واگذار کند. توجه داشته باشید که ما در مورد اینترفیسها یا کلاسهای انتزاعی در زبان برنامهنویسی صحبت نمیکنیم و اینها مفاهیمی متفاوت هستند. هنگامی که در مورد برنامههای کاربردی واقعی صحبت میکنیم، انتزاع را میتوان با یک رابط کاربری گرافیکی (GUI) نشان داد، و پیادهسازی میتواند کد سیستم عامل زیرین (API) باشد که لایه GUI در پاسخ به تعاملات کاربر فراخوانی میکند.
به طور کلی، میتوانید چنین برنامهای را در دو جهت مستقل گسترش دهید:
- چندین رابط کاربری گرافیکی مختلف داشته باشید (مثلاً برای مشتریان معمولی یا مدیران طراحی شده است).
- از چندین API مختلف پشتیبانی کنید (مثلاً برای اینکه بتوانید برنامه را تحت ویندوز، لینوکس و macOS راه اندازی کنید).
در بدترین سناریو، این برنامه ممکن است شبیه یک کاسه اسپاگتی غول پیکر به نظر برسد، جایی که صدها مفهوم شرطی انواع مختلف رابط کاربری گرافیکی را با APIهای مختلف در سراسر کد به هم متصل میکنند.
شما میتوانید با استخراج کدهای مربوط به ترکیبات اینترفیس-پلتفرم خاص در کلاسهای جداگانه، به این هرج و مرج نظم بخشید. با این حال، به زودی متوجه خواهید شد که تعداد زیادی از این کلاسها وجود دارد. سلسله مراتب کلاس به صورت تصاعدی رشد خواهد کرد زیرا افزودن یک رابط کاربری گرافیکی جدید یا پشتیبانی از یک API دیگر مستلزم ایجاد کلاسهای بیشتر و بیشتری است.
بیایید سعی کنیم این مشکل را با الگوی Bridge حل کنیم. با استفاده از این الگو پیشنهاد میشود که کلاسها را به دو سلسله مراتب تقسیم کنیم:
- انتزاع (Abstraction): لایه رابط کاربری گرافیکی برنامه.
- پیاده سازی (Implementation): API های سیستم عامل.
شیء انتزاعی، ظاهر برنامه را کنترل میکند و کار واقعی را به شیء پیادهسازی مرتبط واگذار میکند. پیادهسازیهای مختلف تا زمانی که از یک اینترفیس مشترک پیروی میکنند، قابل تعویض هستند و به همان رابط کاربری گرافیکی میتوانند تحت ویندوز و لینوکس کار کنند. در نتیجه، میتوانید کلاسهای رابط کاربری گرافیکی را بدون دست زدن به کلاسهای مرتبط با API تغییر دهید. علاوه بر این، افزودن پشتیبانی برای یک سیستم عامل دیگر فقط نیاز به ایجاد یک کلاس فرعی در سلسله مراتب پیادهسازی دارد.
ساختار الگوی Bridge
در این بخش به بررسی ساختار الگوی Bridge میپردازیم و پیادهسازی آن را به صورت UML خواهیم دید.
- گام 1: Abstraction منطق کنترل سطح بالایی را ارائه میدهد. برای انجام کار سطح پایین واقعی به شی پیادهسازی متکی است.
- گام 2: Implementation اینترفیسی را که برای همه پیادهسازیهای concrete مشترک است، اعلان میکند. یک انتزاع فقط میتواند با یک شی پیادهسازی از طریق متدهایی که در اینجا تعریف شدهاند ارتباط برقرار کند. انتزاع ممکن است همان روشهای پیادهسازی را فهرست کند، اما معمولاً انتزاع رفتارهای پیچیدهای را بیان میکند که بر طیف گستردهای از عملیات اولیه اعلامشده توسط پیادهسازی متکی است.
- گام 3: پیادهسازیهای concrete حاوی کدهای مخصوص پلتفرم هستند.
- گام 4: انتزاعات پالایش شده(Refined Abstractions) انواعی از منطق کنترل را ارائه میدهند. آنها مانند والدین خود با پیادهسازیهای مختلف از طریق اینترفیس پیادهسازی عمومی کار میکنند.
- گام 5: معمولاً مشتری فقط علاقهمند به کار با انتزاع است. با این حال، این وظیفه مشتری است که شی انتزاعی را با یکی از اشیاء پیادهسازی پیوند دهد.
شبه کد(Pseudocode)
این مثال نشان میدهد که چگونه الگوی Bridge میتواند به تقسیم کد یکپارچه برنامهای که دستگاهها و کنترلهای از راه دور آنها را مدیریت میکند، کمک کند. کلاسهای Device به عنوان پیادهسازی عمل میکنند، در حالی که Remotes به عنوان انتزاع عمل میکنند.
کلاس کنترل از راه دور پایه یک فیلد مرجع را اعلام میکند که آن را با یک شی دستگاه پیوند میدهد. همه ریموتها از طریق اینترفیس عمومی دستگاه با دستگاههای دیگر کار میکنند، که به همان ریموت امکان پشتیبانی از چندین نوع دستگاه را میدهد. شما میتوانید کلاسهای کنترل از راه دور را مستقل از کلاسهای دستگاه توسعه دهید. تنها چیزی که نیاز است ایجاد یک زیر کلاس راه دور جدید است. برای مثال، یک کنترل از راه دور اصلی ممکن است فقط دو دکمه داشته باشد، اما میتوانید آن را با ویژگیهای اضافی، مانند باتری اضافی یا صفحه لمسی، گسترش دهید. کد مشتری نوع مورد نظر کنترل از راه دور را با یک شی دستگاه خاص از طریق سازنده کنترل از راه دور پیوند میدهد.
// The "abstraction" defines the interface for the "control"
// part of the two class hierarchies. It maintains a reference
// to an object of the "implementation" hierarchy and delegates
// all of the real work to this object.
class RemoteControl is
protected field device: Device
constructor RemoteControl(device: Device) is
this.device = device
method togglePower() is
if (device.isEnabled()) then
device.disable()
else
device.enable()
method volumeDown() is
device.setVolume(device.getVolume() - 10)
method volumeUp() is
device.setVolume(device.getVolume() + 10)
method channelDown() is
device.setChannel(device.getChannel() - 1)
method channelUp() is
device.setChannel(device.getChannel() + 1)
// You can extend classes from the abstraction hierarchy
// independently from device classes.
class AdvancedRemoteControl extends RemoteControl is
method mute() is
device.setVolume(0)
// The "implementation" interface declares methods common to all
// concrete implementation classes. It doesn't have to match the
// abstraction's interface. In fact, the two interfaces can be
// entirely different. Typically the implementation interface
// provides only primitive operations, while the abstraction
// defines higher-level operations based on those primitives.
interface Device is
method isEnabled()
method enable()
method disable()
method getVolume()
method setVolume(percent)
method getChannel()
method setChannel(channel)
// All devices follow the same interface.
class Tv implements Device is
// ...
class Radio implements Device is
// ...
// Somewhere in client code.
tv = new Tv()
remote = new RemoteControl(tv)
remote.togglePower()
radio = new Radio()
remote = new AdvancedRemoteControl(radio)
قابلیتها و کاربردها
- هنگامی که میخواهید یک کلاس یکپارچه را که دارای چندین نوع عملکرد است (مثلاً اگر کلاس بتواند با سرورهای پایگاه داده مختلف کار کند) تقسیم و سازماندهی کنید، از الگوی Bridge استفاده کنید. هر چه یک کلاس بزرگتر میشود، درک نحوه کارکرد آن سختتر میشود و ایجاد تغییر بیشتر طول میکشد. تغییرات ایجاد شده در یکی از متغیرهای عملکرد ممکن است نیاز به ایجاد تغییرات در کل کلاس داشته باشد که اغلب منجر به ایجاد خطا یا عدم رسیدگی به برخی از عوارض جانبی مهم میشود.
الگوی Bridge به شما امکان میدهد کلاس یکپارچه را به چندین سلسله مراتب کلاس تقسیم کنید. پس از این، میتوانید کلاسهای هر سلسله مراتب را مستقل از کلاسهای دیگر تغییر دهید. این رویکرد نگهداری کد را ساده میکند و خطر شکستن کد موجود را به حداقل میرساند.
- زمانی که نیاز دارید یک کلاس را در چندین بعد متعامد (مستقل) گسترش دهید از الگو استفاده کنید. الگوی Bridge پیشنهاد میکند که برای هر یک از ابعاد یک سلسله مراتب کلاس جداگانه استخراج کنید. کلاس اصلی به جای اینکه همه چیز را به تنهایی انجام دهد، کار مرتبط را به اشیا متعلق به آن سلسله مراتب محول میکند.
- اگر نیاز دارید که بتوانید پیادهسازیها را در زمان اجرا تغییر دهید، از الگوی Bridge استفاده کنید. با وجود اختیاری بودن، الگوی Bridge به شما امکان میدهد شیء پیادهسازی را در داخل انتزاع جایگزین کنید. این کار به آسانی تخصیص یک مقدار جدید به یک فیلد است.
به هر حال، این مورد آخر دلیل اصلی این است که بسیاری از مردم الگوی Bridge را با الگوی Strategy اشتباه میگیرند. به یاد داشته باشید که یک الگو چیزی بیش از یک روش خاص برای ساختاربندی کلاسهای شما است.
نحوه پیادهسازی
- ابعاد متعامد را در کلاسهای خود شناسایی کنید. این مفاهیم مستقل میتوانند عبارتند از:
- Abstraction/Platform، Domain/Infrastructure، front-end/back-end، یا interface/implementation.
- ببینید مشتری به چه عملیاتی نیاز دارد و آنها را در کلاس انتزاع پایه تعریف کنید.
- عملیات موجود در همه پلتفرمها را تعیین کنید. مواردی را که انتزاع به آن نیاز دارد را در اینترفیس اجرای کلی اعلام کنید.
- برای همه پلتفرمهای دامنه خود، کلاسهای پیادهسازی مشخص ایجاد کنید، اما مطمئن شوید که همه آنها از اینترفیس پیادهسازی پیروی میکنند.
- در داخل کلاس abstraction، یک فیلد مرجع برای نوع پیادهسازی اضافه کنید. انتزاع بیشتر کار را به شیء پیادهسازی که در آن فیلد ارجاع داده شده است واگذار میکند.
- اگر چندین نوع منطق سطح بالا دارید، با گسترش کلاس انتزاع پایه، برای هر نوع انتزاعات اصلاح شده ایجاد کنید.
- کد مشتری باید یک شی پیادهسازی را به سازنده انتزاع ارسال کند تا یکی را با موارد دیگر ارتباط دهد. پس از آن، مشتری میتواند پیادهسازی را فراموش کند و فقط با شی انتزاعی کار کند.
مزایا و معایب الگوی Bridge
این الگوی طراحی دارای مزایا و معایبی به شرح زیر است:
– مزایا
- میتوانید کلاسها و برنامههای مستقل از پلتفرم ایجاد کنید.
- کد مشتری با انتزاعات سطح بالا کار میکند. و در معرض جزئیات پلتفرم نیست.
- اصل باز/بسته: شما میتوانید انتزاعات و پیادهسازیهای جدید را مستقل از یکدیگر تعریف کنید.
- اصل مسئولیت واحد: شما میتوانید روی منطق سطح بالا در انتزاع و بر روی جزئیات پلتفرم در پیادهسازی تمرکز کنید.
– معایب
- ممکن است با اعمال الگو در یک کلاس بسیار منسجم، کد را پیچیدهتر کنید.
ارتباط با الگوهای دیگر
- الگوی Bridge معمولاً از قبل طراحی میشود و به شما امکان میدهد بخشهایی از یک برنامه را مستقل از یکدیگر توسعه دهید. از سوی دیگر، الگویAdapter معمولاً با یک برنامه موجود استفاده میشود تا برخی از کلاسهای ناسازگار را به خوبی با هم کار کنند.
- الگوهای Bridge، State، Strategy (و تا حدی Adapter) ساختارهای بسیار مشابهی دارند. در واقع، همه این الگوها بر اساس ترکیببندی هستند که کار را به اشیاء دیگر واگذار میکند. با این حال، همه آنها مشکلات مختلفی را حل میکنند. یک الگو فقط دستورالعملی برای ساختار کد شما به روشی خاص نیست. همچنین میتواند مشکلی را که الگو حل میکند با توسعهدهندگان دیگر در میان بگذارد.
- میتوانید از الگوی Abstract Factory در کنار Bridge استفاده کنید. این جفت شدن زمانی مفید است که برخی از انتزاعات تعریف شده توسط Bridge فقط میتوانند با پیادهسازیهای خاص کار کنند. در این مورد، Abstract Factory میتواند این روابط را کپسوله کند و پیچیدگی را از کد کلاینت پنهان کند.
- میتوانید الگوی Builder را با الگوی Bridge ترکیب کنید: کلاس Director نقش انتزاع را بازی میکند، در حالی که سازندههای مختلف به عنوان پیادهسازی عمل میکنند.