Pull to refresh

Неверное использование паттерна проектирования «Мост» / «Bridge»

Reading time 8 min
Views 11K
Предистория

Я прочитал эту статью о паттерне проектирования «Мост». Увы, его очень часто используют неверно. Более того, я затем открыл книгу Приемы объектно-ориентированного проектирования. Паттерны проектирования. Оказалось — и там авторы очень смутно декларируют причины его наличия и когда его использовать. Поэтому ниже я вам сообщу, как и зачем подобное использовать.

Обновление

Это поверхностная статья, которую можно не совсем точно трактовать. Но её достоинство, что она короткая и вводит в проблематику. У специалистов она может вызвать вопросы более глубокого содержания, а у молодых разработчиков некоторые недоразумения, т.к. я спорю по сути с «Бандой четырех», но полностью согласен с Фаулером и его подходом к рефакторингу (да и у них между собой есть противоречия) — но типа а кто я такой, чтобы спорить.

Я готовлю расширенную статью для специалистов, но она может быть полезна и молодым разработчикам механистически выучившим паттерны проектирования. Они не очень хорошо понимают когда их использовать, а специалистам думаю будет важно аргументация в смысле, что из паттернов предпочесть. Эта расширенная статья надеюсь пояснит почему надо избавляться от паттерна «Мост», а также использовать паттерн Посредник в ограниченном смысле.

Уже есть ответвление этой статьи Правильное использование паттерна «Мост» (Мост с двухсторонним движением) или MVC->«Бизнес-сущность — Визуализация — Контроллер». Где показано, что Мост/Посредник можно использовать в некой комбинации при разделении визуализации и бизнес-логики, но это практически единственная сфера для этих шаблонов. В чистой бизнес-логике и низкоуровневых/системных задачах этих паттернов следует избегать.

Но публиковать не могу — нету кармы, а как я понимаю других вариантов нет, пока не наберу. Поэтому хотите прочитать, знаете что делать :)

Что такое паттерн проектирования «Мост» на самом деле

Если Вы знаете объектно-ориентированное программирование, то со всей ответственностью заявляю, что знать о паттернах совершенно не обязательно. Паттерны — это лишь частное и не всегда самое удачное решение на базе ООП принципов.

Посмотрим, что такое паттерн «Мост», что кроется за этим заумным термином. Это не что иное как комбинация применения наследования и агрегации. Увы, часто не знают, что такое агрегация. По- простому, это когда один объект включается в другой.


Истинные причины использования паттерна «Мост»

Авторы книги декларируют такую цель: «отделить абстракцию от её реализации».

Но это совершенно неверно. Как принцип вообще — это даже вредно. Это противоречит ООП. Реализация должна полностью соответствовать тому, что декларируется в абстракции. Иначе, грош цена вообще отделению декларации от реализации. Если мы смотрим на декларацию класса, а потом оказывается, что реализация класса ей не соответствует, значит мы потеряли самое главное — мы потеряли полиморфизм. Мы тогда становимся вынуждены знакомиться с реализацией, и нам не достаточно знать только абстракцию/спецификацию.

Но и дело-то оказывается в несколько другом. И это становится ясно на примерах. Показываются примеры, где применяется наследование. Но наследование не простое, а по двум разным критериям. Например, «Окна» могут классифицироваться как для разных операционных систем: XWindow, PMWindow, ..., так и разные по назначению: окна для пиктограмм (IconWindow), окна для сообщений (MessageWindow)…

Тогда конечное окно должно содержать функции из одной иерархии и функции из другой. Т.е. быть специализированным по назначению и для операционной системы. Тогда, конечно аппарат наследования становится проблематичным, получается, что надо создать классы как перемножение этих иерархий, т.е. XIconWindow, PMIconWindow, XMessageWindow, PMMessageWindow. Конечно, это уже нарушает другой принцип ООП — создание лишних сущностей, которые есть всего лишь комбинация более простых.

Отметим, что это классический пример применения множественного наследования. Т.е. XIconWindow есть наследник от IconWindow и XWindow. Но новейшие веяния определили множественное наследование как несоответствущие ООП. Здесь обсуждать не будем, но действительно теоретически множественное наследование излишнее, и есть скорее непонимание предметной области.

Так вот, авторы паттерна предлагают другой путь. Они увидели частность, что одна из иерархий отвечает за физическую реализацию, т.е. для какой операционки это делается. И, конечно, это должно быть скрыто. Но это лишь частность, а на самом деле могут быть другие иерархии. Например, окна могут различаться еще по принципу Однооконное (SDI) или Многооконное (MDI). И что, еще одно перемножение иерархий?

И конечно, нет. Спасает тут понимание того, что эти иерархии объектов — это их свойства. А свойства как нужно реализовывать? Правильно, включая их в определяемый объект. Т.е. применять агрегацию.

Вот это и делает паттерном «Мост». Но, по сути, этот паттерн — это неуклюжие объяснение более общих принципов ООП.

А как же тогда рассуждать более правильно?

Очень просто. Есть отношения «общее-частное» — применяем наследование, есть отношения «часть-целое» применяем агрегацию. Но иногда это не столь очевидно, и нам кажется, что есть отношения «общее-частное», но по двум, трем различным критериям (как описано выше). Но мы помним, что такое рассуждение нам позволит только запутаться, и соответственно, ухудшить архитектуру программы. Тогда мы должны решить, какие из критериев более важны, а какие менее. Выстроить их в иерархию, вложить одно в другое. Авторы паттерна «Мост» считают что реализация признака операционной системе менее важна, чем назначение окна. Но это лишь частный случай. Могут быть и другие критерии, и каждый раз придется решать, что есть часть, а что целое.

Более того, частенько это придется решать, когда уже часть реализована, и появилась дополнительная постановка. Тогда придется заниматься рефакторингом и наследование превращать в агрегацию. Поэтому нужно помнить, что наследование создает между классами более жесткую связку. И всегда, когда есть возможность лучше предпочесть агрегацию. Это позволит более четко сформировать спецификацию класса и отношения между разными классами. Наследование же надо использовать очень аккуратно. Иначе потом придется его переделывать в агрегацию — а это часто не так просто. Наследование нужно использовать только тогда, когда вы четко не можете выделить критерий наследственной классификации, т.е. когда вы знаете, что этот класс есть подвид более абстрактного класса, и не по какому-то критерию, а вообще.

Но никогда не выделяйте реализацию «будильника» от абстракции «будильника». Это говорит лишь о непонимании ООП. Так в статье, которая послужила причиной для написания этой заметки в качестве «моста» вынесена сущность, объявленная реализацией будильника. Интересно, а то, что осталось в т.н. абстракции будильника — не реализация?

На самом деле там вынесены две функции — зазвонить и выдать сообщение. Причем это можно сделать разными способами. Очевидно, что объединение этих функций искусственное, т.к. возможны разные их комбинации. Поэтому абстракция будильник у нас состоит/использует не псевдореализацию, а две сущности — абстракцию менеджера сообщений и менеджера проигрывателя мелодий. А уже конкретные классы с помощью конструкторов задают какой тип сообщений и как проигрывать мелодии нужно.

Вывод один: паттерны проектирования, особенно плохо продуманные такие как обсуждавшийся «Мост», и особенно теми кто не понимает более важные принципы — лишь мешают, путают программистов, и их программы.

upd. Дополнительные пояснения.

Статья рассчитана, что читатель знаком, что такое «Мост». Очень прошу не путать паттерн «Мост» с:
1. Простой агрегацией — когда в Машину агрегируются двигатели (независимо, от того, сколько видов машин и двигателей имеется)
2. Использованием интерфейсов, похоже на первое, только с тем отличием, что вместо абстрактного класса применяется интерфейс, типа IДвигатель, IМашина

все это не дотягивает до необходимости в паттерне «Мост» (слишком мелко, т.е. паттерн «Мост» используется по замыслу авторов в более сложных случаях). Еще раз повторим декларируется, что при применении «Моста» выделяется ВСЯ реализация, т.е. ВСЕ низкоуровневые функции в другой класс, т.е. не функциональность Двигателя из Машины, а низкоуровневая функциональность (private) самой машины в МашинаРеализация.

Если вы под «Мостом» понимаете, что-то другое — то эта статья не для вас. Вы все делаете правильно, или просто еще не столкнулись с ситуацией когда нужен «Мост» — и очень хорошо, т.к. он на самом деле не нужен.

Еще «Мост» — можно реализовать с помощью множественного наследования, поэтому проверьте себя и свои примеры. Так например, первые два вида — «простая агрегация» и «использование интерфейсов» — очевидно показывает, что там некуда воткнуть множественное наследование — значит, и «Мост» тут будет не к чему.

Или бывает даже проще — разве вам придет в голову Двигатель наследовать от Машины? Или скажем ОтправительПисем от Письма? Или абстракцию базы данных или класс работы с файлами от Окна?

А ведь в примерах использования паттерна «Мост» таково начальное положение, которое надо исправить используя «Мост». Но подумайте — возможны ли такие виды наследования? Почему же вы считаете, что использовали «Мост», когда даже ваша начальная ситуация была другая и о другом?

История с «мостом» начинается когда есть наследование, и не в одном разрезе. Т.е. мало, что от Машины наследуются ЛегковаяМашина, ГрузоваяМашина. Нужно в системе еще желание наследовать от Машины — МашинаДляТуризма, МашинаДляТранспортировки… А потом еще захотеть иметь классы комбинации ЛегковаяМашинаДляТуризма, ЛегковаяМашинаДляТранспортировки… и только тогда, когда такой ужас появляется — вы будите вынуждены применить «Мост» (иначе ваша ситуация не о том, и вы даже при желании мост не примените). Но очень надеюсь не так как описывают авторы паттерна, а как объясняет данная статья.

upd2. Попробуем убрать еще одно недоразумение.

Конечно, чтобы реализовать «Мост» используется агрегация абстрактных классов или используется интерфейсы, или еще как-то. Речь не об этом. Речь о причинах — когда использовать паттерн «Мост», а когда нет. Речь о мотивировки использования. Когда есть определенная мотивировка описанная у авторов, и частично здесь в статье — то надо. Но это не означает обратного, что всюду где вы использовали интерфейсы или абстрактные классы — это будет мост. Так приведенные примеры, с Машиной и двигателем не будет паттерном «Мост», чисто семантически, хотя синтаксически вы можете увидеть те же интерфейсы — но «Мостом» это не будет.
Это лишь один аспект. Другой и главный в статье, когда вы действительно с толкнулись с ситуацией в которой авторы рекомендуют использовать «Мост», я не рекомендую этого — это вредно, и есть другие способы. Но главное я не рекомендую рассуждать как авторы паттерна — выделим реализацию из абстракции.

upd3. У меня какие-то проблемы с комментариями. Отвечаю здесь на два вопроса.

1. По поводу множественного наследования для TheShock. Ну видимо потому, что компилятор не может проверить, так вы поступили или нет, а следовательно, возможны ошибки.

2. Интересная задачка от Colwin. Один из вариантов. Первый шаг создание иерархии, OS, с наследниками Win и Os2. Создание интерфейса ImageProcessing с методами ImageMap LoadFromFile() и Paint(BigImage argImage). Win и Os2 реализуют ImageProcessing, каждый по своему. Избавляюсь от BigImageImp путем рефакторинга в подготовленную иерархию. В BigImage агрегирую интерфейс ImageProcessing im. В наследниках BigImage, вначле метод Load обращается к im.LoadFromFile(), а Show в заключение вызывает im.Paint(this).

Можете сказать, что тоже мост? Но тогда, как минимум семантика стала на порядок яснее.

Вариант второй (делается из первого, и можно даже параллельно, т.е. оставля первый вариант роботоспособным ). Можно сделать так, чтобы клиент использовал через операционку, т.е. путем вызова OS.Paint(argBigImage). Для этого нужно наоборот, чтобы операционка вызывала методы BigImage. Т.е. Paint вызывает argImageMap=LoadFromFile(), затем argBigImage.Load(argImageMap) — наследник изображения, например, BmpBigImage делает свою специфику. Затем операционка вызывает argBigImage.PrepareForShow() (это уточняется смысл метода Show у изображения, т.к. по сути оказывается он не может себя отобразить), и тогда операционка реально отображает. И тут уже точно никакого моста. И если убрать первое решение, можно избавится от интерфейса ImageProcessing, но это по желанию. И заметим между прочим, что второй вариант можно придумать только разложив первый на верную семантику, а мысля в рамках моста — действительно вариантов нет, и решение плохое.
Tags:
Hubs:
+8
Comments 231
Comments Comments 231

Articles