Перехват вызовов методов (interception). Аспектно-ориентированное программирование средствами Microsoft Unity Application Block.

Вы наверняка сталкивались при разработке приложений с увеличением размера текста программ за счёт вспомогательного кода. Так, например, при разработке моделей представлений (паттерн View-Model-View Model), вам приходится реализовывать интерфейс INotifyPropertyChanged. И ваш код выглядит подобно приведённому ниже.

public interface ITestViewModel
{
        int Foo1 { get; set; }

        int Foo2 { get; set; }
}

//Модель представления
public class TestViewModel : ITestViewModel, INotifyPropertyChanged
{
 private int _foo1;
        public int Foo1
        {
            get
            {
                return _foo1;
            }
            set
            {
                if(_foo1 != value)
                {
                    _foo1 = value;
                    OnPropertyChanged("Foo1");

                }
            }
        }

        private int _foo2;
        public int Foo2
        {
            get
            {
                return _foo2;
            }
            set
            {
                if(_foo2 != value)
                {
                    _foo2 = value;
                    OnPropertyChanged("Foo2");                
                }
            }
        }

        protected virtual void OnPropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if(handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    
        public event PropertyChangedEventHandler PropertyChanged;

 }

* This source code was highlighted with Source Code Highlighter.


Необходимость генерации события PropertyChanged при каждом изменении свойств делает невозможным использование авто-свойств, из-за чего код быстро «разбухает» за счёт вспомогательного кода.
Упростить подобный код позволит применение подходов аспектно-ориентированного программирования, которые реализованы в Microsoft Unity Application Block при помощи перехватов (interception) вызовов методов.
Реализуются перехваты вызовов для зарегистрированных в контейнере типов одним из трёх способов.

Перехват при помощи генерации для объекта прозрачного прокси (TransparentProxyInterceptor).
В данном случае класс, чьи методы подлежат перехвату, должен быть наследником MarshalByRefObject. Для объекта генерируется прозрачный прокси, через который и осуществляется работа.
Преимущества: возможность перехвата любых свойств и методов класса.
Недостатки: необходимость наследования от MarshalByRefObject, более медленный вызов перехватываемых методов по сравнению с другими способами.
Перехват при помощи интерфейса (InterfaceInterceptor)
Для исходного класса, реализующего указанный интерфейс, генерируется на лету тип, владеющий экземпляром исходного класса и реализующим тот же самый интерфейс (паттерн Decorator).
Преимущества: более быстрый вызов перекрываемых свойств и методов.
Недостатки: перехватываются только те методы и свойства, объявление которых определено в интерфейсе.

Перехват при помощи виртуальной функции (VirtualMethodInterceptor)
В данном случае перехвату подлежат только виртуальные методы и свойства класса.
Преимущества: такие же, как и у перехвата при помощи интерфейса.
Недостаткн: необходимость делать перехватываемый метод или свойство виртуальными.


Реализовать перехват в Microsoft Unity Application Block можно
• при помощи атрибутов, наследующих тип HandlerAttribute,
• при помощи поведенческих классов, наследующих интерфейс IInterceptionBehavior.

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

//Класс атрибута, определяющий перехватчик для метода
  public class LogAttribute : HandlerAttribute
  {

    //Класс перехватчика метода
    private class LogHandler : ICallHandler
    {

      public LogHandler()
      {
        Order = 1;
      }

      #region ICallHandler Members

      //Реализация перехвата вызова метода
      public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
      {
        Console.WriteLine("calling {0}\n", input.MethodBase.Name); //Вывод имени перехватываемого метода на консоль
        return getNext()(input, getNext); //Вызов остальных перехватчиков и перехватываемого метода
      }

      public int Order {get; set;} //Порядок вызова перехватчика (чем меньше, тем раньше он вызовется)
      

      #endregion
    }
    
    public override ICallHandler CreateHandler(IUnityContainer container)
    {
      return new LogHandler();
      //Лучше, конечно, так
      //return container.Resolve<LogHandler>();
    }
  }

* This source code was highlighted with Source Code Highlighter.


Применение

  public class Foo : MarshalByRefObject
  {
    [Log]  //Атрибут перехватчика
    public void Bar() //Вызовы метода Bar теперь будут протоколироваться на консоль
    {
    }
  }

  class Program
  {
    static void Main(string[] args)
    {

      IUnityContainer container = new UnityContainer();
      container.AddNewExtension<Interception>();
      container.RegisterType<Foo>(new Interceptor<TransparentProxyInterceptor>()); //Перехват через генерацию прозрачного прокси

      Foo foo = container.Resolve<Foo>();
      foo.Bar(); //Вызов Bar будет перехвачен

    }
  }

* This source code was highlighted with Source Code Highlighter.


Второй же способ позволяет не только перехватить методы объекта, но внедрить в объект «на лету» поддержку одного или нескольких интерфейсов.


public class NotifyPropertyChangedBehavior : IInterceptionBehavior
  {

    private PropertyChangedEventHandler _handler;
    private readonly object _handlerLock = new object();

    
    #region IInterceptionBehavior Members

    //Возвращаем "внедряемые" интерфейсы
    public virtual IEnumerable<Type> GetRequiredInterfaces()
    {
      return new[] { typeof(INotifyPropertyChanged) };
    }

    //Перехватываем методы и свойства
    public virtual IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
    {

      var methodReturn = InvokeINotifyPropertyChangedMethod(input);
      if (methodReturn != null)
      {
        return methodReturn;
      }

      //Обрабатываем изменение значения свойства (вызов set_PropertyName)
      var propertyName = (input.MethodBase.IsSpecialName && input.MethodBase.Name.StartsWith("set_")) ? input.MethodBase.Name.Substring(4) : null;
      object propertyValue = null;
      Func<object, object> propertyGetAccessor = null;
      if (propertyName != null)
      {
        //Иcпользуем для доступа к значению свойств делегаты вместо Reflection для более быстрой работы
        propertyGetAccessor = PropertyAccessor.GetPropertyValueAccessor(input.MethodBase.DeclaringType, input.Target, propertyName);
        propertyValue = propertyGetAccessor(input.Target); //значение свойства до изменения значения
      }

      methodReturn = getNext()(input, getNext); //изменяем значение свойства


      if (propertyName != null)
      {
        try
        {
          //Если значение свойства изменилось, генерируем событие PropertyChanged
          var newValue = input.Arguments[0];
          IComparable comparable = newValue as IComparable;
          if (comparable != null)
          {
            if (comparable.CompareTo(propertyValue) != 0)
            {
              OnPropertyChanged(input, propertyName);
            }
          }
          else if (newValue != propertyValue)
          {
            OnPropertyChanged(input, propertyName);
          }
        }
        catch (Exception e)
        {
          return input.CreateExceptionMethodReturn(e);
        }
      }

      return methodReturn;

    }

    
    public bool WillExecute
    {
      get { return true; } //выполнять всегда
    }

    #endregion

    protected virtual void OnPropertyChanged(IMethodInvocation input, string propertyName)
    {
      //Генерация события PropertyChangedу перекрываемого объекта
      var currentHandler = _handler;
      if (currentHandler != null)
      {
        currentHandler(input.Target, new PropertyChangedEventArgs(propertyName));
      }
    }

    //Вызов INotifyPropertyChanged
    protected IMethodReturn InvokeINotifyPropertyChangedMethod(IMethodInvocation input)
    {
      if (input.MethodBase.DeclaringType == typeof(INotifyPropertyChanged))
      {
        switch (input.MethodBase.Name)
        {
          case "add_PropertyChanged":
            lock (_handlerLock)
            {
              _handler = (PropertyChangedEventHandler)Delegate.Combine(_handler, (Delegate)input.Arguments[0]);
            }
            break;

          case "remove_PropertyChanged":
            lock (_handlerLock)
            {
              _handler = (PropertyChangedEventHandler)Delegate.Remove(_handler, (Delegate)input.Arguments[0]);
            }
            break;

          default:
            return input.CreateExceptionMethodReturn(new InvalidOperationException());
        }

        return input.CreateMethodReturn(null);
      }

      return null;
    }



  }

* This source code was highlighted with Source Code Highlighter.

Исходный текст класса PropertyAccessor

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;

namespace Unity.InterceptionExtension
{
  internal static class PropertyAccessor
  {
    private static int GetKey(Type target, string propertyName)
    {
      return (new { V1 = target.GetHashCode(), V2 = propertyName.GetHashCode() }).GetHashCode();
    }

    private static Dictionary<int, Func<object, object>> _getPropertyCache;

    private static Dictionary<int, Func<object, object>> GetPropertyCache
    {
      get
      {
        if (_getPropertyCache == null)
        {
          _getPropertyCache = new Dictionary<int, Func<object, object>>();
        }
        return _getPropertyCache;
      }
    }

    private static Dictionary<int, Action<object, object>> _setPropertyCache;

    private static Dictionary<int, Action<object, object>> SetPropertyCache
    {
      get
      {
        if (_setPropertyCache == null)
        {
          _setPropertyCache = new Dictionary<int, Action<object, object>>();
        }
        return _setPropertyCache;
      }
    }
       

    public static Func<object, object> GetPropertyValueGetter(Type type, object target, string propertyName)
    {
      Func<object, object> Accessor;
      var key = GetKey(type, propertyName);
      if (!GetPropertyCache.TryGetValue(key, out Accessor))
      {
        var parameter = Expression.Parameter(typeof(object), "obj");
        var expression = Expression.Lambda<Func<object, object>>
        (
          Expression.Convert(Expression.Property(Expression.Convert(parameter, type), propertyName), typeof(object)), parameter
        );
        
        Accessor = expression.Compile(); //(obj)=>(object)((T)obj).PropertyName
        GetPropertyCache.Add(key, Accessor);
      }
      return Accessor;
    }

    public static Action<object, object> GetPropertyValueSetter(Type type, object target, string propertyName)
    {
      return GetPropertyValueSetter(target, type.GetProperty(propertyName));
    }

    public static Action<object, object> GetPropertyValueSetter(object target, PropertyInfo property)
    {

      Action<object, object> Accessor;
      var type = property.DeclaringType;
      var key = GetKey(type, property.Name);
      if (!SetPropertyCache.TryGetValue(key, out Accessor))
      {
        var parameter = Expression.Parameter(typeof(object), "obj");
        var value = Expression.Parameter(typeof(object), "value");
        var expression = Expression.Lambda<Action<object, object>>
        (
          Expression.Call(Expression.Convert(parameter, type), property.GetSetMethod(), Expression.Convert(value, property.PropertyType)), parameter, value
        );

        Accessor = expression.Compile(); //(obj, value)=>((T)obj).PropertyName = (PropertyType)value        Accessor = expression.Compile(); //(obj)=>(object)((T)obj).PropertyName
        SetPropertyCache.Add(key, Accessor);
      }
      return Accessor;

    }
  }
}

* This source code was highlighted with Source Code Highlighter.

И теперь приведённую в самом начале статьи модель представления можно будет представить так.

public class TestViewModel : ITestViewModel
{
    //Нет необходимости в INotifyPropertyChanged и в связанном с ним вспомогательном коде  
    public int Foo1 {get; set;}
    public int Foo2 {get; set;}
}

* This source code was highlighted with Source Code Highlighter.


В коде регистрации типов в контейнере нужно будет добавить следующее

container.AddNewExtension<Interception>();
container.RegisterType<ITestViewModel, TestViewModel>(new Interceptor<InterfaceInterceptor>(), new InterceptionBehavior<NotifyPropertyChangedBehavior>()); //Перехват через интерфейс и использование поведенческого класса NotifyPropertyChangedBehavior
//Перехватываться будут только методы и свойства интерфейса ITestViewModel

* This source code was highlighted with Source Code Highlighter.


Для создания модели представления, как и раньше, используется вызов Resolve у контейнера. Ниже приведён тест работы с моделью представления.

var test = container.Resolve<ITestViewModel>();
((INotifyPropertyChanged)test).PropertyChanged += (s, e) => Console.WriteLine(e.PropertyName);

test.Foo1 = 11; //На консоль выведется строка Foo1
test.Foo1 = 11; //На консоль ничего не выведётся
test.Foo1 = 12; //На консоль выведется строка Foo1


* This source code was highlighted with Source Code Highlighter.


Таким образом, используя механизм перехвата вызовов методов Microsoft Unity Application Block возможно:
• определить дополнительный код, выполняемый при вызове определённых методов;
• реализовать «на лету» в объекте поддержку одного или нескольких интерфейсов.

Перехват Microsoft Unity Application Block осуществляется во время выполнения и накладывает дополнительные требования к перехватываемым методам или объектам (наследование MarshalByRefObject или виртуальность перехватываемых методов).


Существующие на сегодняшний день специальные инструменты для аспектно-ориентированного программирования (PostSharp, NAspect и др.) позволяют, конечно же, справиться с подобными задачами более эффективно. Но лёгкость дистрибутива Microsoft Unity Application Block, его бесплатность, открытость исходных текстов, реализация IoС и DI-контейнеров из коробки (изначально Unity Application Block для этого и создавался), простая расширяемость, делают его хорошим выбором для повседневных задач.

2 комментария:

AProoks комментирует...

Спасибо за статью.
хочется только узнать код на:
PropertyAcceptor.GetPropertyValueAcceptor

Unknown комментирует...

AProoks, спасибо за замечание. Внёс исправления в статью. Acceptor я спутал с Accessor.

Отправить комментарий