Декоратор предназначен для динамического добавления объекту новой функциональности. Является гибкой альтернативой механизму наследования, в том числе и множественного.
Данный шаблон используется если необходимо:
- динамически и прозрачно для клиента изменять функциональность объекта;
- (или) реализовать небольшую функциональность, которая в дальнейшем может быть исключена;
- (или) уменьшить число классов, получающихся в результате использования наследования;
- (или) добавить функциональность классу, от который невозможно наследоваться;
- (или) реализовать аналог множественного наследования, в языках его не поддерживающих.
Идея шаблона заключена в следующем: Декоратор является оберткой над исходным компонентом. Он реализует тот же самый интерфейс, поэтому может замещать этот компонент. Однако, цель шаблона не просто в переадресации запросов. Он добавляет свой код до и/или после вызовов исходных методов, в крайнем случае замещая их полностью. Это приводит к изменению исходного поведения и появлению новых возможностей.
Из этого описания можно сделать следующие выводы:
- Декоратор прозрачен для использования, т.к. клиент не заметит подмены компонента за счет реализации одного и того же интерфейса с ним.
- Добавление новой функциональности осуществляется подменой экземпляра оригинального компонента. Исключить ее так же легко – нужно использовать оригинальный объект. Причем его не обязательно пересоздавать и можно извлечь из Декоратора.
- Поскольку шаблон реализует интерфейс исходного компонента, то ничего не мешает вкладывать один Декоратор в другой, создавая их цепочки. Данный подход является альтернативой множественному наследованию.
- В шаблоне нет ограничения на добавления новых свойств и методов. Такой объект может использоваться как вместо декорируемого компонента, так и самостоятельно.
- Декоратор способен работать как с самим исходным компонентом, так и его наследниками.
При разработке нескольких Декораторов для одного компонента целесообразно создать базовый Декоратор. Он предоставляет механизм подключения компонента и обеспечивает переадресацию всех методов. В дальнейшем, используя наследование, можно создавать классы конкретных Декораторов реализуя в них только методы и свойства с изменениями функциональности.
Таким образом, можно выделить следующих участников шаблона Декоратор:
- Общий интерфейс (IComponent) – определяет интерфейс объектов и Декоратора;
- Конкретный компонент (Concrete component) – компонент(ы), реализующие общий интерфейс, функциональность которых необходимо модифицировать.
- Декоратор (Decorator) – базовый класс для декораторов, как правило включает механизм хранения компонента и реализует простую переадресацию.
- Конкретный декоратор (Concrete decorator) – реализация Декоратора, добавляющая определенные функции компоненту.
Особенности использования
Важно отметить, что при использовании шаблона необходимо обращать внимание на декорируемые компоненты. Использование большого количества Декораторов с тяжеловесными классами может повлечь большие накладные расходы.
Интересной чертой Декоратора является возможность уменьшения числа создаваемых классов по сравнению с результатами использования наследования. Это прямое следствие возможности вкладывать один Декоратор в другой.
Пример уменьшения числа классов
Предположим, необходимо разработать класс Element, реализующий элемент блок-схемы. Есть четыре стиля его отображения, которые надо комбинировать: по-умолчанию, инвертированный (inverted), зачеркнутый (striked), выделенный (highlighted). Кроме того, необходимо в дальнейшем легко добавлять новые стили и их комбинации.
При использовании наследования получим 8 классов: от Element, ElementInverted, ElementStriked, ElementHighlighted, ElementInvertedStriked и до ElementInvertedStrikedHighlighted. Добавление еще одного стиля, например рамки (ElementBorder), увеличит число классов до 16. Назвать это удобным, простым и надежным решением навряд ли можно.
Использование шаблона Декоратор ограничит число создаваемых классов до 4: основной Element и 3 Декоратора (ElementInvert, ElementStrike, ElementHighlight). В дальнейшем возможно их вкладывать один в другой для достижения нужного результата. Кроме того, такой подход более гибкий в работе. Например, можно сначала инвертировать, а потом подсветить или наоборот. Да и поддерживать такой код проще.
Отличия в применении Декоратора и механизма наследования:
Применение шаблона обеспечивает:
- более гибкий механизм, который позволяет комбинировать классы в процессе выполнения программы;
- возможность дважды применить один и тот же Декоратор (например, для рисования двойной рамки вокруг элемента можно дважды применить ElementBorder с разным отступом);
- изменение функциональности не только применением самих Декораторов, но и различным порядком их вложенности (например, результат операции "инвертировать цвет + зачеркнуть" будет отличаться от результата "зачеркнуть + инвертировать цвет");
- меньшую зависимость, за счет использования только открытых (public) методов и свойства декорируемого объекта (но это может быть и ограничением использования шаблона).
Наследование, не обеспечивая возможностей указанных, позволяет обращаться к защищенным (protected) областям базового класса. Кроме того, нет необходимости создавать механизм переадресации.
Реализация шаблона в общем виде
- определяем общий интерфейс (IComponent) и его реализации;
- разрабатываем базовый Декоратор (DecoratorBase), реализующий общий интерфейс (IComponent):создаем механизм подключения и хранения компонента;
реализуем переадресацию всех методов и свойств; - создаем конкретные Декораторы (Decorator), используя наследование от базового;
- в клиентском коде используем Декоратор вместо конкретного компонента;
- при необходимости создаем цепочки Декораторов, передавая один из них в другой. Это позволяет добавить несколько новых возможностей компоненту;
- если функции декоратора становятся не нужны, то используем вновь исходный объект.
Пример реализации
На примере формирования данных для передачи по каналу связи рассмотрим применение данного паттерна.
Создадим интерфейс IProcessor
Создадим класс, реализующий интерфейс.
Создадим декоратор ввиде абстрактного класса Shell.
Далее создадим класс HammingCoder, наследуемый от абстрактного Shell, данный класс будет представлять из себя устройство по наложению помехоустойчивого кода Хэминга на передаваемые данные.
По аналогии создадим класс Encryptor данный класс будет выступать в качестве устройства шифрования переданных данных.
Посмотрим применение