Эти шаблоны объединяются под общим названием "Обёртка" (Wrapper). Суть их, как и у Фасада, в том, чтобы дать доступ к некому функционалу, который они заворачивают внутрь себя. Почему их столько разных, сейчас разберёмся.
Предыдущие части: Фасад, ПабСаб, Наблюдатель, Визитёр, Фабрика, Синглтон, Стратегия, MVC, Вступление
Adapter
С адаптерами мы близко знакомы в бытовом плане. Это, например, переходники для электровилок с американской системы на европейскую или разные разъёмы USB.
Суть их в том, что они никак не меняют функционал – напряжение в розетке остаётся таким же, и USB продолжает работать так же – но меняют сопряжение, то есть интерфейс.
Следовательно, паттерн Адаптер применяется тогда, когда у вас есть рабочий функционал с определённым интерфейсом, но этот интерфейс вам не подходит, а менять его тоже проблематично.
В этом случае вы создаёте класс-адаптер. Он имеет нужный вам интерфейс, но внутри себя перенаправляет методы этого интерфейса на другой (целевой) интерфейс, таким образом сопрягая два интерфейса.
В качестве примера рассмотрим соединения с разными базами данных на PHP. Для работы с расширением mysqli мы пишем адаптер:
Для расширения PDO пишем другой адаптер:
Оба адаптера имеют метод-конструктор с параметрами соединения, и метод query() – весьма упрощённый, чисто для примера.
Внутри же адаптеров эти методы реализованы по-разному, так как у расширений mysqli и PDO разные интерфейсы. Выбрав, с каким расширением мы намерены работать, мы создаём соответствующий адаптер и далее пользуемся его методами:
Proxy
Шаблон Прокси это Адаптер с точно таким же интерфейсом, как целевой. Если интерфейс не меняется, для чего он тогда нужен?
Прокси может совершить дополнительные действия перед тем, как передать управление в целевой интерфейс или вернуть результат. Для того, кто вызывает метод, всё выглядит штатно, но прокси может скорректировать параметры метода или сделать что-то ещё.
Например, у нас есть класс Security с методом hash():
Результат возвращается в виде 16-ричной строки, но вдруг нам понадобилось, чтобы к нему приписывался префикс '0x'. Оригинальный класс менять мы не можем. Тогда мы делаем прокси-класс:
Прокси имеет ссылку на оригинальный класс и вызывает его метод hash(), а затем присоединяет к результату префикс.
Decorator
Когда у нас есть класс с определённым функционалом, иногда в нём чего-то не хватает. Тогда класс можно расширить, дописав необходимые методы. Но это бывает нецелесообразно, так как можно излишне усложнить класс и вообще потерять его изначальное назначение и идеологию использования.
В этом случае мы "декорируем" класс, подобно новогодней ёлочке. Мы не меняем его, но навешиваем на него дополнительные плюшки.
Для декорирования есть два способа.
Первый способ это простое наследование. Мы можем отнаследовать от рабочего класса новый класс, и в нём дописать новые методы. По сути любое наследование применяет шаблон Декоратор.
Второй способ называется композицией, или делегацией. Это сам по себе отдельный паттерн.
Вместо наследования от родителя мы создаём новый, независимый класс с новыми методами, а для доступа к старым методам внутрь него помещаем (делегируем) экземпляр рабочего класса.
Если кроме новых методов нужно также напрямую обращаться к старым, новый класс может для удобства продублировать у себя методы старого класса. В этих дубликатах управление будет просто перебрасываться на старый класс. Тогда мы получаем фактически Прокси с дополнительными методами.
У меня была необходимость в декораторах, когда надо было выводить на экран спрайты и в определённых ситуациях рисовать вокруг спрайта эффект сияния.
Реализация была примерно такая (пишу пример на PHP, в котором нет вывода графики на экран, но это мне не мешает :)
Был определён класс Sprite:
И декоратор Shine:
Оба класса используют одинаковый интерфейс с методом render() для вывода графики на экран. Так что для программы разницы нет, это спрайт или декоратор.
Декоратор в своём методе сначала вызывает render() у спрайта, чтобы он нарисовался, а затем рисует поверх него свою картинку.
- Имея несколько декораторов, я мог придавать совершенно любым спрайтам нужные свойства: заставлять их вращаться, быть полупрозрачными, мигать и т.д.
- Так как интерфейс спрайта и декоратора одинаков, можно в конструктор декоратора передать не спрайт, а другой декоратор. Таким образом, можно вкладывать декораторы друг в друга, и все они будут работать.
Итоги
Если вы ещё раз посмотрите на описания и примеры кода, то обнаружите, что эти шаблоны очень похожи.
Например, почему Адаптер это не Декоратор или не Фасад?
Выделим основные черты каждого шаблона:
- Фасад: простой доступ к страшной помойке
- Адаптер: не меняет функционал, но адаптирует интерфейс
- Прокси: не меняет интерфейс, но адаптирует функционал
- Декоратор: расширяет функционал, не трогая исходный класс
По факту же в ваших реализациях может оказаться некий гибрид Фасада и Прокси, или Адаптера и Декоратора.
Нет никакого смысла переживать по этому поводу. Фактически, один шаблон от другого может отличаться лишь названием, что вы и должны подчеркнуть в названиях своих классов. Например, если вы назвали класс AdapterPDO, то тем самым вы декларируете себе и другим, что намерены использовать шаблон Адаптер, главное правило которого – адаптация интерфейса без изменения функционала.
Естественно, тогда вы должны придерживаться именно такого правила, и если в реализации оно остаётся главным – значит вы применили именно этот шаблон, даже если в нём есть вещи, похожие на другие шаблоны.
Читайте дальше: