Design Pattern Decorator

Примеры реализации

Автор: Михаил Новиков
Источник: RSDN Magazine #3-2005
Опубликовано: 08.10.2005
Версия текста: 1.0
Decorator
Event Decorator
Template Decorator
Generic Decorator
Заключение

Decorator

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


Диаграмма классов Decorator

В общем случае паттерн состоит из 4 частей

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

IWidget widget  = new DecoratorA(new DecoratorB(new Widget()));
Widget.Draw();

В данном случае при вызове метода Draw() сначала вызовется метод Draw() у объекта DecoratorA, далее DecoratorB, а потом у самого виджета. Ничто не мешает прервать цепочку вызовов, то есть один из декораторов может принять решения не вызывать соответствующий метод декорируемого им объекта.

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


Графическое представление паттерна Декоратор

Классическая реализация патера Декоратор
      public
      interface IWidget 
{
    void Draw();
}

publicabstractclass Decorator : IWidget 
{
    public Decorator() 
{
    }

    public Decorator(IWidget child) 
{
        Child = child;
    }

    private IWidget child;

    protected IWidget Child 
{
        get { return child; }
        set { child = value; }
    }

    publicvirtualvoid Draw() 
{
        if (Child != null) 
{
            Child.Draw();
           }
    }
}

publicclass DecoratorA : Decorator 
{
    public DecoratorA() 
{
    }

    public DecoratorA(IWidget child) : base(child) 
{
    }

    publicoverridevoid Draw() 
{
        Console.WriteLine("DecoratorA");
        base.Draw();
    }
}

publicclass DecoratorB : Decorator 
{
    public DecoratorB() 
{
    }

    public DecoratorB(IWidget child) : base(child) 
{
    }

    publicoverridevoid Draw() 
{
        Console.WriteLine("DecoratorB");
        base.Draw();
    }
}

publicclass Widget : IWidget 
{
    public Widget() 
{
    }

    publicvoid Draw() 
{
        Console.WriteLine("Widget");
    }
}

Примеры применения шаблона проектирования Decorator можно найти в .NET Framework и Java API. Самым наглядным примером является реализация работы с потоками

      byte[] data = ….;
ByteInputStream byteStream = new ByteInputStream(data);
DataInputStream dataStream = new DataInpuStream(byteStream);

Здесь создается поток байтов и далее он декорируется DataInputStream. Стоит отметить, что и DataInputStream и ByteInputStream наследуются от класса Stream, то есть имеют общий интерфейс. Так же возможны случаи с FileStream, StringWriter и прочими классами.

При работе с XML в .NET в пространстве имен System.Xml существует класс XmlTextReader, который осуществляет быстрое некэшируемое чтение XML документа как из потока (SAX – Simple API for XML). Для того чтобы проверить соответствует ли XML файл XSD схеме, применим XmlValidatingReader

XmlTextReader textReader = new XmlTextReader(fileName);
XmlValidatingReader validReader = new XmlValidatingReader(textReader);

Здесь аналогичная ситуация: декорируется объект XmlTextReader. Заметим, что и декоратор и сам декорируемый класс потомки XmlReader и соответственно часть интерфейса одинакова.

Еще одним полезным свойствам паттерна является его возможность эмулировать наследование от sealed классов, то есть классов, наследование от которых запрещено.

      public
      interface Stream 
{
  byte[] Read();
}

publicsealedclass ZipStream : Stream 
{
 publicbyte[] Read() 
{   
   …
   return …;
 }
}

publicclass BufferedStream  : Stream 
{
 public BufferedStream(Stream stream) 
{
  this.stream = stream;
 }
 
 private Stream stream;
 
 publicbyte[] Read() 
{
   byte[] data = stream.Read();
   ….
   return  …;
 }
}

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

В качестве схожих паттернов выделяются Proxy (Заместитель), Адаптер (Adapter) и Компоновщик (Composite). Заместитель аналогично декоратору делегирует вызовы методов, однако сам объект, в частности, может пока не существовать вовсе или находиться на удаленной системе. Адаптер можно рассматривать как частный случай декоратора, при котором интерфейсы декоратора и декорируемого объекта различны. Сам паттерн Декоратор является частным случаем Компоновщика, содержащего один объект, такой вывод можно сделать, сравнив UML схемы (не совсем верно, так как оба паттерна предназначены для разных целей, что говорит о том, что шаблон проектирования нечто большее, чем просто UML схема). Все перечисленные шаблоны является структурными, как и сам Декоратор.

Event Decorator

Одним из разновидностей паттерна Декоратор можно считать Декоратор Событий. Усовершенствование заключается в применении шаблона проектирования Publisher – Subscriber (Издатель - Подписчик). То есть помимо того, что декоратор делегирует вызов метода декорируемому объекту, он еще берет функции издателя, то есть оповещает всех подписчиков о том, что был вызван метод. Так же подписчикам может быть передана любая другая полезная информация, например, с какими параметрами был вызван метод.

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


Диаграмма классов Event Decorator

Реализация паттерна Декоратор Событий
      public
      interface IWidget
{
    void Run();
    void Draw();
}

publicsealedclass Widget : IWidget
{
    publicvoid Run()
    {
        Console.WriteLine("Widget.Run()");
    }
    publicvoid Draw()
    {
        Console.WriteLine("Widget.Draw()");
    }
}

publicdelegatevoid WidgetHandler();

publicclass EventDecorator : IWidget
{
    private IWidget widget;

    public EventDecorator(IWidget widget)
    {
        this.widget = widget;
    }

    publicevent WidgetHandler Run;

    publicevent WidgetHandler Draw;

    void IWidget.Run()
    {
        widget.Run();
        if (this.Run != null)
        {
            this.Run();
        }
    }

    void IWidget.Draw()
    {
        widget.Draw();
        if (this.Draw != null)
        {
            this.Draw();
        }
    }
}

Рассмотрим пример использования паттерна

      class Program
{
    staticvoid Draw()
    {
        Console.WriteLine("Program.Draw()");
    }

    staticvoid Run()
    {
        Console.WriteLine("Program.Run()");
    }

    staticvoid Main(string[] args)
    {
        EventDecorator decorator = new EventDecorator(new Widget());
        decorator.Draw += new WidgetHandler(Draw);
        decorator.Run += new WidgetHandler(Run);

        IWidget widget = (IWidget) decorator;
        widget.Draw();
        widget.Run();
    }
}

При вызове методов widget Draw и Run, будут оповещены об этом статические методы класса Program.

Template Decorator

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


Диаграмма классов Template Decorator

Реализация паттерна Шаблонный Декоратор
      class IWidget 
{
public:
    virtualvoid Draw() = 0;
};

class Widget : public IWidget 
{
public:
    virtualvoid Draw() 
    {
        cout << "Widget" << endl;
    }
};

template <typename Type>
class DecoratorA : public Type 
{
public:
    virtualvoid Draw() 
    {
        cout << "DecoratorA" << endl;
        Type::Draw();
    }
};

template <typename Type>
class DecoratorB : public Type 
{
public:
    virtualvoid Draw() 
    {
        cout << "DecoratorB" << endl;
        Type::Draw();
    }
};

Составим цепочку наследования: класс DecoratorB наследуется от класса DecoratorA, а тот в свою очередь от Widget. Тем самым происходит декорирование класса Widget во время компиляции с помощью наследования.

DecoratorB<DecoratorA<Widget> >* decorator = new DecoratorB<DecoratorA<Widget> >();
IWidget* widget = dynamic_cast<IWidget*>(decorator);
widget->Draw();
delete decorator;

При вызове метода Draw() в начале будет вызван метод класса DecoratorA, далее DecoratorB и в конце Widget. Так же декоратор остался полностью прозрачен, так как возможно приведение к интерфейсу IWidget, однако сам класс декоратора явно не реализует этот интерфейс.

Возможно, сделать таким же образом не одиночное наследование, то есть, как представлено по цепочке, а множественное, формируя при этом более сложную иерархию классов. Это можно достигнуть благодаря виртуальному наследованию. В данном случае все декораторы должны виртуально наследовать Type.

ПРИМЕЧАНИЕ

Декораторы не должны всегда виртуально наследоваться от Type. В данном случае это необходимо, так как возможна комбинация DecoratorMultiple<DecoratorA<Widget>, DecoratorB<Widget> >, то есть наследование ромбом.

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

      template <typename BaseLeft, typename BaseRight>
class DecoratorMultiple : publicvirtual BaseLeft, publicvirtual BaseRight
{
public:
    virtualvoid Draw()
    {
        cout << "DecoratorMultiple" << endl;
        BaseLeft::Draw();
        BaseRight::Draw();
    }
};

В качестве примера приведем следующую структуру: DecoratorA наследуется от Widget, DecoratorB от Gidget (другой класс, реализующий IWidget), и в конце DecoratorMultiple наследуетcя от получившихся классов.

DecoratorMultiple<DecoratorA<Gidget>, DecoratorB<Widget> >* decorator = new DecoratorMultiple<DecoratorA<Gidget>, DecoratorB<Widget> >();
IWidget* widget = dynamic_cast<IWidget*>(decorator);
a->Draw();
delete decorator;

Однако в данном случае исчезает прозрачность декоратора, так как при попытке приведения к интерфейсу IWidget компилятор выдает warning, помимо этого происходит ошибка в run time, так как приведение произвести не удалось и dynamic_cast вернет 0. Для решения этой проблемы необходимо указать, что Widget и Gidget виртуально наследуются от IWidget, то есть использовать модификатор virtual.

ПРИМЕЧАНИЕ

Compiler Warning (level 1) C4540

Dynamic_cast used to convert to inaccessible or ambiguous base; run-time test will fail ('type1' to 'type2')

(VC++ 7.1, VC++ 2005 Beta 2).

Интерес представляет реализация данного приема на C++/CLI, однако сразу следует отметить, что множественное наследование не поддерживается.

Generic Decorator

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


Диаграмма классов Generic Decorator

Реализация паттерна Обобщенный Декоратор
      public
      interface IWidget
{
    void Draw();
}

publicabstractclass Decorator<Type> : IWidget
    where Type : IWidget, new()
{
    public Decorator()
    {
        Child = new Type();
    }

    private Type child;

    public Type Child
    {
        get { return child; }
        set { child = value; }
    }

    publicvirtualvoid Draw()
    {
        if (Child != null)
        {
            Child.Draw();
        }
    }
}

publicclass DecoratorA<Type> : Decorator<Type>
    where Type : IWidget, new()
{
    publicoverridevoid Draw()
    {
        Console.WriteLine("DecoratorA");
        base.Draw();
    }
}

publicclass DecoratorB<Type> : Decorator<Type>
    where Type : IWidget, new()
{
    publicoverridevoid Draw()
    {
        Console.WriteLine("DecoratorB");
        base.Draw();
    }
}

publicclass Widget : IWidget
{
    publicvoid Draw()
    {
        Console.WriteLine("Widget");
    }
}

Важно отметить, что базовый класс Decorator выполняет функции коммуникации между декорируемым объектом и декоратором. Сам объект декоратор может являться декорируемым объектом для другого декоратора, за счет этого достигается вложенность. Следует обратить внимание, на то, что в Decorator метод Draw() объявлен как виртуальный, то есть его переопределяет каждый конкрентный порожденный класс DecoratorA и DecoratorB.

DecoratorB<DecoratorA<Widget>> decorator = new DecoratorB<DecoratorA<Widget>>();
IWidget widget = (IWidget)decorator;
widget.Draw();

В приведенном примере, объект Widget содержится и создается в классе DecoratorA, объект которого в свою очередь содержится в объекте DecoratorB. Однако ни DecoratorA, ни DecoratorB не знает, какой именно объект он содержит за счет интерфейса IWidget. Так же достингнута прозрачность, так как возможно приведение к интерфесу IWidget. При вызове метода Draw() вначале он будет вызван у DecoratorB, потом у DecoratorA и в конце у Widget.

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

DecoratorC<DecoratorA<DecoratorB<Widget>>, Gidget> multiple = new DecoratorC<DecoratorA<DecoratorB<Widget>>, Gidget>();
IWidget widget = (IWidget)multiple;         
widget.Draw();

Реализацию можно найти в придагаемых к статье исходниках. Следует отметить, что данный паттерн не составляет труда реализовать на Java с использованием генериков.

Заключение

Получить новые виды паттернов можно, экпериментируя с ними и с новыми средствами языков программирования. В частности такой подход был продемонстрирован на генериках и шаблонах. Объединение паттернов также дает результаты. Это показывает пример с событиями (Publisher - Subscriber).

Шаблон проектирования Filter (Фильтр) так же является разновидностью паттерна Декоратор. Фильтр предназначен для обработки данных потока. Подробное описание этого паттерна можно найти в книге Mark Grand «Patterns in Java. Second Edition». В этой книге Фильтр классифицирован как разделяющий паттерн проектирования, а Декоратор как структурный.

Так же среди книг выделяется «Applied Java Patterns» Stephen Stelting, Olav Maassen. Книгой, в которой дано подробное описание паттерна Декоратор является GoF («Design Patterns. Elements of Reusable Object-Oriented Software» E. Gamma и другие).


Эта статья опубликована в журнале RSDN Magazine #3-2005. Информацию о журнале можно найти здесь