Паттерны проектирования, впервые опубликованные в книге «Design Patterns — Elements of Reusable Object-Oriented Software» Эрихом Гаммой и его товарищами, можно разделить на три группы: порождающие паттерны проектирования (Creational Patterns), структурные паттерны проектирования классов/объектов (Structural Patterns) и паттерны проектирования поведения классов/объектов (Behavioral Patterns). В это статье я рассмотрю основные структурные шаблоны.
К числу структурных шаблонов принадлежат следующие шаблоны:
− Адаптер (Adapter);
− Декоратор (Decorator) или Оболочка (Wrapper);
− Заместитель (Proxy) или Суррогат (Surrogate);
− Компоновщик (Composite);
− Мост (Bridge), Описатель (Handle) или Тело (Body);
− Приспособленец (Flyweight);
− Фасад (Facade).
Шаблон «Адаптер» решает следующую проблему:
− необходимо обеспечить взаимодействие несовместимых интерфейсов или как создать единый устойчивый интерфейс для нескольких компонентов с разными интерфейсами.
Стандартное решение для паттерна «Адаптер»:
− конвертировать исходный интерфейс компонента к другому виду с помощью промежуточного объекта — адаптера, то есть, добавить специальный объект с общим интерфейсом в рамках данного приложения и перенаправить связи от внешних объектов к этому объекту-адаптеру.
Другими словами, если у нас есть библиотека все методы в которой, к примеру, на немецком языке, то для человека владеющего только английским крайне неудобно постоянно пользоваться словарем, чтобы работать с данной библиотекой. Можно применить шаблон «Адаптер», переведя все на английский язык.
Второй вариант, если у нас есть две библиотеки, которые делают одно и то же, но методы в них называются по-разному. Мы не можем позвонить разработчику библиотеки и попросить, чтобы они сделали такой же интерфейс, как у конкурента, потому что нам неудобно с ним работать. Поэтому мы можем обе эти библиотеки привести к общему интерфейсу.
Рис. 1. UML-диаграмма паттерна «Адаптер»
Например, у нас есть клиентский код, которому что-то нужно. И есть сервер, который делает все, что нам надо, но делает не так, как нам надо: сигнатуры методов неправильные, названия методов на немецком, или нам нужно, чтобы у сервера был другой интерфейс, другой набор методов и т. д. Для этого, мы между клиентом и сервером вставляем адаптер. Для удобства создадим интерфейс (Target), который будет имплементировать наш адаптер.
В последнее время Computer science пришел к тому, что интерфейс является принадлежностью не того, кто его имплементирует, а того, кто его вызывает. Потому что мы не знаем какие нужны методы, это знает тот, кто будет этот интерфейс вызывать, то есть клиент. Если у нас есть программист, который пишет на самом низком уровне, который пишет на среднем уровне, и который отвечает за бизнес-логику, то интерфейс для низкого уровня должен написать тот, кто работает на среднем, потому что он будет им пользоваться. А тот, кто пишет на нижнем должен этот интерфейс имплементировать, то есть написать класс-адаптер, который вызывает серверные метода. С точки зрения клиента работа идет с этим интерфейсом, а не с сервером.
«Адаптер» применяется, когда система поддерживает требуемые данные, но имеет неподходящий интерфейс. К его плюсам относят инкапсуляцию реализации внешних классов (компонентов, библиотек), независимость системы от интерфейса внешних классов. Более того, переход на использование других внешних классов не требует переделки самой системы, достаточно реализовать один класс Adapter.
Шаблон Декоратор (Decorator) решает следующую проблему:
− Возложить дополнительные обязанности (прозрачные для клиента) на отдельный объект, а не класс в целом.
Применение нескольких «Декораторов» к одному «Компоненту» позволяет произвольным образом сочетать обязанности, например, одно свойство можно добавить дважды.
Рис. 2. UML-диаграмма паттерна «Декоратор»
Другими словами, нам нужно добавлять обязанности к классу динамически, то есть нам нужно в runtime определять, какая функциональность нам нужна от класса. Если у нас есть обращение от клиента к серверу, и нам нужно добавить дополнительную функциональность. Например, клиент хочет то кэширование, то логирование, то аутентификацию, то что-то из этого одновременно и т. д. Классическое решение написать отдельный класс для сервера с кэшированием и т. д. Так же написать класс для сервера с и логированием, и кэшированием и т. д. Таким образом, чем больше требований, тем больше классов нужно написать, количество которых определяется комбинаторной комбинацией. Это не совсем удобно.
«Декоратор» предлагает: вызывать сервер через его интерфейс. Для реализации функциональности создать класс-наследник (по сути адаптер) этого интерфейса, который, выполняя свою функциональность (например, логирование) вызывает методы сервера, но через интерфейс сервера, то есть свой родительский интерфейс, для этого в конструкторе наш класс принимает instance, то есть содержит в конструкторе поле ServerInterface.
Аналогично для класса, осуществляющего кэширование, аутентификацию и т. д. Теперь клиент, который сам решает, какая функциональность ему нужна, может проинстанциировать один адаптер, передав ему в конструкторе другой адаптер. И такую цепочку можно осуществлять до бесконечности. Важно, что в зависимости от того, кого и кому мы передаем, код будет сначала кэшировать, а потом логировать, или наоборот.
Если мы, к примеру, пишем класс Report, который будет выводить одну строчку, но при этом каждый раз её нужно будет обрамить по-новому, очень удобно использовать «Декоратор». Или создаем окошко, у которого может быть или горизонтальный скролл-бар, или вертикальный скролл-бар, или вообще никакого.
К основным преимуществам паттерна «Декоратор» относят:
− большая гибкость, чем у статического наследования: можно добавлять и удалять обязанности во время выполнения программы в то время, как при использовании наследования надо было бы создавать новый класс для каждой дополнительной обязанности;
− данный паттерн позволяет избежать перегруженных методами классов на верхних слоях уровня иерархии — новые обязанности можно добавлять по мере необходимости.
К недостаткам данного паттерна относят то, что «Декоратор» и его «Компонент» не идентичны, и, кроме того, получается, что система состоит из большого числа мелких объектов, которые похожи друг на друга и различаются только способом взаимосвязи, а не классом и не значениями своих внутренних переменных. Такая система сложна в изучении и отладке.
Литература:
- Гамма Э., Хелм Р., Джонсон Р., Влиссидес Дж. Приемы объектно-ориентированного проектирования. Паттерны проектирования. СПб.: Питер, 2001.
- Ларман К. Применение UML и шаблонов проектирования. Вильямс, 2002.
- DeanLeffingwell, Don Widrig. Managing Software Requirements. Addison-Wesley, 2000.
- Rational Unified Process. Versions 2001–2003. Rational Software Corporation. http://www.rational.com/
- Мартин Фаулер — Архитектура корпоративных программных — М.: «Вильямс», 2007. — С. 544.
- Алан Шаллоуей, Джеймс Р. Тротт. Шаблоны проектирования. Новый подход к объектно-ориентированному анализу и проектированию = Design Patterns Explained: A New Perspective on Object-Oriented Design. — М.: «Вильямс», 2002. — С. 288. —ISBN 0–201–71594–5.
- Эрик Фримен, Элизабет Фримен. Паттерны проектирования = Head First Design Patterns. — СПб: Питер. — 656 с. — ISBN 978–5–459–00435–9.