handbookНаписать

patterns/structural/bridge/README.md · тема · 4 языка · 22 теста

+ практика: отчёты в разных форматах

Мост (Bridge) ​

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

Проблема ​

Есть телевизор и радио, к ним обычный пульт и пульт с кнопкой «без звука». Если пульт наследует устройство, получаются TvRemote, RadioRemote, AdvancedTvRemote, AdvancedRadioRemote: четыре класса на два устройства и два пульта. Третье устройство добавляет ещё два класса, третий пульт — ещё три. Логика «прибавить громкость» копируется в каждый, и исправление в одном месте забывают сделать в остальных.

Беда в том, что в одном наследовании смешаны две оси изменений: что умеет пульт и как устроено устройство.

Решение ​

Разрезать по осям. Абстракция RemoteControl — то, с чем работает пользователь: включить, громче, тише. Она не знает, какое перед ней устройство, и держит ссылку на реализацию — интерфейс Device с примитивными операциями: включено ли, включить, выключить, прочитать и задать громкость. Эта ссылка и есть мост.

Дальше оси растут независимо: новый пульт AdvancedRemote расширяет абстракцию и работает с любым устройством, новое устройство реализует Device и работает с любым пультом. Вместо N x M классов получается N + M.

Структура ​

bridge

«interface»

Device

+isEnabled() : bool

+enable()

+disable()

+volume() : int

+setVolume(percent)

Tv

Radio

RemoteControl

#device: Device

+togglePower()

+volumeUp()

+volumeDown()

AdvancedRemote

+mute()

Устройства 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 и получает его методы без наследования
PythonDevice — 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), а некоторые форматы просто не умеют выразить часть содержимого. Практика: отчёты в разных форматах показывает два отчёта над тремя форматами без шести классов и тесты, где новый формат и новый отчёт добавляются без правки другой стороны.

Связанные темы ​

© 2014-2026 Тимур ТурдыевСайт и справочник работают на VPS от AdminVPS