Andrey on .NET | Декоратор. Примеры реализации.

Декоратор. Примеры реализации.

Рассмотрим некоторые варианты использования шаблона Декоратор.

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&lt;TComponentInterface&gt;"/> 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);
    }
}

Как можно было убедиться из примеров, использование шаблона Декоратора является таким же мощным средством, как и механизм наследования.

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