Динамический декоратор. Часть 2 – Dynamic proxy

В прошлой части, используя dynamic и reflection, была решена проблема динамической поддержки методов и свойств декорируемого объекта. Но осталась еще одна: необходимо реализовать поддержку заданного интерфейса. Для это используем …

Динамический прокси (Dynamic proxy)

Динамический прокси позволяет “на лету” генерировать объекты, реализующие шаблон Прокси. Для этого определяется конкретный экземпляр или интерфейс для перехвата, а так же выполняемые при этом действия. Порожденный объект является прозрачным для клиента и передается ему в использование. Т.е. всё аналогично обычному созданию класса Прокси, но только все действия по созданию происходят в run-time. Соответственно, это предоставляет больше гибкости для решения различных различных задач.

Реализация Динамического прокси задача не тривиальная. Поэтому, как и в случае с интерфейсом IDynamicMetaObjectProvider, воспользуемся готовым решением. Только на этот раз входящим не в ядро .NET, а из библиотеки Castle Project.

Как создать Динамический прокси? Для этого потребуется две составляющих:

  1. Интерфейс, который создаваемый экземпляр должен поддерживать. В случае Динамического декоратора это будет TComponentInterface.
  2. Класс-перехватчик, определяющий действия при обращении к Динамическому прокси. Для этого нужно реализовать интерфейс IInterceptor и его единственный метод Intercept().

Кроме того, динамический прокси может быть:

  • “с целью”, когда после перехвата запрос передается объекту-цели;
  • “без цели”, когда вызывается только перехватчик, что подходит для Динамического декоратора.

Рассмотрим создание непосредственно на примере разработки Динамического декоратора. Добавим свойство для экземпляра Динамического прокси:

/// <summary>Gets or sets the component interface.</summary>
/// <value>The dynamic proxy used to implement the TComponent interface.</value>
public TComponentInterface Interface { get; private set; } 

Теперь внесем интерфейс IInterceptor в определение класса DynamicDecorator:

public class DynamicDecorator<TComponentInterface> : DynamicObject, IInterceptor

Все обращения к Динамическому прокси будут переадресовываться в метод Intercept(). Информация о вызываемом методе передается в параметре invocation. Стоит отметить, что IInterceptor, в отличии от DynamicObject, не разделяет методы и свойства. Поэтому обращения к ним передаются как get и set вызовы. В остальном код Intercept() схож с TryInvokeMember(): по имени и параметрам ищем объект сначала среди определенных в Динамическом декораторе, а затем в декорируемом объекте.

/// <summary>Intercepts the specified invocation.</summary>
/// <param name="invocation">The invocation.</param>
public void Intercept(IInvocation invocation)
{
    Type decoratorType = this.GetType();
    Type[] argTypes = this.GetArgumentsTypes(invocation.Arguments);

    MethodInfo method = decoratorType.GetMethod(invocation.Method.Name,
        BindingFlags.Public | BindingFlags.Instance, null, argTypes, null);

    if (method != null) {
        invocation.ReturnValue = method.Invoke(this, invocation.Arguments);
        return;
    }

    method = this._componentType.GetMethod(invocation.Method.Name,
        BindingFlags.Public | BindingFlags.Instance, null, argTypes, null);

    if (method != null) {
        invocation.ReturnValue = method.Invoke(this._component, invocation.Arguments);
        return;
    }

    // No match was found for required method
    string argList = this.GetArgumentsString(invocation.Arguments);
    throw new InvalidOperationException(
        string.Format("No match was found for method {0}({1}).",
        invocation.Method.Name, argList));
}

Теперь, зная требуемый интерфейс и имея метод-перехватчик, остается только создать сам прокси. Для этого немного изменим конструктор класса:

/// <summary>Initializes a new instance of the 
/// <see cref="DynamicDecorator&lt;TComponentInterface&gt;"/> class.</summary>
/// <param name="component">The component to which 
/// additional responsibilities can be attached.</param>
public DynamicDecorator(TComponentInterface component)
{
    this._component = Component;

    var _generator = new ProxyGenerator();
    this._interfaceProxy = (TComponentInterface)_generator.CreateInterfaceProxyWithoutTarget(
        typeof(TComponentInterface), this);
}

Создание Динамического прокси завершено.

Добавим еще два метода, которые были в реализации Декоратора с использованием generic: SetComponent() и GetComponent(). Напомню, что они позволяют устанавливать и получать используемый экземпляр компонента, независимо от того сколько Декораторов выстроено вложено друг в друга.

/// <summary>Gets the component.</summary>
/// <returns>The component.</returns>
public TComponentInterface GetComponent()
{
    var decorator = this.Component as DynamicDecorator<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 DynamicDecorator<TComponentInterface>;
    if (decorator != null) {
        decorator.SetComponent(component);
        return;
    }

    this.Component = component;
}

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

/// <summary>Dynamic implementation of the Decorator pattern.</summary>
/// <typeparam name="TComponent">The type of the component.</typeparam>
/// <typeparam name="TComponentInterface">The interface of the component.</typeparam>
public class DynamicDecorator<TComponent, TComponentInterface> :
    DynamicDecorator<TComponentInterface>
    where TComponent : class, TComponentInterface, new()
{
    /// <summary>Initializes a new instance of the
    /// <see cref="DynamicDecorator&lt;TComponent, TComponentInterface&gt;"/> class.</summary>
    public DynamicDecorator() : base((TComponentInterface)new TComponent()) { }
}

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

В качестве примера воспользуемся тем же самым классом, что и в описании шаблона Декоратор:

public interface IElement
{
    string Text { get; set; }

    void Draw();
}

public class Element : IElement
{
    public string Text { get; set; }

    public void Draw()
    {
        Console.WriteLine("Drawing element ({0})", this.Text);
    }
}

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

public class ElementStrikedDecorator : DynamicDecorator<IElement>
{
    public ElementStrikedDecorator(IElement component) : base(component) { }

    public void Draw()
    {
        this._component.Draw();
        this.Strike();
    }

    private void Strike()
    {
        Console.WriteLine("Striked");
    }
}

public class ElementBgndDecorator : DynamicDecorator<IElement>
{
    public ElementBgndDecorator(IElement component) : base(component) { }

    public string BackgroundFileName { get; set; }

    public void Draw()
    {
        this.SetBackground();
        this._component.Draw();
    }

    private void SetBackground()
    {
        Console.WriteLine("Background: {0}", this.BackgroundFileName);
    }
}

Осталось привести непосредственно пример использования экземпляров этих классов:

class Program
{
    public static void DrawElement(string title, IElement element)
    {
        Console.WriteLine("{0} ---------------------------", title);
        element.Draw();
        Console.WriteLine(string.Empty);
    }

    static void Main(string[] args)
    {
        Element element = new Element();
        DrawElement("Base element", element);

        dynamic elementBgnd = new ElementBgndDecorator(element);
        elementBgnd.BackgroundFileName = "SomeBgndFile.png";

        dynamic elementStriked = new ElementStrikedDecorator(elementBgnd.Interface);
        elementStriked.Text = "Demo";

        DrawElement("Striked element with background", elementStriked.Interface);

        IElement sourceElement = elementStriked.GetComponent();
        DrawElement("Element with background", sourceElement);

        Console.WriteLine("Press any key ...");
        Console.ReadKey();
    }
}

Посмотреть онлайн исходный код разработанного класса можно в третьей части. Кроме того, можете воспользоваться ссылкой на демонстрационный проект для Visual Studio 2010:

DynamicDecorator.zip (141 kb).