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

Динамический декоратор. Часть 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).

Комментарии закрыты