Динамический декоратор. Часть 1 – dynamic и reflection

При разработке Декоратора, переадресация вызовов к декорируемому объекту достаточно скучная и рутинная работа. Для её упрощения существуют утилиты, генерирующие необходимый код.

Но есть другой подход – использовать в качестве базового класса Динамический декоратор, который обеспечит автоматическую переадресацию вызовов. Это стало возможно c появлением dynamic в .NET 4. Кроме того, потребуется интересный вариант реализации еще одного шаблона – Динамический прокси.

При разработке будет необходимо решить следующие проблемы:

  • На этапе создания неизвестно, какие методы предоставляет декорируемый объект. Поэтому Декоратор должен определять свои методы в процессе выполнения приложения.
  • Необходимо указанные выше методы объединить в рамках заданного, но так же неизвестного на момент разработки, интерфейса.

Эти задачи могли бы быть практически тупиковыми, если бы не ...

Dynamic

В .NET 4 появился тип dynamic, объекты которого обходят контроль статического типа. Проще говоря, при компиляции не выполняется проверка существования используемых методов и свойств. И только в процессе выполнения приложения, в момент непосредственного обращения, определяется их фактическое наличие и привязка.

Стоит отметить, что для dynamic теряется возможность контролировать ошибки в вызовах до момента выполнения приложения. Компилятор справедливо полагает, что данный тип поддерживает любые операции. Кроме того, по той же причине пропадает IntelliSense в редакторе Visual Studio. Но с другой стороны – появляется возможность динамически менять сам объект, его методы и свойства.

В простейшем случае под видом dynamic может скрываться любой существующий тип. В момент обращения к такому объекту, .NET определяет наличие у него требуемого метода или свойства. При его отсутствии будет выброшено исключение, а при наличии – выполнено связанное действие: запущен код, присвоено или прочитано значение. Сценарии и примеры использования такого подхода можно в достаточном количестве найти в интернете.

Но возможности dynamic этим не ограничиваются. Он позволяет самому объекту определять поддерживаемые им операции в процессе выполнения приложения. Для этого необходимо реализовать интерфейс IDynamicMetaObjectProvider. Его единственный метод GetMetaObject() используется для определения привязки методов, свойств и полей при обращении к объекту.

Давайте посмотрим на простой пример, чтобы лучше понять суть написанного:

public class MyDynamic : IDynamicMetaObjectProvider
{
    public void DefinedMethod ()
    {
        Console.WriteLine("DefinedMethod");
    }

    public DynamicMetaObject GetMetaObject(System.Linq.Expressions.Expression parameter)
    {
        throw new NotImplementedException();
    }
}

class Program
{
    static void Main(string[] args)
    {
        dynamic myDynamicObject = new MyDynamic();
        // myDynamicObject.DefinedMethod();
        myDynamicObject.Test();
    }
}

Обратите внимание, что экземпляр объявляется используя ключевое слово dynamic.Если запустить выполнение по шагам, то будет видно, что любые обращения к myDynamicObject приводят к вызову метода GetMetaObject(). Это справедливо даже для членов класса, которые определены на этапе компиляции. Например, таким является DefinedMethod().

Реализация IDynamicMetaObjectProvider достаточно сложная задача и требует знаний Dynamic Language Runtime (DLR) . Для ее упрощения в .NET существует базовый класс DynamicObject. В этом случае, определение класса Динамического декоратора будет таким:

public class DynamicDecorator<TComponentInterface> : DynamicObject

, где TComponentInterface – интерфейс, который должен будет поддерживать создаваемый декоратор.

DynamicObject предоставляет реализацию IDynamicMetaObjectProvider, которая обеспечивает следующее поведение при обращении к объекту:

  • если запрашиваемый метод, свойство или после определены в самом классе, то запрос будет переадресован к ним;
  • в противном случае, вызывается один из методов для определения реализации, например:
    • TrySetMember() – для установки значения свойства или поля;
    • TryGetMember() – для чтения значения свойства или поля;
    • TryInvokeMember() – осуществляет вызов метода.

Полный список методов доступен в документации на класс DynamicObject. Все они являются виртуальными, что предоставляет возможность переопределить их поведение в классе Динамического декоратора.

Таком образом решена половина первой проблемы – динамическая поддержка нужных членов класса. Осталось найти способ "на лету" определить их у декорируемого объекта. И в этом поможет ...

Reflection

Еще один мощный инструмент .NET – Reflection. Он позволяет внимательно изучить любой тип. И это как раз то, что нужно для выяснения подробностей декорируемого объекта.

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

/// <summary>Component - defines an object to which additional
/// responsibilities can be attached.</summary>
protected TComponentInterface _component;

/// <summary>Represents component type.</summary>
private Type _componentType;

/// <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;
}

/// <summary>Gets or sets the object to which 
/// additional responsibilities can be attached.</summary>
public TComponentInterface Component
{
    get { return this._component; }
    set
    {
        this._component = value;
        this._componentType = this._component.GetType();
    }
}

В начале получим информацию о типе переданного объекта, используя вызов GetType(). Она сохраняется в объекте типа Type, который содержит методы для поиска членов класса по имени.

Перейдем к реализации метода TrySetMember(), который будет отвечать за установку значений свойств и полей класса. Поскольку Динамический декоратор должен использовать только public доступ к объекту, то указываем это в параметрах поиска. Исходный код будет выглядеть так:

/// <summary>Provides the implementation for operations that set member values.</summary>
/// <param name="binder">Provides information about the object
/// that called the dynamic operation.</param>
/// <param name="value">The value to set to the member.</param>
/// <returns>true if the operation is successful; otherwise, false.</returns>
public override bool TrySetMember(SetMemberBinder binder, object value)
{
    // search for a property
    PropertyInfo property = this._componentType.GetProperty(binder.Name,
        BindingFlags.Public | BindingFlags.Instance);
    if (property != null) {
        property.SetValue(this._component, value, null);
        return true;
    }

    // search for a public field
    FieldInfo field = this._componentType.GetField(binder.Name,
        BindingFlags.Public | BindingFlags.Instance);
    if (field != null) {
        field.SetValue(this._component, value);
        return true;
    }

    return base.TrySetMember(binder, value);
}

Параметр binder содержит информацию о запрашиваемом поле или свойстве, в том числе и его имя (binder.Name). В случае успешного поиска, найденному члену класса будет установлено заданное значение. При неудаче произойдет вызов базового метода, который выбросит исключение.

Метод получения значений TryGetMember() почти аналогичен, за исключением замены записи значения на его чтение. Поэтому рассматривать его тут не будем.

С вызовом методов чуть сложнее. В отличии от свойств могут существовать два метода с одинаковым названием. Поэтому придется еще учитывать какие использовались при вызове параметры. Для этого напишем вспомогательный метод, который возвращает список их типов:

/// <summary>Gets the list of arguments types.</summary>
/// <param name="args">An object array that contains arguments.</param>
/// <returns>The list of arguments types.</returns>
private Type[] GetArgumentsTypes(object[] args)
{
    Type[] argTypes = new Type[args.GetLength(0)];

    int index = 0;
    foreach (var arg in args) {
        argTypes[index] = arg.GetType();
        index++;
    }

    return argTypes;
}

Теперь будем искать метод не только по имени, но и по списку типов его параметров:

/// <summary>Provides the implementation for operations that invoke a member.</summary>
/// <param name="binder">Provides information about the dynamic operation.</param>
/// <param name="args">The arguments that are passed to the object member
/// during the invoke operation.</param>
/// <param name="result">The result of the member invocation.</param>
/// <returns>true if the operation is successful; otherwise, false.</returns>
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, 
    out object result)
{
    MethodInfo method = this._componentType.GetMethod(binder.Name,
        BindingFlags.Public | BindingFlags.Instance, null, this.GetArgumentsTypes(args), null);

    if (method != null) {
        result = method.Invoke(this._component, args);
        return true;
    }

    return base.TryInvokeMember(binder, args, out result);
}

Вызов метода Invoke() обеспечивает выполнение заданного метода с передачей ему параметров. При необходимости можно переопределить и другие методы DynamicObject. Например бинарные операции или обращение по индексу.

Итак, Динамический декоратор на данный момент предоставляет следующие возможности:

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

Экземпляр создаваемого класса уже можно использовать как самостоятельный объект. Однако, необходимо ещё одно – предоставить возможность использовать методы через заданный интерфейс. И для этого потребуется очень интересный вариант реализации шаблона Прокси.

Читайте во второй части: Применение Динамического прокси.

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