Паттерн Декоратор (Decorator; Wrapper, Обертка)

Назначение паттерна Decorator

Паттерн Decorator динамически добавляет новые обязанности объекту. Декораторы являются гибкой альтернативой порождению подклассов для расширения функциональности.

Рекурсивно декорирует основной объект.

Паттерн Decorator использует схему «обертываем подарок, кладем его в коробку, обертываем коробку».

Решаемая проблема

Иногда бывает нужно возложить дополнительные обязанности на отдельный объект, а не на класс в целом. Предположим, вы работаете над библиотекой для построения графических пользовательских интерфейсов и хотите иметь возможность добавлять в окно рамку и полосу прокрутки. Тогда можно определить иерархию наследования следующим образом (рис. 47).

Иерархия наследования при построении графического интерфейса

Рис. 47. Иерархия наследования при построении графического интерфейса

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

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

aBorderDecorator

aScrollDecorator

Пример добавления функций с помощью паттерна Декоратор

Рис. 48. Пример добавления функций с помощью паттерна Декоратор

aTextView

ствие прозрачно для клиентов компонента. Декоратор переадресует запросы внутреннему компоненту, но может выполнять и дополнительные действия (например, рисовать рамку) до или после переадресации (рис. 48). Поскольку декораторы прозрачны, они могут вкладываться друг в друга, добавляя тем самым любое число новых обязанностей.

Предположим, что имеется объект класса TextView, который отображает текст в окне. По умолчанию TextView не имеет полос прокрутки, поскольку они не всегда нужны. Но при необходимости их можно добавить с помощью Декоратора ScrollDecorator. Допустим, что еще мы хотим добавить рамку вокруг объекта TextView. Здесь может помочь Декоратор BorderDecorator. Мы просто компонуем оба Декоратора и получаем искомый результат.

Классы ScrollDecorator и BorderDecorator являются подклассами Decorator — абстрактного класса, который представляет визуальные компоненты, применяемые для оформления других визуальных компонентов. VisualComponent — это абстрактный класс для представления визуальных объектов. В нем определен интерфейс для рисования и обработки событий. Отметим, что класс Decorator просто переадресует запросы на рисование своему компоненту, а его подклассы могут расширять эту операцию.

Подклассы Decorator могут добавлять любые операции для обеспечения необходимой функциональности. Так, операция ScrollTo объекта ScrollDecorator позволяет другим объектам выполнять прокрутку, если им известно о присутствии объекта ScrollDecorator. Важная особенность этого паттерна состоит в том, что Декораторы могут употребляться везде, где возможно появление самого объекта VisualComponent. Поэтому клиент не может отличить декорированный объект от недекорированного, а значит, и никоим образом не зависит от наличия или отсутствия оформлений.

Применимость

Использование паттерна Decorator обоснованно:

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

Структура паттерна Decorator

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

При этом предполагается инкапсуляция исходного объекта в абстрактный интерфейс. Как объекты-декораторы, так и основной объект наследуют от этого абстрактного интерфейса. Интерфейс использует рекурсивную композицию для добавления к основному объекту неограниченного количества «слоев» — декораторов.

CoreFunctionality и метод doThis() выполняются всегда. Клиент может по желанию использовать декорирующие методы OptionalOne, OptionalTwo и OptionalThree. Каждый из этих классов переадресует запрос базовому классу Decorator, а тот направляет его в декорируемый объект (рис. 49).

UML-диаграмма классов паттерна Decorator

Рис. 49. UML-диаграмма классов паттерна Decorator

Участники

Interface (VisualComponent): определяет интерфейс для объектов, на которые могут быть динамически возложены дополнительные обязанности.

CoreFunctionality (TextView): определяет объект, на который возлагаются дополнительные обязанности.

Decorator — Декоратор: хранит ссылку на объект Component и определяет интерфейс, соответствующий интерфейсу Component.

OptionalOne, OptionalTwo... (BorderDecorator, ScrollDecorator) — конкретный Декоратор: возлагает дополнительные обязанности на компонент.

Отношения

Decorator переадресует запросы объекту Component. Может выполнять и дополнительные операции до и после переадресации.

Результаты

У паттерна Декоратор есть, по крайней мере, два плюса и два минуса:

  • большая гибкость, нежели у статического наследования. Декоратор может добавлять и удалять обязанности во время выполнения программы. Кроме того, применение нескольких декораторов к одному компоненту позволяет произвольным образом сочетать обязанности. Декораторы позволяют легко добавить одно и то же свойство дважды. Например, чтобы окружить объект Text View двойной рамкой, нужно просто добавить два Декоратора BorderDecorators. Двойное наследование классу Border в лучшем случае чревато ошибками;
  • позволяет избежать перегруженных функциями классов на верхних уровнях иерархии. Декоратор разрешает добавлять новые обязанности по мере необходимости. Вместо того чтобы пытаться поддержать все мыслимые возможности в одном сложном, допускающем разностороннюю настройку классе, вы можете определить простой класс и постепенно наращивать его функциональность с помощью Декораторов. Нетрудно также определять новые виды Декораторов независимо от классов, которые они расширяют, даже если первоначально такие расширения не планировались;
  • Декоратор и его компонент не идентичны. Декоратор действует как прозрачное обрамление. Но декорированный компонент все же не идентичен исходному. При использовании Декораторов это следует иметь в виду;
  • множество мелких объектов. При использовании в проекте паттерна Декоратор нередко получается система, составленная из большого числа мелких объектов, которые похожи друг на друга и различаются только способом взаимосвязи, а не классом и не значениями своих внутренних переменных. Отлаживать такую систему непросто.

Реализация

При применении паттерна Декоратор требуется рассмотреть несколько вопросов:

  • соответствие интерфейсов. Интерфейс Декоратора должен соответствовать интерфейсу декорируемого компонента. Поэтому классы ConcreteDecorator должны наследовать общему классу (по крайней мере, в C++);
  • отсутствие абстрактного класса Decorator. Нет необходимости определять абстрактный класс Decorator, если планируется добавить всего одну обязанность. Так часто происходит, когда вы работаете с уже существующей иерархией классов, а не проектируете новую. В таком случае ответственность за переадресацию запросов, которую обычно несет класс Decorator, можно возложить непосредственно на Concrete Decorator;
  • облегченные классы Component. Чтобы можно было гарантировать соответствие интерфейсов, Компоненты и Декораторы должны наследовать общему классу Component. Важно, чтобы этот класс определял интерфейс, а не хранил данные. В противном случае Декораторы могут стать весьма тяжеловесными, и применять их в большом количестве будет накладно. Включение большого числа функций в класс Component также не рекомендуется.

Пример паттерна Decorator

Паттерн Decorator динамически добавляет новые обязанности объекту. Украшения для новогодней елки являются примерами Декораторов. Огни, гирлянды, игрушки и т. д. вешают на елку для придания ей праздничного вида. Украшения не меняют саму елку, а только делают ее новогодней.

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

Использование паттерна Decorator

Подготовьте исходные данные: один основной компонент и несколько дополнительных (необязательных) «оберток».

Создайте общий для всех классов интерфейс по принципу «наименьшего общего знаменателя НОЗ» (lowest common denominator LCD). Этот интерфейс должен делать все классы взаимозаменяемыми.

Создайте базовый класс второго уровня (Decorator) для поддержки дополнительных декорирующих классов.

Основной класс и класс Decorator наследуют общий НОЗ-интер-фейс.

Класс Decorator использует отношение композиции. Указатель на НОЗ-объект инициализируется в конструкторе.

Класс Decorator делегирует выполнение операции НОЗ-объекту.

Для реализации каждой дополнительной функциональности создайте класс, производный от Decorator.

Подкласс Decorator реализует дополнительную функциональность и делегирует выполнение операции базовому классу Decorator.

Клиент несет ответственность за конфигурирование системы: устанавливает типы и последовательность использования основного объекта и Декораторов.

Реализация паттерна Decorator Паттерн Decorator: до и после

До

Используется следующая иерархия наследования:

class А { public:

virtual void do_it() { cout << 'A';

}

};

class AwithX: public A { public:

/*virtual*/ void do_it() { A::do_it(); do_X();

};

private: void do_X() { cout << 'X';

}

};

class AwithY: public A { public:

/*virtual*/ void do_it() { A::do_it(); do_Y();

}

protected: void do_Y() { cout << 'Y';

}

};

class AwithZ: public A { public:

/““virtual*/ void do_it() { A::do_it(); do_Z();

}

protected: void do_Z() { co?t << 'Z';

}

dass AwithXY: public AwithX, public AwithY

{

public:

/*virtual*/ void do_it() {

AwithX: :do_it();

AwithY: :do_Y();

}

};

dass AwithXYZ: public AwithX, public AwithY, public AwithZ

{

public:

/*virtual*/ void do_it() {

AwithX::do_it();

AwithY: :do_Y();

AwithZ::do_Z();

}

};

int main() {

AwithX anX; AwithXY anXY; AwithXYZ anXYZ; anX.do_it(); co?t << ' '; anXY.do_it(); co?t << ' '; anXYZ.do_it(); co?t << ' ';

}

Вывод программы:

АХ

AXY

AXYZ

После

Заменим наследование делегированием.

При использовании агрегирования вместо наследования для декорирования «основного» объекта клиент сможет динамически добавлять новые обязанности объектам, в то время как архитектура, основанная на множественном наследовании, является статичной.

class I { public: virtual ~I(){} virtual void do_it() = 0;

};

class A: public I { public:

~A() {

cout << "A dtor" << ' ';

}

/““virtual*/ void do_it() { cout << A';

}

class D: public I { public:

D(I *inner) { m_wrappee = inner;

}

~D() {

delete m_wrappee;

}

/*virtual*/ void do_it() { m_wrappee->do_it();

}

private:

I *m_wrappee;

};

class X: public D { public:

X(I *core): D(core){} ~X(){

сои! << "X скот" <<

}

/*у1г!иа1*/ уоМ с!о_к() { 0::с1о_к(); сои! << 'X';

}

с1а$8 У: риЬПс Э { риЬНс:

У(1 *соге): 0(соге){}

~У(){

СОШ « "У ског" « " "

}

/*у1г!иа1*/ уЫс! с1о_к() {

0::с1о_к(); сои! << 'У;

}

с1а88 Ъ. риЬПс Э { риЬНс:

Z(I *соге): 0(соге){}

~го{

сои! << "2 скот" <<

}

/*у1г!иа1*/ уоШ с1о_к() { 0::ёо_к(); сои! <<

}

};

т! тат() {

I *апХ = пеу X(new А);

I *апХУ = new Y(new Х(пеу А));

I *апХУ2 = пеу Z(new Y(new X(new А)));

апХ->с1о_к();

сои! << 'п';

апХУ->с!о_к();

сои! << 'п';

апХУ2->с1о_к();

сои! << 'п';

delete апХ; delete anXY; delete anXYZ;

}

Вывод программы:

АХ

AXY

AXYZ

Xdtor Adtor Y dtor X dtor A dtor Z dtor Y dtor X dtor A dtor

Паттерн проектирования Decorator no шагам

Создайте «наименьший общий знаменатель», делающий классы взаимозаменяемыми.

Создайте базовый класс второго уровня для реализации дополнительной функциональности.

Основной класс и класс-декоратор используют отношение «является».

Класс-декоратор «имеет» экземпляр «наименьшего общего знаменателя».

Класс Decorator делегирует выполнение операции объекту «имеет». Для реализации каждой дополнительной функциональности создайте подклассы Decorator.

Подклассы Decorator делегируют выполнение операции базовому классу и реализуют дополнительную функциональность.

Клиент несет ответственность за конфигурирование нужной функциональности.

#include using namespace std;

// 1." Наименьший общий знаменатель" class Widget

{

public:

virtual void draw() = 0;

};

// 3. Основной класс, использующий отношение "является" class TextField: public Widget {

int width, height; public:

TextField(int w, int h)

{

width = w; height = h;

}

/““virtual*/ void draw()

{

cout << "TextField:" << width << "," << height << ' ';

}

};

// 2. Базовый класс второго уровня

class Decorator: public Widget // 3. использует отношение "является"

{

Widget *wid; // 4. отношение "имеет" public:

Decorator(Widget *w)

{

wid = w;

}

/*virtual*/ void draw()

{

wid->draw(); // 5. делегирование

}

};

// 6. Дополнительное декорирование class BorderDecorator: public Decorator {

public:

BorderDecorator(Widget *w): Decorator(w){}

/*virtual*/ void draw()

{

// 7. Делегирование базовому классу и Decorator::draw();

// 7. Реализация дополнительной функциональности cout << " BorderDecorator" << ' ';

}

};

// 6. Дополнительное декорирование class Scroll Decorator: public Decorator {

public:

Scroll Decorator(Widget *w): Decorator(w){}

/*virtual*/ void draw()

{

// 7. Delegate to base class and add extra stuff

Decorator::draw();

cout << " Scroll Decorator" << ' ';

}

};

int main()

{

// 8. Клиент ответствен за конфигурирование нужной // функциональности Widget *aWidget = new BorderDecorator( new BorderDecorator( new ScrollDecorator (new TextField(80, 24)))); aWidget->draw();

}

TextField: 80, 24 ScrollDecorator BorderDecorator BorderDecorator

Особенности паттерна Decorator

  • 1. Adapter придает своему объекту новый интерфейс, Proxy предоставляет тот же интерфейс, a Decorator обеспечивает расширенный интерфейс.
  • 2. Adapter изменяет интерфейс объекта. Decorator расширяет ответственность объекта. Decorator, таким образом, более прозрачен для клиента. Как следствие, Decorator поддерживает рекурсивную композицию, что невозможно с чистыми Адаптерами.
  • 3. Decorator можно рассматривать как вырожденный случай Composite с единственным компонентом. Однако Decorator добавляет новые обязанности и не предназначен для агрегирования объектов.
  • 4. Decorator позволяет добавлять новые функции к объектам без наследования. Composite фокусирует внимание на представлении, а не декорировании. Эти характеристики являются различными, но взаимодополняющими, поэтому Composite и Decorator часто используются вместе.
  • 5. Decorator и Рюху имеют разное назначение, но схожие структуры. Их реализации хранят ссылку на объект, которому они отправляют запросы.
  • 6. Decorator позволяет изменить внешний облик объекта, Strategy — его внутреннее содержание.

Родственные паттерны

Адаптер: если Декоратор изменяет только обязанности объекта, но не его интерфейс, то Адаптер придает объекту совершенно новый интерфейс.

Компоновщик: Декоратор можно считать вырожденным случаем составного объекта, у которого есть только один компонент. Однако Декоратор добавляет новые обязанности, агрегирование объектов не является его целью.

Стратегия: Декоратор позволяет изменить внешний облик объекта, стратегия — его внутреннее содержание. Это два взаимодополняющих способа изменения объекта.

 
< Пред   СОДЕРЖАНИЕ     След >