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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


Участники:

Отношения:

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


Результаты:

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

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


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

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

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

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

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


Участники:

Отношения:

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

Результаты:


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


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

Адаптер (Adapter)

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

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

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

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

Участники:

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

Отношения:


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

Результаты:


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

Отношения:

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


Результаты:

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

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

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

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

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


Участники:

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


Отношения:

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

Результаты:


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

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

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

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

Мост (Bridge)

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

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

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

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


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

Участники:

Отношения:

Абстракция перенаправляет запросы клиента одной из реализаций 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(Заместитель)

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

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

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

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


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

Участники:

Отношения:


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

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

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

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

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

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


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

Участники:

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

Результаты:

Iterator (Итератор)

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

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

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

Участники:


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

Результаты:

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

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

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

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

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

Участники:

Результаты:


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

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

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

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

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

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

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

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

Участники:

Результаты:


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


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

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

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

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

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

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

Участники:


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

Результаты:

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


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


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