Рассмотрим некоторые варианты использования шаблона Декоратор.
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.OnGetPost\nTitle: {0}\nContent: {1}\n",
post.Title, post.Content);
}
public void OnPublish(BlogPost post)
{
post.Content += " [Updated]";
Console.WriteLine("PostWatcher.OnPublish\nTitle: {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);
}
}
Как можно было убедиться из примеров, использование шаблона Декоратора является таким же мощным средством, как и механизм наследования.