Декоратор (Decorator) представляет структурный шаблон проектирования, который позволяет динамически подключать к объекту дополнительную функциональность.
Для определения нового функционала в классах нередко используется наследование. Декораторы же предоставляет наследованию более гибкую альтернативу, поскольку позволяют динамически в процессе выполнения определять новые возможности у объектов.
Когда следует использовать декораторы?
Когда надо динамически добавлять к объекту новые функциональные возможности. При этом данные возможности могут быть сняты с объекта
Когда применение наследования неприемлемо. Например, если нам надо определить множество различных функциональностей и для каждой функциональности наследовать отдельный класс, то структура классов может очень сильно разрастись. Еще больше она может разрастись, если нам необходимо создать классы, реализующие все возможные сочетания добавляемых функциональностей.
Формальная организация паттерна в C# могла бы выглядеть следующим образом:
abstract class Component
{
public abstract void Operation();
}
class ConcreteComponent : Component
{
public override void Operation()
{}
}
abstract class Decorator : Component
{
protected Component component;
public void SetComponent(Component component)
{
this.component = component;
}
public override void Operation()
{
if (component != null)
component.Operation();
}
}
class ConcreteDecoratorA : Decorator
{
public Object NewState { get; set; }
public override void Operation()
{
base.Operation();
}
}
class ConcreteDecoratorB : Decorator
{
public override void Operation()
{
base.Operation();
}
public void NewMethod()
{
}
}
Участники
- Component: абстрактный класс, который определяет интерфейс для наследуемых объектов
- ConcreteComponent: конкретная реализация компонента, в которую с помощью декоратора добавляется новая функциональность
- Decorator: собственно декоратор, реализуется в виде абстрактного класса и имеет тот же базовый класс, что и декорируемые объекты. Поэтому базовый класс Component должен быть по возможности легким и определять только базовый интерфейс.
Класс декоратора также хранит ссылку на декорируемый объект в виде объекта базового класса Component и реализует связь с базовым классом как через наследование, так и через отношение агрегации.
- Классы ConcreteDecoratorA и ConcreteDecoratorB представляют дополнительные функциональности, которыми должен быть расширен объект ConcreteComponent. ConcreteDecoratorA добавляет новое свойство
NewState, а ConcreteDecoratorB добавляет новый методNewMethod(). Подобных классов может быть множество. И они не обязательно должны что-то добавлять: свойства, методы. Они просто могут переопределять уже имеющийся функционал.
Рассмотрим пример. Допустим, у нас есть пиццерия, которая готовит различные типы пицц с различными добавками. Есть итальянская, болгарская пиццы. К ним могут добавляться помидоры, сыр и т.д. И в зависимости от типа пицц и комбинаций добавок пицца может иметь разную стоимость. Теперь посмотрим, как это изобразить в программе на C#:
class Program
{
static void Main(string[] args)
{
Pizza pizza1 = new ItalianPizza();
pizza1 = new TomatoPizza(pizza1); // итальянская пицца с томатами
Console.WriteLine("Название: {0}", pizza1.Name);
Console.WriteLine("Цена: {0}", pizza1.GetCost());
Pizza pizza2 = new ItalianPizza();
pizza2 = new CheesePizza(pizza2);// итальянская пиццы с сыром
Console.WriteLine("Название: {0}", pizza2.Name);
Console.WriteLine("Цена: {0}", pizza2.GetCost());
Pizza pizza3 = new BulgerianPizza();
pizza3 = new TomatoPizza(pizza3);
pizza3 = new CheesePizza(pizza3);// болгарская пиццы с томатами и сыром
Console.WriteLine("Название: {0}", pizza3.Name);
Console.WriteLine("Цена: {0}", pizza3.GetCost());
Console.ReadLine();
}
}
abstract class Pizza
{
public Pizza(string n)
{
this.Name = n;
}
public string Name {get; protected set;}
public abstract int GetCost();
}
class ItalianPizza : Pizza
{
public ItalianPizza() : base("Итальянская пицца")
{ }
public override int GetCost()
{
return 10;
}
}
class BulgerianPizza : Pizza
{
public BulgerianPizza()
: base("Болгарская пицца")
{ }
public override int GetCost()
{
return 8;
}
}
abstract class PizzaDecorator : Pizza
{
protected Pizza pizza;
public PizzaDecorator(string n, Pizza pizza) : base(n)
{
this.pizza = pizza;
}
}
class TomatoPizza : PizzaDecorator
{
public TomatoPizza(Pizza p)
: base(p.Name + ", с томатами", p)
{ }
public override int GetCost()
{
return pizza.GetCost() + 3;
}
}
class CheesePizza : PizzaDecorator
{
public CheesePizza(Pizza p)
: base(p.Name + ", с сыром", p)
{ }
public override int GetCost()
{
return pizza.GetCost() + 5;
}
}
В качестве компонента здесь выступает абстрактный класс Pizza, который определяет базовую функциональность в виде свойства Name и метода GetCost(). Эта функциональность реализуется двумя подклассами ItalianPizza и BulgerianPizza, в которых жестко закодированы название пиццы и ее цена.
Декоратором является абстрактный класс PizzaDecorator, который унаследован от класса Pizza и содержит ссылку на декорируемый объект Pizza. В отличие от формальной схемы здесь установка декорируемого объекта происходит не в методе SetComponent, а в конструкторе.
Отдельные функциональности – добавление томатов и сыры к пиццам реализованы через классы TomatoPizza и CheesePizza, которые обертывают объект Pizza и добавляют к его имени название добавки, а к цене – стоимость добавки, то есть переопределяя метод GetCost и изменяя значение свойства Name.
Благодаря этому при создании пиццы с добавками произойдет ее обертывание декоратором:
Pizza pizza3 = new BulgerianPizza();
pizza3 = new TomatoPizza(pizza3);
pizza3 = new CheesePizza(pizza3);
Сначала объект BulgerianPizza обертывается декоратором TomatoPizza, а затем CheesePizza. И таких обертываний мы можем сделать множество. Просто достаточно унаследовать от декоратора класс, который будет определять дополнительный функционал.
А если бы мы использовали наследование, то в данном случае только для двух видов пицц с двумя добавками нам бы пришлось создать восемь различных классов, которые бы описывали все возможные комбинации. Поэтому декораторы являются более предпочтительным в данном случае методом.
Реализация шаблона в общем виде
- определяем общий интерфейс (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);
}
}
Рассмотрим некоторые варианты использования шаблона Декоратор.
1. Подмена и получение используемого в Декораторе компонента
В некоторых ситуациях может потребоваться отменить добавленные Декоратором возможности. При этом создавать исходный компонент или его копию может быть накладно. Причины могут быть различные:
- сложность порождения объекта (например, реализация компонента скрыта за интерфейсом);
- ресурсоемкость процесса создания или копирования;
- объект находится в определенном состоянии или содержит накопленные данные. В этом случае придется повторить все действия уже над новым экземпляром компонента.
В таких случаях можно лучше получить уже используемый в Декораторе компонент в его текущем состоянии. При этом не правильно просто возвращать значение поля _component, т.к. это может быть вложенный Декоратор. Необходимо предварительно выяснить, что за объект содержится в нем. Для этой цели в языке C# будем использовать оператор as.
Кроме того, возможна и обратная ситуация, когда нужно подменить используемый компонент. Вернемся к примеру с элементами блок-схем из описания шаблона. Декоратор ElementSelected используется для выделения последнего выбранного пользователем элемента. Такой объект нужен только один по определению. Поэтому можно просто подменять декорируемый объект. В этом случае, так же потребуется проверка типа хранимого экземпляра с помощью оператора as.
Чтобы не копировать методы получения и подменны компонента, создадим следующий generic-класс:
/// <summary>Decorator pattern. Attach additional responsibilities
/// to an object dynamically keeping the same interface.</summary>
/// <typeparam name="TComponentInterface">The type of the component interface.</typeparam>
public class DecoratorBase<TComponentInterface>
{
/// <summary>Component -
/// defines an object to which additional responsibilities can be attached.</summary>
protected TComponentInterface _component;
/// <summary>Initializes a new instance of the
/// <see cref="DecoratorBase<TComponentInterface>"/> class.</summary>
/// <param name="component">The component to which additional
/// responsibilities can be attached.</param>
public DecoratorBase(TComponentInterface component)
{
this._component = component;
}
/// <summary>Gets the component.</summary>
/// <returns>The component.</returns>
public TComponentInterface GetComponent()
{
var decorator = this._component as DecoratorBase<TComponentInterface>;
if (decorator != null) {
return decorator.GetComponent();
}
return this._component;
}
/// <summary>Sets the component.</summary>
/// <param name="component">The component to set.</param>
public void SetComponent(TComponentInterface component)
{
var decorator = this._component as DecoratorBase<TComponentInterface>;
if (decorator != null) {
decorator.SetComponent(component);
return;
}
this._component = component;
}
}
На основе полученного класса можно уже создавать базовые Декораторы. Например, вот так можно переписать класс ElementDecoratorBase из примера в описании шаблона:
public class ElementDecoratorBase : DecoratorBase<IElement>, IElement
{
public ElementDecoratorBase(IElement component) :
base(component) { }
public virtual string Text
{
get { return this._component.Text; }
set { this._component.Text = value; }
}
public virtual void Draw()
{
this._component.Draw();
}
}
2. Имитация наследования от закрытых (sealed) классов в C#
Одно из применений, специфичное для C# и на которое просто напрашиваются Декораторы, это имитация наследования от закрытых (sealed) классов.
Предположим, что класс PostStorage, предназначенный для работы с хранилищем записей блога, объявлен как закрытый. При этом он реализует интерфейс IPostStorage. Дальше все просто – используем шаблон для получения “аналога наследника” и добавляем необходимые возможности в новый класс. Поскольку реализация ничем не отличается от обычного Декоратора, то нет смысла приводить исходный код. Но используем закрытый класс в следующем примере.
3. События при вызове методов компонента
Решим еще одну задачу: необходимо предоставить возможность получать уведомления каждый раз , когда происходит загрузка или публикация записи в блог. Общий интерфейс и исходный компонент выглядят следующим образом:
public class BlogPost
{
public string Title { get; set; }
public string Content { get; set; }
}
public interface IPostStorage
{
BlogPost GetPost(int postId);
void Publish(BlogPost post);
}
public sealed class PostStorage : IPostStorage
{
public BlogPost GetPost(int postId) { /* Skipped */ }
public void Publish(BlogPost post) { /* Skipped */ }
}
Поскольку класс является закрытым, то используем шаблон Декоратор. В нем создадим события, соответствующие вызовам методов общего интерфейса.
public class PostStorageDecorator : IPostStorage
{
private IPostStorage _component;
public delegate void PostHandler(BlogPost userData);
public event PostHandler OnGetPost;
public event PostHandler OnPublish;
public PostStorageDecorator(IPostStorage component)
{
this._component = component;
}
public BlogPost GetPost(int userId)
{
BlogPost post = this._component.GetPost(userId);
this.OnGetPost(post);
return post;
}
public void Publish(BlogPost post)
{
this.OnPublish(post);
this._component.Publish(post);
}
}
Обратите внимание, что подписчики событий получат данные до завершения работы методов класса. Они не только узнают о загрузке или публикации записей, но и смогут контролировать их содержимое. Это позволяет, например, создавать расширения, которые будут автоматически подставлять ссылки, создавать подсказки для аббревиатур и т.д.
Рассмотрим пример использования созданного Декоратора. Как всегда, его использование прозрачно для клиента (в данном случае это метод PublishCurrentPost()). А в результате два созданных подписчика, получат уведомления перед публикацией записи.
public class PostWatcher
{
public void OnGetPost(BlogPost post)
{
Console.WriteLine("PostWatcher.OnGetPostnTitle: {0}nContent: {1}n",
post.Title, post.Content);
}
public void OnPublish(BlogPost post)
{
post.Content += " [Updated]";
Console.WriteLine("PostWatcher.OnPublishnTitle: {0}nContent: {1}n",
post.Title, post.Content);
}
}
public static class DecoratorDemo
{
public static void PublishCurrentPost(IPostStorage storage)
{
BlogPost newPost = new BlogPost() {
Title = DateTime.Now.ToString(),
Content = "Coming soon ..."
};
storage.Publish(newPost);
}
public static void Execute()
{
var postStorage = new PostStorageDecorator(new PostStorage());
var pw1 = new PostWatcher();
postStorage.OnGetPost += pw1.OnGetPost;
postStorage.OnPublish += pw1.OnPublish;
var pw2 = new PostWatcher();
postStorage.OnGetPost += pw2.OnGetPost;
postStorage.OnPublish += pw2.OnPublish;
DecoratorDemo.PublishCurrentPost(postStorage);
}
}
Как можно было убедиться из примеров, использование шаблона Декоратора является таким же мощным средством, как и механизм наследования.