Структурные шаблоны: Декоратор (Decorator)

Название шаблона

Декоратор (Decorator / Wrapper).

Тип

Структурный шаблон проектирования (Structural).

Описание

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

Данный шаблон используется если необходимо:

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

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

Из этого описания можно сделать следующие выводы:

  1. Декоратор прозрачен для использования, т.к. клиент не заметит подмены компонента за счет реализации одного и того же интерфейса с ним.
  2. Добавление новой функциональности осуществляется подменой экземпляра оригинального компонента. Исключить ее так же легко – нужно использовать оригинальный объект. Причем его не обязательно пересоздавать и можно извлечь из Декоратора.
  3. Поскольку шаблон реализует интерфейс исходного компонента, то ничего не мешает вкладывать один Декоратор в другой, создавая их цепочки. Данный подход является альтернативой множественному наследованию.
  4. В шаблоне нет ограничения на добавления новых свойств и методов. Такой объект может использоваться как вместо декорируемого компонента, так и самостоятельно.
  5. Декоратор способен работать как с самим исходным компонентом, так и его наследниками.

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

Таким образом, можно выделить следующих участников шаблона Декоратор:

  1. Общий интерфейс (IComponent) – определяет интерфейс объектов и Декоратора;
  2. Конкретный компонент (Concrete component) – компонент(ы), реализующие общий интерфейс, функциональность которых необходимо модифицировать.
  3. Декоратор (Decorator) – базовый класс для декораторов, как правило включает механизм хранения компонента и реализует простую переадресацию.
  4. Конкретный декоратор (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);
    }
}

В следующей части посмотрим на варианты использования шаблона.