# Лекция 10. Образцы проектирования

Образец (паттерн) - это типовое проектное решение конкретной задачи проектирования, описанное специальным образом, чтобы облегчить его повторное применение.

Фактически, каждый паттерн является формализованным опытом лучших разработчиков в индустрии создания ПО. Исторически понятие образца возникло в архитектуре, введено архитектором Кристофером Александром - проектировщиком зданий и городов в конце 1970-х.

«Любой паттерн описывает задачу, которая снова и снова возникает в нашей работе, а также принцип ее решения, причем таким образом, что это решение можно потом использовать миллион раз, ничего не изобретая заново.» Кристофер Александр.

Основные составляющие части образца:

Имя. Идентифицирует образец, Хорошее имя характеризует решаемую проблему и способ ее решения.

Задача. Описание ситуации, в которой следует применять образец. Это описание включает в себя: постановку проблемы, контекст проблемы, перечень условий, при выполнении которых имеет смысл применять образец.

Решение. Описание элементов архитектуры, связей между ними, функций каждого элемента. Включает в себя UML-диаграммы.

Результаты. Следствия применения паттерна и компромиссы. Преимущества и недостатки образца. Влияние использования образца на гибкость, расширяемость и переносимость системы.

Полное описание образца в сборнике Гаммы и др. («банды четырех») включает:

- Название.
- Назначение (краткое описание функций образца и решаемой задачи).
- Другие известные названия.
- Мотивация (иллюстрация решаемой задачи проектирования).
- Применимость (описание ситуаций, в которых применим паттерн. Примеры проектирования, которые можно улучшить с помощью образца).
- Структура (диаграмма классов).
- Участники (слоты или роли, задействованные в образце, на место которых следует подставлять конкретные проектные классы).
- Отношения (диаграмма взаимодействия экземпляров классов-участников).
- Результаты.
- Реализация (сложности, подводные камни при реализации образца, рекомендации, зависимость от языка программирования).
- Пример кода.
- Известные применения (2 или более применений в различных областях).
- Родственные паттерны (связь с другими образцами, различия, использование образца в сочетании с другими).

Каталог образцов содержит 23 образца. Существуют другие каталоги - например, каталог Фаулера.

Классификация образцов:

- Порождающие образцы (способы создания экземпляров классов).
- Структурные образцы (способы задания статических связей между проектными классами).
- Образцы поведения (способы организации взаимодействий).

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

Абстрактная фабрика (Abstract Factory)

Классификация: образец порождения объектов.

Назначение: предоставляет интерфейс для создания взаимосвязанных и взаимозависимых объектов, не определяя их конкретных классов.

Мотивация: часто встает задача проектирования программной системы независимой от конкретной реализации GUI.

Ситуации применимости:

- Система не должна зависеть от того как создаются, компонуются и представляются входящие в нее объекты;
- Входящие в семейство объекты должны использоваться вместе и необходимо обеспечить выполнение этого ограничения;
- Система должна конфигурироваться одним из семейств составляющих ее объектов;
- Предоставляется библиотека классов, реализация которых скрыта за интерфейсом.

Участники:

- AbstractFactory - интерфейс с операциями для порождения экземпляров абстрактных классов-продуктов.
- ConcreteFactory - реализация порождения экземпляров конкретных классов.
- AbstractProduct - интерфейс с операциями класса-продукта.
- ConcreteProduct - реализация абстрактного продукта, объекты которой порождаются одной из конкретных фабрик.
- Client - класс пользующийся интерфейсами AbstractFactory и AbstractProduct.

Отношения:

Обычно, во время выполнения создается один экземпляр ConcreteFactory, который создает экземпляры конкретных продуктов одного из семейств. Для использования объектов другого семейства нужно породить другую конкретную фабрику.

Результаты:

- изоляция клиента от деталей реализации классов-продуктов (их имена известны только конкретной фабрике);
- упрощение замены семейств продуктов;
- набор продуктов фиксирован, добавлять новые трудно.

Представим, что нужно добавить третий класс продуктов. Потребуется добавить иерархию из 3-х классов и дополнительный метод в каждую фабрику, что довольно затратно.

Пример: две фабрики обеспечивают производство семейств классов-драйверов, работающих с низким или высоким разрешением. Предполагается, что разрешение драйвера принтера должно соответствовать дисплейному.

Фабричный метод (Factory method)

Классификация: образец порождения объектов.

Назначение: определяет интерфейс для создания объектов, но оставляет реализациям решение о том, какой объект какого именно класса создавать.

Мотивация: нужно создать экземпляр одной из реализаций интерфейса, но заранее неизвестно какой. Например, в текстовом редакторе, работающем с разными форматами при создании нового документа не известен его тип.

Ситуации применимости:

- Класс заранее не знает, объекты каких классов нужно создавать;
- Известно лишь, что это будет экземпляр одного из подклассов (реализации) какого-то абстрактного класса (интерфейса);
- Класс делегирует свои обязанности одному из вспомогательных подклассов и планируется локализовать знание о том, какой именно из подклассов берет эти обязанности на себя.

Участники:

- Product - интерфейс порождаемых объектов;
- ConcreteProduct - конкретные реализации продукта;
- Creator - интерфейс порождения экземпляров продукта, в котором определен фабричный метод (на диаграмме это createProduct()), может определять реализацию фабричного метода по умолчанию;
- ConcreteCreator - реализация создателя для порождения конкретного продукта.

Отношения:

Creator полагается на свои подклассы в определении фабричного метода, возвращающего экземпляр конкретного продукта.

Результаты:

- нет необходимости встраивать в код зависящие от приложения классы;
- код связан лишь с интерфейсом класса Product, поэтому может работать с любым конкретным продуктом;
- накладные расходы: при создании даже одного экземпляра конкретного продукта понадобится создать экземпляр ConcreteCreator.

Пример (редактор документов, фабричный метод createDoc(), конкретный тип создаваемых документов определяется в реализации фабричного метода в подклассе):

Другой пример - приложение, настраиваемое на работу с одной из двух СУБД (фабричный метод makeDB()):

Адаптер (Adapter)

Классификация: структурный образец.

Назначение: преобразует один интерфейс в другой, обеспечивая совместимость.

Мотивация: есть библиотечный класс, который можно было бы использовать, но он не поддерживает требуемый интерфейс.

Ситуация применимости: обеспечение совместимости существующего класса при его повторном использовании.

Участники:

- Target - требуемый клиентом интерфейс;
- Client - клиент;
- Adaptee - адаптируемый класс или интерфейс;
- Adapter - класс-адаптер, в методе request вызывается specificRequest из Adaptee.

Адаптер объектов (агрегация от адаптера к адаптируемому классу)

Отношения:

Адаптер получает запросы клиентов и преобразует их в вызовы операций адаптируемого класса или интерфейса.

Результаты:

- паттерн позволяет адаптировать не только объекты класса Adaptee, но и объекты его подклассов;
- для каждого адаптируемого объекта вводится один дополнительный объект.

Адаптер класса (Adaptee - класс, не интерфейс, вместо агрегации обобщение):

Отношения:

Адаптер получает запросы клиентов и преобразует их в вызовы собственных операций, унаследованных у адаптируемого класса.

Результаты:

- адаптер класса не позволяет адаптировать подклассы Adaptee, т. е. для каждого подкласса нужен отдельный адаптер;
- обходимся без дополнительного объекта, так как объект-адаптер заменяет собой экземпляр адаптируемого класса.

Composite (Компоновщик)

Классификация: структурный образец.

Назначение: организует объекты в древовидные структуры, позволяет клиентам единообразно работать с составными и элементарными объектами.

Мотивация: есть совокупность объектов-контейнеров, состоящих из элементарных объектов и других контейнеров.

Ситуации применимости:

- нужно представить иерархию объектов «часть-целое»;
- нужно дать одинаковый интерфейс к составным и простым объектам.

Участники:

- Component - компонент, определяющий единый интерфейс, содержащий реализации общих операций по умолчанию;
- Leaf - простые (элементарные) объекты;
- Composite - составной объект;
- Client - работает с объектами через интерфейс, объявленный в компоненте.

Диаграмма объектов демонстрирует пример составной структуры, соответствующей паттерну:

Отношения:

Клиенты вызывают операции Component. Если получатель запроса - лист, то он и обрабатывает запрос. Если - составной объект, то он обрабатывает запрос и дополнительно рассылает запросы своим частям.

Результаты:

- упрощается клиент, т. к. он единообразно работает с целым и частями;
- облегчается добавление новых классов - они являются подклассами Leaf или Composite;
- трудно ограничить состав видов объектов в составе композиции того или иного вида.

Рассмотрим пример. Диаграмма классов (абстрактный класс заменен интерфейсом, наследование - связями реализации):

Диаграмма объектов:

Диаграмма последовательности:

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

Мост (Bridge)

Классификация: структурный образец.

Назначение: отделить абстракцию от реализации.

Мотивация: наследование жестко привязывает реализацию к абстракции, поэтому лучше иметь иерархию наследования для интерфейсов и отдельно их реализации.

Ситуации применимости:

- обеспечение независимости абстракции и реализации;
- необходимо расширять подклассами как интерфейсы, так и их реализации;
- изменения в реализации не должны влиять на клиента;

необходимо разделить большую иерархию наследования на части.

Участники:

- Abstraction - абстракция, в которой определен интерфейс требуемый клиенту;
- RefinedAbstraction - уточненная абстракция с расширенным интерфейсом;
- Implementor - интерфейс для классов-реализаций;
- ConcreteImplementor - конкретный реализатор.

Отношения:

Абстракция перенаправляет запросы клиента одной из реализаций Implementora.

Результаты:

- реализация отделяется от интерфейса;
- чтобы заменить реализацию нет необходимости перекомпилировать абстракцию и ее клиента;
- система становится более легко модифицируемой.

Пример: Пусть есть абстракция Shape (форма), в ней есть операция draw(), отвечающая за отрисовку. В каждой конкретной форме (Rectangle, Circle) отрисовка реализуется с помощью примитивов drawLine(), drawCircle(), описанном в интерфейсе Drawing, реализуемом разными графическими пакетами DrawingV1, DrawingV2, рассчитанными на работу с разными графическими устройствами Driver1, Driver2. Диаграмма классов:

Диаграмма взаимодействия:

Если не применять образец, то у Rectangle и Circle могли бы быть два наследника, каждый из которых рассчитан на работу с одним из двух графических устройств. Т. е. в иерархии форм было бы 7 классов. Если добавить ещё формы - наследницы Shape - Triangle, PolyLine, то в первом случае при их отрисовке дополнительные классы не нужны, так как можно воспользоваться реализациями Drawing. Во втором случае иерархия разрастается, в ней становится 13 классов. Аналогично применение паттерна Мост выгодно при добавлении поддержки еще одного графического устройства. Будет достаточно добавить новую реализацию интерфейса Drawing, вместо того, чтобы заводить каждой конкретной форме наследника с реализацией отрисовки для нового устройства.

Фасад (Facade)

Структурный паттерн, идея которого в том, чтобы предоставить точку входа в подсистему (или пакет) в виде прокси-класса. Тем самым от внешних классов скрывается внутреннее устройство подсистемы или пакета, уменьшается количество связей между внешними классами и элементами пакета.

Идея продемонстрирована на диаграмме (без фасада клиентский класс имел бы связи со всеми классами подсистемы):

Пример: подсистема BillingSystem системы регистрации на курсы реализует доступ к внешней расчетной системе. Прокси-класс скрывает детали подсистемы, реализует её интерфейс. Реализация операции заключается в формировании параметра для вызова метода BillingSystemInterface::submit(theTransaction) и самого вызова этой операции.

Диаграмма взаимодействия объектов при использовании образца Фасад.

Proxy(Заместитель)

Классификация: структурный образец.

Назначение: для объекта создается суррогат, контролирующий доступ.

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

Ситуации применимости:

- удаленный заместитель (тяжелый объект невыгодно перемещать с узла на узел, поэтому на другом узле его представляет заместитель);
- виртуальный заместитель;

защищающий заместитель (проверяет права доступа).

Участники:

- Proxy - заместитель, хранящий ссылку на реальный объект;
- Class_Client - клиентский класс;
- Subject -общий интерфейс для класса и его заместителя;
- RealClass - класс, для которого создается заместитель.

Отношения:

Заместитель получает запрос клиента и переадресует его реальному классу. Детали зависят от вида заместителя.

Результаты (зависят от вида заместителя):

- удаленный заместитель скрывает тот факт, что реальный объект находится на другом узле (можно экономить сетевой трафик, если создать локальный заместитель удаленного объекта с копиями часто используемых атрибутов);
- виртуальный заместитель оптимизирует приложение («тяжелые» объекты создаются по требованию, пока в их создании нет необходимости, их представляют «легкие» объекты-заместители);
- защищающий заместитель обеспечивает нужный режим доступа к объекту.

Цепочка обязанностей (Chain of Responsibility)

Классификация: образец поведения.

Назначение: избежать привязки отправителя запроса к получателю, давая возможность передать запрос через многих (заранее неизвестно скольких) посредников.

Ситуации применимости:

- есть более одного объекта, способного обработать запрос, настоящий обработчик неизвестен и должен быть найден автоматически (передадим по цепочке, пока кто-нибудь не обработает);

адресат запроса не указан, но один из группы;

Участники:

- Handler - обобщенный обработчик, все специальные обработчики в цепочке являются его подклассами;
- ConcreteHandler - конкретный обработчик:
- обрабатывает запрос;
- знает следующего по цепочке;
- если может обработать - обрабатывает, иначе передает дальше.
- Client - отправляет запрос началу цепочки.

Например, при вызове справки о кнопке Print кнопка не знает, кто должен показывать справку, поэтому она передает запрос по цепочке окну Print, оно, в свою очередь, главному окну, которое может обработать запрос.

Результаты:

- ослабляется связность (клиент связан лишь с началом цепочки);
- гибкость в распределении обязанностей;
- нет гарантий, что запрос будет обработан, может дойти до конца цепочки и пропасть.

Iterator (Итератор)

Классификация: образец поведения.

Назначение: дать последовательный доступ к набору однородных объектов, не раскрывая его внутреннего представления.

Ситуация применимости: есть структура данных, для которой нужно реализовать один или несколько способов обхода - каждый осуществляется отдельным итератором.

Участники:

- Iterator - общий интерфейс для доступа и обхода структуры данных;
- ConcreteIterator -
- конкретный способ обхода;
- помнит позицию курсора (текущий элемент);
- Aggregate - интерфейс для создания итератора;

ConcreteAggregate - реализация интерфейса Aggregate.

Результаты:

- поддерживаются разные способы обхода;
- упрощается интерфейс Aggregate;
- есть возможность иметь одновременно несколько активных обходов (столько, сколько объектов-итераторов).

Strategy (Стратегия)

Классификация: образец поведения.

Назначение: Определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми.

Мотивация: есть несколько алгоритмов решения одной задачи, которые нежелательно «зашивать» в клиентский класс.

Ситуации применимости:

- Имеется много родственных классов, отличающихся только поведением.
- Необходимо иметь несколько разных реализаций одной операции.
- Нужно скрыть от клиента сложные, специфичные для алгоритма структуры данных.
- Упрощение кода метода, представляющего собой длинное ветвление или switch.

Участники:

- Strategy - интерфейс общий для семейства алгоритмов;
- ConcreteStrategy - конкретная стратегия, реализующая интерфейс;
- Context - контекст, направляющий запросы клиента стратегиям;
- Client - клиентский класс.

Результаты:

- Иерархия классов стратегий определяет семейство алгоритмов или поведений, которые можно повторно использовать.
- Инкапсуляция алгоритма в отдельный класс позволяет изменять его независимо от контекста.
- Избавляемся от if и switch (улучшаем читаемость кода).
- Интерфейс класса Strategy общий для всех подклассов ConcreteStrategy - неважно, сложна или тривиальна их реализация. Поэтому вполне вероятно, что некоторые стратегии не будут пользоваться всей передаваемой им информацией, особенно простые.

Приведем пример использования образца для реализации разных стратегий расчета налогов:

Предполагается, что объект класса Configuration сообщает SalesOrderссылку на объект-алгоритм расчета налогов (либо экземпляр USTax, пригодный для США, либо CanTax, пригодный для Канады). Если потребуется добавить новые способы расчета, достаточно добавить подклассы CalcTax. Обратите внимание, что в примере вместо интерфейса и реализации используется абстрактный класс и наследование.

Альтернативой предложенному решению является внесение внутрь SalesOrder::calcTax() логики выбора схемы расчета и реализация расчетов в отдельных операциях SalesOrder. Модифицируемость такого решения ниже.

Decorator (Декоратор)

Классификация: структурный образец.

Назначение: добавление объекту новых обязанностей в динамике. Альтернатива подклассам.

Мотивация: Например, хотим, чтобы библиотека GUI могла добавлять свойства (рамку) или новое поведение (прокрутку) к любому элементу GUI. Для этого «оборачиваем» элемент GUI в объект-декоратор. Декоратор имеет тот же интерфейс. Он переадресует запросы элементу, который в него «завернут».

Ситуации применимости:

- для динамического, прозрачного для клиентов добавления обязанностей объектам;
- для реализации обязанностей, которые могут быть сняты с объекта;
- когда расширение путем порождения подклассов по каким-то причинам неудобно или невозможно.

Участники:

- Component - интерфейс для декорируемых объектов;
- ConcreteComponent - класс декорируемых объектов;
- Decorator - декоратор, ссылающийся на декорируемый объект и определяющий интерфейс для декораторов, соответствующий интерфейсу Component;
- ConcreteDecorator - реализует какое-либо дополнительное поведение;
- Client - клиентский класс.

Результаты:

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

Рассмотрим пример, в котором для класса Ticket применены две обертки для печати с верхним и нижним колонтитулом. Диаграмма классов:

Диаграмма кооперации, демонстрирующая цепочку объектов, задействованных при печати:

Observer(Наблюдатель)

Классификация: образец поведения.

Назначение: Определяет зависимость типа «один ко многим» между объектами так, что при изменении состояния одного объекта все зависящие от него оповещаются об этом и автоматически обновляются..

Мотивация: таблица и связанные диаграммы, «издатель» и «подписчики» и т. п..

Ситуации применимости:

- при модификации одного объекта требуется изменить другие и заранее неизвестно, сколько именно объектов нужно изменить;
- один объект должен оповещать других, не делая предположений об уведомляемых объектах.

Участники:

- Observable - интерфейс наблюдаемых объектов, который предоставляет операции для присоединения и отделения наблюдателей;
- Observer - интерфейс наблюдателей с операциями для обновления наблюдающих объектов при изменении субъекта;
- ConcreteObservable - реализация наблюдаемых объектов;
- ConcreteObserver - реализация объектов-наблюдателей.

Взаимодействие объектов при оповещении:

Результаты:

- Субъекту неизвестны конкретные классы наблюдателей. Связи между субъектами и наблюдателями сведены к минимуму.
- Последствия изменений в субъекте непредсказуемы. Безобидная, на первый взгляд, операция над субъектом может вызвать целый ряд обновлений наблюдателей и зависящих от них объектов.

Приведем пример использования образца для реализации оповещения при изменении данных о клиенте. Например, изменение состояния (State) может влиять на отправку писем клиенту. Если вызывается Customer::setState(), то в теле его метода срабатывает вызов Customer::notify(), при обработке которого будут вызваны операции update() у всех наблюдателей подписавшихся на изменения в конкретном экземпляре класса Customer.

Менее подходящим решением является явный вызов операций нужных объектов из тела метода, обновляющего объект Customer, так как возникают явные зависимости между классами:

## Литература к лекции 10

- Гамма Э., Хелм Р., Джонсон Р., Влиссидес Дж. Приемы объектно-ориентированного проектирования. Паттерны проектирования. - СПб.: Питер, 2007.
- Фаулер М. Архитектура корпоративных приложений. - М.: Вильямс, 2007.
- Дж. Кериевски. Рефакторинг с использованием шаблонов. - М.: Вильямс, 2006 г.
- Кулямин В. В. Технологии программирования. Компонентный подход. - М.: Бином. Лаборатория знаний. 2007
- Фримен Э., Фримен Э. и др. Паттерны проектирования - СПб: Питер, 2011.