Название шаблона
Декоратор (Decorator / Wrapper).
Тип
Структурный шаблон проектирования (Structural).
Описание
Декоратор предназначен для динамического добавления объекту новой функциональности. Является гибкой альтернативой механизму наследования, в том числе и множественного.
Данный шаблон используется если необходимо:
- динамически и прозрачно для клиента изменять функциональность объекта;
- (или) реализовать небольшую функциональность, которая в дальнейшем может быть исключена;
- (или) уменьшить число классов, получающихся в результате использования наследования;
- (или) добавить функциональность классу, от который невозможно наследоваться;
- (или) реализовать аналог множественного наследования, в языках его не поддерживающих.
Идея шаблона заключена в следующем: Декоратор является оберткой над исходным компонентом. Он реализует тот же самый интерфейс, поэтому может замещать этот компонент. Однако, цель шаблона не просто в переадресации запросов. Он добавляет свой код до и/или после вызовов исходных методов, в крайнем случае замещая их полностью. Это приводит к изменению исходного поведения и появлению новых возможностей.
Из этого описания можно сделать следующие выводы:
- Декоратор прозрачен для использования, т.к. клиент не заметит подмены компонента за счет реализации одного и того же интерфейса с ним.
- Добавление новой функциональности осуществляется подменой экземпляра оригинального компонента. Исключить ее так же легко – нужно использовать оригинальный объект. Причем его не обязательно пересоздавать и можно извлечь из Декоратора.
- Поскольку шаблон реализует интерфейс исходного компонента, то ничего не мешает вкладывать один Декоратор в другой, создавая их цепочки. Данный подход является альтернативой множественному наследованию.
- В шаблоне нет ограничения на добавления новых свойств и методов. Такой объект может использоваться как вместо декорируемого компонента, так и самостоятельно.
- Декоратор способен работать как с самим исходным компонентом, так и его наследниками.
При разработке нескольких Декораторов для одного компонента целесообразно создать базовый Декоратор. Он предоставляет механизм подключения компонента и обеспечивает переадресацию всех методов. В дальнейшем, используя наследование, можно создавать классы конкретных Декораторов реализуя в них только методы и свойства с изменениями функциональности.
Таким образом, можно выделить следующих участников шаблона Декоратор:
- Общий интерфейс (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), используя наследование от базового;
- в клиентском коде используем Декоратор вместо конкретного компонента;
- при необходимости создаем цепочки Декораторов, передавая один из них в другой. Это позволяет добавить несколько новых возможностей компоненту;
- если функции декоратора становятся не нужны, то используем вновь исходный объект.
Пример реализации
Разработаем Декораторы для уже упомянутых элементов блок-схемы. Чтобы не загромождать исходный код, уберем большую часть свойств (координаты, размер элемента и т.д.). Оставим только текст элемента и метод его отображения. Кроме того, для упрощения заменим рисование выводом на консоль. В итоге, интерфейс компонента и его реализация будут такими:
public interface IElement
{
string Text { get; set; }
void Draw();
}
public class Element : IElement
{
public string Text { get; set; }
public void Draw()
{
Console.WriteLine("Element text = {0}", this.Text);
}
}
Следующий шаг – создание базового Декоратора и реализация работы с исходным компонентом и переадресации обращений.
public class ElementDecoratorBase : IElement
{
protected readonly IElement _component;
public ElementDecoratorBase(IElement component)
{
this._component = component;
}
public virtual string Text
{
get { return this._component.Text; }
set { this._component.Text = value; }
}
public virtual void Draw()
{
this._component.Draw();
}
}
Все готово для создания Декораторов. Их будет два – зачеркивание элемента и отображение фона под ним (у исходного компонента фон прозрачный). Обратите внимание, что момент вызова метода компонента меняется в зависимости от решаемой задачи. Кроме того, в ElementBgndDecorator появилось новое свойство для задания фонового изображения.
public class ElementStrikedDecorator : ElementDecoratorBase
{
public ElementStrikedDecorator(IElement component) : base(component) { }
public override void Draw()
{
this._component.Draw();
this.Strike();
}
private void Strike ()
{
Console.WriteLine("Striked");
}
}
public class ElementBgndDecorator : ElementDecoratorBase
{
public Bitmap Background { get; set; }
public ElementBgndDecorator(IElement component) : base(component) { }
public override void Draw()
{
this.SetBackground();
this._component.Draw();
}
private void SetBackground()
{
Console.WriteLine("Background");
}
}
Все готово к использованию. Создадим вначале исходный компонент, а потом дополним его возможности, используя оба Декоратора. При этом демонстрационному методу DrawElement() безразлично, какой экземпляр использовать в работе.
public static class DecoratorDemo
{
public static void DrawElement (IElement element)
{
element.Draw();
Console.WriteLine("---------------------------");
}
public static void Execute ()
{
Element element = new Element();
DecoratorDemo.DrawElement(element);
ElementBgndDecorator elementBgnd = new ElementBgndDecorator(element);
elementBgnd.Background = new Bitmap(10, 10);
ElementStrikedDecorator elementStriked = new ElementStrikedDecorator(elementBgnd);
elementStriked.Text = "Demo";
DecoratorDemo.DrawElement(elementStriked);
}
}
В следующей части посмотрим на варианты использования шаблона.