Паттерн Decorator
В случае, если вы не совсем знакомы с тем, что такое паттерн, то просто думайте о паттернах, как о проверенных временем решениях для определенных задач.
Поскольку большинство книг о паттернах не содержат примеров для ColdFusion, то для ColdFusion программистов изучение паттернов не просто. Чтобы разобраться в паттернах, давайте вместе создадим полностью автоматизированный магазин кофе! Назовем наш магазин AutoBarista, а напиток - StartBoard.
При создании магазина кофе, настоящим успехом будет создание машины, которая позволит создавать продукт с учетом разных потребностей потребителей. С оборудованием разобраться просто, но правильно спроектировать программное обеспечение будет большим испытанием.
Рисунок 1. Потрясающий интерфейс AutoBarista
Начнем с прототипа. Это будем меню, с которым потребители сталкиваются в сотнях других подобных магазинах (см. Рис.1). Вот код, создающий это меню:
Самой важной вещью, приводящей проект к успеху, является легкость его дальнейшего поддерживания, т.к. 70-90% стоимости всего приложения в течении всей жизни проекта уходит на его поддержку.
Думая о легкой поддержке, невольно вспоминаем об объектном ориентировании и CFC. Давайте составим список тех классов, которые нам могут понадобиться:
- Coffee
- CoffeeWithCream
- CoffeeWithSoyMilk
- CoffeeWithCreamAndShotOfExpresso
- CoffeeWithSoyMilkAndShotOfExpresso
- CoffeewithCreamAndFlavorSyrup
- CoffeeWithCreamAndFlavorSyrupAndShotOfExpresso
- CoffeeWithSoyMilkAndShotOfExpressoAndFlavorSyrup
Посмотрев на наш неполный список, мы можем заметить, что что-то в нем не то. Если каждый раз у нас новые опции для напитка, то придется создавать классы для всех возможных комбинаций кофе с опциями. В результате у нас на руках неуправляемый монстр. Далее, все это нам придется повторить для чая и всех остальных напитков, которые мы можем предложить. Нам нужна идея по лучше. «Лучшие идеи» - это именно то, что предоставляют паттерны. Существует такой проверенный временем паттерн, который называется Decorator. Он позволяет легко справиться с объектами, у которых могут быть множество опций.
Чтобы понять, как работает паттерн Decorator, давайте сперва посмотрим на то, мы можем смоделировать кофе и чай, используя CFC. Поскольку эти два напитка в чем-то похожи, то выделим эти похожести в абстрактный суперкласс Beverage (см. Рис. 2)
Рисунок 2. Суперкласс Beverage
Код для Beverage.cfc:
Код для Coffee.cfc:
Большинство возможностей находится в классе Beverage. Классы Coffee и Tea имеют псевдо конструктор init(), который инициализирует объекты, создаваемые из этих классов.
Вспомним, что перед нами стоит проблема добавления опций в каждый тип напитка. Первый предложенный способ решения этой проблемы оказался просто кошмарным. А вот паттерн Decorator предлагает вполне гибкое и удобное решение. Decorator содержит в себе основной объект и является того же типа, что и содержащийся в нем объект. В нашем случае все декораторы будут наследовать класс Beverage. Затем, декоратор может быть использован везде, где нужно использовать основной объект, находящийся внутри декоратора. Поскольку декоратор имеет доступ к основному объекту, то, следовательно, он имеет доступ ко всем его не приватным методам.
Чтобы увидеть, как это работает, давайте рассмотрим случай, когда потребитель хочет добавить в свой кофе ванильный сироп (см. Рис. 3).
Рисунок 3. SyropDecorator
В Мире Объектов сначала нам понадобиться создать основной объект Coffee:
Далее мы можем декорировать beverage с помощью SyropDecorator:
Поскольку оригинальный напиток и декорированный напиток относятся к типу Beverage, то они оба будут иметь одни и те же типы данных и отвечать на методы getCost() и asString().
Скоро мы рассмотрим один очень важный метод, который принимает beverage, но сперва рассмотрим код для создания суперкласса Decorator.cfc и одного из его подклассов – SyropDecorator.cfc:
Код для Decorator.cfc:
Код для SyropDecorator.cfc:
Метод init() принимает два аргумента: baseObject (тип Beverage) и flavor (тип String), описывающий вкус сиропа. Этот метод init() вызывает метод setBaseObject() из суперкласса Decorator, таким образом, позволяя декоратору хранить напиток (beverage), переданный в него как переменную.
Но как это нам поможет? Давайте посмотрим на другой класс, который, несомненно, будет использоваться очень много раз – CashRegister:
Обратите внимание, что тут только один метод order(), который описывает и отображает потребителю заказанный напиток. Название напитка и его стоимость берутся из переданного в метод order() аргумента beverage.
Теперь, когда потребитель заказывает просто кофе (без опций), будет создан объект типа Coffee и передан в метод order() в классе CashRegister. Поскольку кофе является типом Beverage, то у него есть метод getCost(), а в Coffee CFC метод init() обозначает, что начальная цена за чашку кофе составляет всего лишь $2.55.
Но когда потребитель желает чашку кофе с добавленным в нее сиропом, то мы создадим объект Coffee, а затем передадим его в метод init() в классе SyropDecorator (где объект Coffee зарегистрирован как основной объект в SyropDecorator). Обратите внимание, что в SyropDecorator (а также во всех подклассах Decorator’а) метод getCost() отличается от метода getCost() в классе Beverage. Каждый подкласс Decorator’а сообщает свою стоимость в зависимости от стоимости его основного объекта (Coffee или Tea) плюс дополнительные опции. В случае с сиропов надбавка к начальной стоимости составит $0.55.
Теперь, когда у нас есть объект SyrupDecorator (с основным объектом Beverage внутри), мы можем направлять его в метод order() класса CashRegister. Это пример отсутствия связи между компонентами и интерфейсом, а не реализацией.
Итак, у нас все хорошо и замечательно, но что если потребитель захочет кофе с ванильным сиропом и экспрессо? Просто декорируйте декоратор. Другими словами, поскольку все декораторы содержат метод init(), который принимает основной объект типа Beverage, мы создадим SyrupDecorator, который декорирует объект Coffee, а затем декорируем SyrupDecorator с помощью ExpressoShotDecorator (см. Рис. 4).
Рисунок 4. ExpressoShotDecorator
Код для создания этого будет выглядеть так:
Конечно, мы можем продолжать декорировать декораторы до тех пор, пока основной объект не будет содержать все необходимые опции. Давайте посмотрим на UML диаграмму, наглядно показывающую нам дизайн классов (см. Рис. 5).
Рисунок 5. UML диаграмма
Обратите внимание на новый тип коннектора между классами Beverage и Decorator. Эта линия с иконкой алмаза (а не со стрелкой) обозначает, что класс Decorator содержит все свойства типа Beverage. Это переменная baseObject, которую он декорирует. Метод init() в каждом подклассе Decorator’a обозначает, что он принимает переменную baseObject типа Beverage. После того, как переменная baseObject определена в методе init(), каждый Decorator получит доступ к переменной baseObject в течение всего его жизненного цикла.
Вот код для других декораторов:
Код для SoyMilkDecorator.cfc:
Код для CreamDecorator.cfc:
Код для ExpressoShotDecorator.cfc:
Если вы ранее не были знакомы с паттерном Decorator, то скорее всего UML диаграмма не сильно вам поможет. Поэтому давайте разберемся с этим более детально. Мы видим, что класс Decorator наследует свойства класса Beverage. Это означает, что по правилам полиморфизма класс Decorator является родственником класса Beverage и может быть использован вместо Beverage тогда, когда мы запрашиваем Beverage. Когда нам может потребоваться вызвать объект Beverage? Давайте вспомним объект CashRegister с методом order() в нем показанный в коде для файла Order.cfm. Предназначением этого метода будет описывать то, что потребитель закал и предоставлять общую стоимость заказа. Вот код для CashRegister.cfc:
Класс CashRegister спрашивает каждый переданный в него объект Beverage о том, какое у него описание (используя метод asString()) и сколько он стоит (используя метод getCost()). Поскольку он принимает общий тип данных “Beverage”, то нет необходимости создавать множество разных методов, которые будут отражать все возможные комбинации основных напитков и опций.
Как было сказано выше, основной силой объектно-ориентированного программирования является его сокращение времени на поддержку кода. Вот что имелось в виду: если позже мы решим добавить опцию – возможно, Irish Cream, то все что нам понадобиться, это создать новую CFC IrishCreamDecorator и добавить новую опцию в наш список опций в меню. Никакой другой код не будет затронут. И наоборот, если однажды мы решим не предлагать какую-то опцию, то мы можем просто удалить CFC и удалить название этой опции из списка опций в меню. Никакой другой код не будет затронут.
Decorator’ы – это паттерны проектирования, предназначенные решать определенные проблемы, одной из которых является то, что основной объект может иметь множество дополнительных опций. Понимание, как использовать Decorator, даст вам возможность использовать правильный инструментарий в правильном месте.
Источник: Designer Coffee and the Decorator Pattern
© 2002-2005 г. Вадим Пушкарев