Andrey on .NET | Структурные шаблоны: Декоратор (Decorator)

Структурные шаблоны: Декоратор (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);
    }
}

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

Комментарии (15) -

@ Дмитрий: а не пробовали сделать базовый Декоратор через DynamicObject? Я подумал, выходит так: разобрать исходный объект на методы и свойства можно, добавить их в DynamicObject тоже. А вот привести DynamicObject к интерфейсу похоже не возможно. Или я ошибаюсь?

Это возможно только если генерировать динамический прокси:
www.fsmpi.uni-bayreuth.de/.../202.html

Интересно, спасибо за ссылку.

Андрей, скажите, корректно ли декорировать объекты базы данных? Я разрабатываю проект, в котором модель данных заранее реализовать сложно. Есть необходимость добавления новых сущностей, а также свойств сущностям базы. Планирую использовать .NET FWork4.0(C#)+ORM(Fluent NHibernate)+РСУБД(Oracle). В моем случае необходимо создавать динамич. классы объектов, создаваемых NHibernate. Корректна ли такая постановка задачи?

Начнем с того, почему вы считаете это некорректным в частности для свойств? (хотя может даже начать с того, почему сами сущности должны добавляться)

Я просто пока сомневаюсь в правильности своих предположений. Идея вот какая: необходим проект на подобие внутренней соцсети учебного заведения. Сначала строим модель для учета успеваемости студентов. Завтра новая задача - учет научных результатов - патенты, публикации личные и соавторстве... Потом подтянутся идеи по автоматизации учета рацпредложений и все остальные как я предвижу. Хотелось бы сделать по возможности универсальную модель данных. Со свойствами - тут реализация возможна без проблем. А насчет сущностей - это новый метод. Так как такой реализации в чистом виде я не встречал, поэтому мне важен ваш ответ в части концепции - правильна ли она в принципе или я могу столкнуться с какими-то проблемами? На мой взгляд - с декораторм все должно получиться. Может, у Вас есть альтернативные варианты, подскажите, пожалуйста. Можно ссылки. Одно хотел подчеркнуть - модель данных не должна быть типа EAV- админы СУБД на нее не согласны.

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

Что касается декорирования объектов БД, я не вижу особых причин мешающих это сделать. Кстати, первое похожее что вспомнилось – Entity Frameworks. Его объекты предоставляют свойства, которых не существует в связанной с ними таблице. Но разумеется могут возникнуть какие-то вопросы уже с конкретными используемыми библиотеками.

Анрей, спасибо большое! Теперь я спокоен насчет инструментария, т.к. долго не мог определиться.

Татьяна 14.04.2012 1:21:29

Я не очень поняла Базовый декоратор и Декоратор это *.dll, а другие два в одном проекте с расширением *.cs?

@ Татьяна: Начнем с того, что переформулируйте вопрос.

Виолетта 26.10.2012 8:38:13

Приведите, пожалуйста, примеры использования шаблона Декоратор в базовых классах (BCL). Заранее спасибо!

@ Виолетта: BufferedStream, CryptoStream.

Виолетта 26.10.2012 13:29:11

А в WPF?@ Andrey:

@ Виолетта: Не поверите - Decorator
msdn.microsoft.com/.../ms753412(v=vs.85).aspx

Добавить комментарий