Autumn SALE

Міст

Також відомий як: Bridge

Суть патерна

Міст — це структурний патерн проектування, який розділяє один або кілька класів на дві окремі ієрархії — абстракцію та реалізацію, дозволяючи змінювати код в одній гілці класів, незалежно від іншої.

Патерн Міст

Проблема

Абстракція? Реалізація?! Звучить страхітливо! Розгляньмо простенький приклад, щоб зрозуміти про що йде мова.

У вас є клас геометричних Фігур, який має підкласи Круг та Квадрат. Ви хочете розширити ієрархію фігур за кольором, тобто мати Червоні та Сині фігури. Але для того, щоб все це об’єднати, доведеться створити 4 комбінації підкласів на зразок СиніКруги та ЧервоніКвадрати.

Проблема патерна Міст

Кількість підкласів зростає в геометричній прогресії.

При додаванні нових видів фігур і кольорів кількість комбінацій зростатиме в геометричній прогресії. Наприклад, щоб ввести в програму фігури трикутників, доведеться створити відразу два нових класи трикутників, по одному для кожного кольору. Після цього введення нового кольору вимагатиме створення вже трьох класів, по одному для кожного виду фігур. Чим далі, тим гірше.

Рішення

Корінь проблеми полягає в тому, що ми намагаємося розширити класи фігур одразу в двох незалежних площинах — за видом та кольором. Саме це призводить до розростання дерева класів.

Патерн Міст пропонує замінити спадкування на делегування. Для цього потрібно виділити одну з таких «площин» в окрему ієрархію і посилатися на об’єкт цієї ієрархії, замість зберігання його стану та поведінки всередині одного класу.

Рішення патерна Міст

Розмноження підкласів можна зупинити, розбивши класи на кілька ієрархій.

Таким чином, ми можемо зробити Колір окремим класом з підкласами Червоний та Синій. Клас Фігур отримає посилання на об’єкт Кольору і зможе делегувати йому роботу, якщо виникне така необхідність. Такий зв’язок і стане мостом між Фігурами та Кольором. При додаванні нових класів кольорів не потрібно буде звертатись до класів фігур і навпаки.

Абстракція і Реалізація

Ці терміни було введено в книзі GoF  при описі Мосту. На мій погляд, вони виглядають занадто академічними та показують патерн складнішим, ніж він є насправді. Пам’ятаючи про приклад з фігурами й кольорами, давайте все ж таки розберемося, що мали на увазі автори патерна.

Отже, абстракція (або інтерфейс) — це уявний рівень керування чим-небудь, що не виконує роботу самостійно, а делегує її рівню реалізації (який зветься платформою).

Тільки не плутайте ці терміни з інтерфейсами або абстрактними класами вашої мови програмування — це не одне і те ж саме.

Якщо говорити про реальні програми, то абстракцією може виступати графічний інтерфейс програми (GUI), а реалізацією — низькорівневий код операційної системи (API), до якого графічний інтерфейс звертається, реагуючи на дії користувача.

Ви можете розвивати програму у двох різних напрямках:

  • мати кілька різних GUI (наприклад, для звичайних користувачів та адміністраторів).
  • підтримувати багато видів API (наприклад, працювати під Windows, Linux і macOS).

Така програма може виглядати як один великий клубок коду, в якому змішано умовні оператори рівнів GUI та API.

Захист від змін

Коли зміни беруть проект в «осаду», вам легше відбиватися, якщо розділити монолітний код на частини.

Ви можете спробувати структурувати цей хаос, створивши для кожної з варіацій інтерфейсу-платформи свої підкласи. Але такий підхід призведе до зростання класів комбінацій, і з кожною новою платформою їх буде все більше й більше.

Ми можемо вирішити цю проблему, застосувавши Міст. Патерн пропонує розплутати цей код, розділивши його на дві частини:

  • Абстракцію: рівень графічного інтерфейсу програми.
  • Реалізацію: рівень взаємодії з операційною системою.
Варіант крос-платформової архітектури

Один з варіантів крос-платформової архітектури.

Абстракція делегуватиме роботу одному з об’єктів реалізації. Причому, реалізації можна буде взаємозаміняти, але тільки за умови, що всі вони слідуватимуть єдиному інтерфейсу.

Таким чином, ви зможете змінювати графічний інтерфейс програми, не чіпаючи низькорівневий код роботи з операційною системою. І навпаки, ви зможете додавати підтримку нових операційних систем, створюючи нові підкласи реалізації, без необхідності правити код у класах графічного інтерфейсу.

Структура

Структура класів патерна МістСтруктура класів патерна Міст
  1. Абстракція містить керуючу логіку. Код абстракції делегує реальну роботу пов’язаному об’єктові реалізації.

  2. Реалізація описує загальний інтерфейс для всіх реалізацій. Всі методи, які тут описані, будуть доступні з класу абстракції та його підкласів.

    Інтерфейси абстракції та реалізації можуть або збігатися, або бути абсолютно різними. Проте, зазвичай в реалізації живуть базові операції, на яких будуються складні операції абстракції.

  3. Конкретні реалізації містять платформо-залежний код.

  4. Розширені абстракції містять різні варіації керуючої логіки. Як і батьківский клас, працює з реалізаціями тільки через загальний інтерфейс реалізацій.

  5. Клієнт працює тільки з об’єктами абстракції. Не рахуючи початкового зв’язування абстракції з однією із реалізацій, клієнтський код не має прямого доступу до об’єктів реалізації.

Псевдокод

У цьому прикладі Міст ділить монолітний код приладів та пультів на дві частини: прилади (виступають реалізацією) і пульти керування ними (виступають абстракцією).

Структура класів прикладу патерна Міст

Приклад поділу двох ієрархій класів — приладів та пультів керування.

Клас пульта має посилання на об’єкт приладу, яким він керує. Пульти працюють з приладами через загальний інтерфейс. Це дає можливість зв’язати пульти з різними приладами.

Пульти можна розвивати незалежно від приладів. Для цього достатньо створити новий підклас абстракції. Ви можете створити як простий пульт з двома кнопками, так і більш складний пульт з тач-інтерфейсом.

Клієнтському коду залишається вибрати версію абстракції та реалізації, з якими він хоче працювати, та зв’язати їх між собою.

// Клас пультів має посилання на пристрій, яким керує. Методи
// цього класу делегують роботу методам пов'язаного пристрою.
class Remote is
    protected field device: Device
    constructor Remote(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)


// Ви можете розширювати клас пультів, не чіпаючи код пристроїв.
class AdvancedRemote extends Remote is
    method mute() is
        device.setVolume(0)


// Всі пристрої мають спільний інтерфейс, тому з ними може
// працювати будь-який пульт.
interface Device is
    method isEnabled()
    method enable()
    method disable()
    method getVolume()
    method setVolume(percent)
    method getChannel()
    method setChannel(channel)


// Разом з цим, кожен пристрій має особливу реалізацію.
class Tv implements Device is
    // ...

class Radio implements Device is
    // ...


// Десь у клієнтському програмному коді.
tv = new Tv()
remote = new Remote(tv)
remote.togglePower()

radio = new Radio()
remote = new AdvancedRemote(radio)

Застосування

Якщо ви хочете розділити монолітний клас, який містить кілька різних реалізацій якої-небудь функціональності (наприклад, якщо клас може працювати з різними системами баз даних).

Чим більший клас, тим важче розібратись у його коді, і тим більше це розтягує час розробки. Крім того, зміни, що вносяться в одну з реалізацій, призводять до редагування всього класу, що може викликати появу несподіваних помилок у коді.

Міст дозволяє розділити монолітний клас на кілька окремих ієрархій. Після цього ви можете змінювати код в одній гілці класів незалежно від іншої. Це спрощує роботу над кодом і зменшує ймовірність внесення помилок.

Якщо клас потрібно розширювати в двох незалежних площинах.

Міст пропонує виділити одну з таких площин в окрему ієрархію класів, зберігаючи посилання на один з її об’єктів у початковому класі.

Якщо ви хочете мати можливість змінювати реалізацію під час виконання програми.

Міст дозволяє замінювати реалізацію навіть під час виконання програми, оскільки конкретна реалізація не «зашита» в клас абстракції.

До речі, через цей пункт Міст часто плутають із Стратегією. Зверніть увагу, що у Моста цей пункт займає останнє місце за значущістю, оскільки його головна задача — структурна.

Кроки реалізації

  1. Визначте, чи існують у ваших класах два непересічних виміри. Це може бути функціональність/платформа, предметна область/інфраструктура, фронт-енд/бек-енд або інтерфейс/реалізація.

  2. Продумайте, які операції будуть потрібні клієнтам, і опишіть їх у базовому класі абстракції.

  3. Визначте поведінки, які доступні на всіх платформах, та виберіть з них ту частину, яка буде потрібна для абстракції. На підставі цього опишіть загальний інтерфейс реалізації.

  4. Для кожної платформи створіть власний клас конкретної реалізації. Всі вони повинні дотримуватися загального інтерфейсу, який ми виділили перед цим.

  5. Додайте до класу абстракції посилання на об’єкт реалізації. Реалізуйте методи абстракції, делегуючи основну роботу пов’язаному об’єкту реалізації.

  6. Якщо у вас є кілька варіацій абстракції, створіть для кожної з них власний підклас.

  7. Клієнт повинен подати об’єкт реалізації до конструктора абстракції, щоб зв’язати їх разом. Після цього він може вільно використовувати об’єкт абстракції, забувши про реалізацію.

Переваги та недоліки

  • Дозволяє будувати платформо-незалежні програми.
  • Приховує зайві або небезпечні деталі реалізації від клієнтського коду.
  • Реалізує принцип відкритості/закритості.
  • Ускладнює код програми внаслідок введення додаткових класів.

Відносини з іншими патернами

  • Міст проектують заздалегідь, щоб розвивати великі частини програми окремо одну від одної. Адаптер застосовується постфактум, щоб змусити несумісні класи працювати разом.

  • Міст, Стратегія та Стан (а також трохи і Адаптер) мають схожі структури класів — усі вони побудовані за принципом «композиції», тобто делегування роботи іншим об’єктам. Проте вони відрізняються тим, що вирішують різні проблеми. Пам’ятайте, що патерни — це не тільки рецепт побудови коду певним чином, але й описування проблем, які призвели до такого рішення.

  • Абстрактна фабрика може працювати спільно з Мостом. Це особливо корисно, якщо у вас є абстракції, які можуть працювати тільки з деякими реалізаціями. В цьому випадку фабрика визначатиме типи створюваних абстракцій та реалізацій.

  • Патерн Будівельник може бути побудований у вигляді Мосту: директор гратиме роль абстракції, а будівельники — реалізації.

Приклади реалізації патерна

Міст на C# Міст на C++ Міст на Go Міст на Java Міст на PHP Міст на Python Міст на Ruby Міст на Rust Міст на Swift Міст на TypeScript