Динамический декоратор. Часть 2 – Dynamic proxy
В прошлой части, используя dynamic и reflection, была решена проблема динамической поддержки методов и свойств декорируемого объекта. Но осталась еще одна: необходимо реализовать поддержку заданного интерфейса. Для это используем …
Динамический прокси (Dynamic proxy)
Динамический прокси позволяет “на лету” генерировать объекты, реализующие шаблон Прокси. Для этого определяется конкретный экземпляр или интерфейс для перехвата, а так же выполняемые при этом действия. Порожденный объект является прозрачным для клиента и передается ему в использование. Т.е. всё аналогично обычному созданию класса Прокси, но только все действия по созданию происходят в run-time. Соответственно, это предоставляет больше гибкости для решения различных различных задач.
Реализация Динамического прокси задача не тривиальная. Поэтому, как и в случае с интерфейсом IDynamicMetaObjectProvider, воспользуемся готовым решением. Только на этот раз входящим не в ядро .NET, а из библиотеки Castle Project.
Как создать Динамический прокси? Для этого потребуется две составляющих:
- Интерфейс, который создаваемый экземпляр должен поддерживать. В случае Динамического декоратора это будет TComponentInterface.
- Класс-перехватчик, определяющий действия при обращении к Динамическому прокси. Для этого нужно реализовать интерфейс 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<TComponentInterface>"/> 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<TComponent, TComponentInterface>"/> 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: