Мост (Bridge)
Разделяет иерархию на две — абстракцию и реализацию, — чтобы каждая росла сама по себе, а не перемножалась с другой.
Проблема
Есть телевизор и радио, к ним обычный пульт и пульт с кнопкой «без звука». Если пульт наследует устройство, получаются TvRemote, RadioRemote, AdvancedTvRemote, AdvancedRadioRemote: четыре класса на два устройства и два пульта. Третье устройство добавляет ещё два класса, третий пульт — ещё три. Логика «прибавить громкость» копируется в каждый, и исправление в одном месте забывают сделать в остальных.
Беда в том, что в одном наследовании смешаны две оси изменений: что умеет пульт и как устроено устройство.
Решение
Разрезать по осям. Абстракция RemoteControl — то, с чем работает пользователь: включить, громче, тише. Она не знает, какое перед ней устройство, и держит ссылку на реализацию — интерфейс Device с примитивными операциями: включено ли, включить, выключить, прочитать и задать громкость. Эта ссылка и есть мост.
Дальше оси растут независимо: новый пульт AdvancedRemote расширяет абстракцию и работает с любым устройством, новое устройство реализует Device и работает с любым пультом. Вместо N x M классов получается N + M.
Структура
Устройства Tv и Radio реализуют одни и те же примитивы, но по-своему: громкость телевизора ограничена 100, радио — 30. Граница живёт в устройстве, поэтому пульт просто прибавляет шаг, а устройство само решает, сколько на самом деле можно. Так знание о железе не протекает в абстракцию.
Как это собирается
Мост собирается там, где известна конфигурация дома: при запуске приложения или при добавлении комнаты. В примере две независимые настройки на каждую комнату - какое там устройство и какой пульт. Устройство создаётся из карты драйверов (Tv, Radio), пульт выбирается отдельно (RemoteControl или AdvancedRemote) и получает устройство в конструкторе. Комнату с устройством без драйвера приложение пропускает и пишет причину, остальные комнаты работают.
Дальше сценарий "вечер" жмёт одни и те же кнопки на всех пультах: включить, три раза громче. Сколько громкости получилось, решает устройство: телевизор дошёл до 50, радио упёрлось в свои 30. Ночной режим зависит уже от пульта: у AdvancedRemote есть mute(), обычный пульт просто убавляет на шаг. Ни одна из этих веток не знает, какое устройство за пультом.
Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.
Реализации
Расширенная абстракция AdvancedRemote добавляет mute(), опираясь на тот же setVolume(). Ей не нужно знать, с телевизором она работает, с радио или с колонкой, которую напишут через год: всё, что ей нужно, уже есть в Device.
| Язык | Особенность |
|---|---|
| PHP | Интерфейс Device, ссылка на него в protected readonly свойстве; AdvancedRemote наследует RemoteControl |
| Go | Интерфейс Device; AdvancedRemote встраивает *RemoteControl и получает его методы без наследования |
| Python | Device — Protocol: устройство не обязано ни от чего наследоваться, достаточно нужных методов |
| TypeScript | Структурный интерфейс: устройством может быть даже объектный литерал; параметр-свойство protected readonly device |
Запуск: make test T=patterns/structural/bridge
Плюсы и минусы
Тесты проверяют свойства моста, а не кнопки. Первый собирает все четыре пары «пульт + устройство» из двух пультов и двух устройств, ни одна пара не написана отдельным классом. Второй показывает, что ограничение громкости — дело устройства: один и тот же пульт доводит телевизор до 50, а радио только до 30. Третий — расширенный пульт работает с любым устройством. Четвёртый подключает колонку, объявленную прямо в тесте, и записывает, какие примитивы вызывал пульт: код пультов для неё не менялся.
- ✅ N + M классов вместо N x M: новый пульт или новое устройство — один класс.
- ✅ Абстракцию и реализацию можно менять и тестировать отдельно; реализацию легко подменить в тесте.
- ✅ Реализацию можно выбрать или сменить во время работы: пульт получает устройство в конструкторе.
- ❌ Лишний уровень косвенности. Если у одной из осей всего один вариант, мост только усложняет код.
- ❌ Интерфейс реализации трудно угадать заранее: примитивы должны подходить всем будущим устройствам, иначе новое устройство потребует правки интерфейса и всех старых реализаций.
Когда не применять
- Ось изменений одна: несколько устройств и один пульт. Хватит интерфейса и полиморфизма.
- Нужно подружить уже существующий класс с чужим интерфейсом: это Адаптер. Мост проектируют заранее, адаптер прикручивают потом.
- Комбинации не независимы (не каждый пульт подходит к каждому устройству): мост обещает любую пару, и придётся расставлять проверки.
Отличия между языками
- PHP — классический вариант: интерфейс реализации и иерархия абстракций через наследование.
protected readonlyдаёт наследникам доступ к устройству и запрещает его подменить после создания. - Go — наследования нет, поэтому расширенная абстракция встраивает базовую:
AdvancedRemote{*RemoteControl}получаетTogglePowerиVolumeUp, аMuteпишет сам, обращаясь к тому жеdevice. Устройства реализуютDeviceнеявно. - Python —
Protocolделает мост структурным: колонка из теста подходит пульту, хотя не знает оDevice. Общее состояние устройств вынесено в обычный базовый классAppliance, но это деталь реализации, а не часть моста. - TypeScript — интерфейсы структурные, как в Go, поэтому в тесте устройство — объектный литерал с пятью функциями. Иерархия пультов — обычные классы.
Практика
Учебный мост соединяет пару иерархий, которые выбирает тест, и обе стороны тривиальны. В проекте реализация выбирается из запроса, у каждой своя сложность (экранирование, кавычки CSV), а некоторые форматы просто не умеют выразить часть содержимого. Практика: отчёты в разных форматах показывает два отчёта над тремя форматами без шести классов и тесты, где новый формат и новый отчёт добавляются без правки другой стороны.
Связанные темы
- Практика: отчёты в разных форматах — отчёты x форматы вывода без взрыва классов
- Адаптер (Adapter) — похожая схема, но подгоняет готовый интерфейс задним числом, а мост разделяет иерархии заранее.
- Стратегия (Strategy) — тоже композиция вместо наследования, но подменяет один алгоритм, а не целую сторону иерархии.
- Абстрактная фабрика (Abstract Factory) — может создавать согласованную пару абстракции и реализации.