На главную
ПРАВИЛА FAQ Помощь Участники Календарь Избранное DigiMania RSS
msm.ru
Модераторы: maxim84_, juice
  
> Unity Framework, как реализация подмножества AOП
    Unity Framework – легковесный, расширяемый, dependency injection контейнер. Одной из его особенностей является поддержка подмножества АОП (Аспектно Ориентированное Программирование). Сегодня я предлагаю, Вам, ознакомиться с этими возможностями.
    Прежде чем переходить к технической части, мне бы хотелось рассмотреть простой пример, который продемонстрирует один из вариантов, когда АОП может оказаться полезным в наших приложениях.

    ExpandedWrap disabled
      using System;
      using System.Collections.Generic;
      using System.Linq;
      using System.Text;
       
      namespace StandardAproachSample
      {
          public class Printer
          {
              public virtual void Print(string line)
              {
                  if (line == null)
                      throw new ArgumentNullException("line");
       
                  Console.WriteLine(line);
                  Console.WriteLine("Was written {0} symbols", line.Length);
              }
          }
       
          class Program
          {
              static void Main(string[] args)
              {
                  Printer printer = new Printer();
                  printer.Print(null);
              }
          }
      }


    Часто ли Вы видели код подобный методу Print класса Printer? Я наблюдаю его ежедневно от метода к методу. Проверять приходящие параметры на null, является общепринятой практикой. И даже если мы не всегда делаем это в каждом методе, то на верхнем уровне, с которым взаимодействует клиент нашего кода это является обыденностью.
    Если бы методы всегда принимали единственный параметр ссылочного типа или наши приложения содержали бы единственный метод, принимающий такие параметры, проблем бы вероятно не возникало. Но признаюсь по секрету, что написания таких проверок для меня является настоящей тягостью. Что может быть скучнее в программирование, чем рутинное, монотонное написание однотипного кода? Более того такой код «засоряет» наши методы добавляя в бизнес логику инфраструктурный код. А часто ли вы пользуетесь логированием? Предположу, что каждый когда-нибудь да пользовался. Необходимая и полезная функциональность, относящаяся к подсистеме логирования или трассировки пересыпает собой нашу бизнес логику. В лучшем случае просто фиксируя факт вызова, а в особо «тяжелых» случаях трассируются алгоритмы, параметры методов, результаты выполнения, контексты вызовов и безопасности … Вот если бы …
    Немного пофантазировав, без труда можно было бы представить идеальное решение для такого кода. Вероятно, это будет, нечто похожее вот на это:

    ExpandedWrap disabled
          public class Printer
          {
              [NullControl]
              public void Print(string line)
              {
                  Console.WriteLine(line);
                  Console.WriteLine("Was written {0} symbols", line.Length);
              }
          }


    Решение: некий «волшебный» атрибут, применив который к любому методы, мы получаем автоматическую проверку параметров на null, а также генерирование соответствующего исключения ArgumentNullException говорящего нам, какой именно параметр привел к его возникновению.
    Мы могли бы попытаться самостоятельно решить этот вопрос. Решение находится практически на поверхности и его реализация пролегает, через паттерн Proxy. Для начала нужно было бы реализовать динамическую генерацию proxy классов для регистрируемых типов и пользоваться локатором для их (proxy) инстанцирования. Сгенерированный proxy объект мог бы выполнить соответствующие проверки и сгенерировать в случае надобности исключение. Проверки могли бы быть вынесены в некий аспект ассоциируемый с целевым типом посредством атрибута. Другое возможное решение это использовать классы которые получают proxy классы на основе самой инфраструктуры .NET Framework, тут можно было бы воспользоваться функциональностью предоставляемой объектами наследующими MarshalByRefObject или его потомка ContextBoundObject. Далее путь путем обращения к ним через TransparentProxy. Все эти решения по-своему хороши, как и не лишены недостатков по отдельности. Первое предполагает написание приличного объема кода, в противном случае решение окажется не универсальным, а второе накладывает заведомые ограничения и не может считаться «производительным» решением.

    Помечтаем еще? Вот если бы … была бы библиотека, которая совмещает оба подхода, да еще дает, что нибудь в довесок … и если бы имела открытый исходный код, да обладала документацией свойственной платным аналогам и была бы написана сотрудниками Microsoft … Фантастика?! Unity нам предлагает все это за бесплатно! Сверх того, как было упомянуто выше, это лишь только часть ее возможностей. Вот об этой самой части мы с Вами будем говорить подробней.

    Что нам понадобится? Во первых загрузить Unity: http://www.microsoft.com/downloads/details.aspx?FamilyId=2C8B79E7-AE56-4F90-822E-A1E43C49D12E&displaylang=en
    Во вторых разобраться с основными принципами работы.

    Контейнер Unity имеет соответствующие настройки которые позволяют указать ему, что мы хотим воспользоваться его возможностями по перехвату обращений к объекту. После чего мы инстанцируем объекты посредством Unity, и вызываем их соответствующие методы. В действительности Unity возвращает прокси объект который перехватывает все обращения к действительному экземпляру класса и перенаправляет их в специальный обработчик написанный нами. В примере с классом Printer это может быть код, который проверяет параметры метода на Null и генерирует соответствующие исключение. В дополнение Unity предлагает поддержку атрибутов, которые позволят нам связать обработчик с методами, требующими соответствующую проверку. Перед демонстрацией примеров использования я должен упомянуть, что существует два типа перехватчиков (объекты создающие прокси) обращений, первый основан на том, что прокси объект выводится на основе экземпляра объекта, а второй на основе его типа. Что бы лучше понять разницу можно вспомнить как .NET Framework создает TransperantProxy. Для того, что бы инфраструктура создала прокси объект ей требуется ссылка на экземпляр. Примером прокси основанного на типе, может быть реализация NHibernate, который требует от нас объявлять свойства объектов который мы будем мапить виртуальными. Это необходимо для того, что бы библиотека могла динамически создать прокси объект путем переопределения соответствующих виртуальных свойств.

    Unity поддерживает оба типа перехватчиков. В нашем с вами распоряжении имеется два экземплярных перехватчика и один перехватчик на основе типов.
    Приступим к примерам.

    TransparentProxyInterceptor - первый тип перехватчика

    Воспользуемся для примера классом Printer, приведенным выше:

    ExpandedWrap disabled
          public class Printer
          {
              public void Print(string line)
              {
                  Console.WriteLine(line);
                  Console.WriteLine("Was written {0} symbols", line.Length);
              }
          }


    В этом сценарии Unity создаст прокси, путем взаимодействия с инфраструктурой .NET Transparent/RealProxy. Его можно использовать если тип наследует MarshalByRefObject (ContextBoundObject) или же когда перехватываются только методы класса реализующие соответствующий интерфейс. Сразу рассмотрим первый случай.

    ExpandedWrap disabled
          public class Printer : ContextBoundObject
          {
              public void Print(string line)
              {
                  Console.WriteLine(line);
                  Console.WriteLine("Was written {0} symbols", line.Length);
              }
          }


    Теперь нам необходимо написать обработчик. Тут все просто нам нужен класс который реализует ICallHandler интерфейс. Ниже пример реализации такого обработчика. В нашем случае он должен проверить параметры на null и в случае необходимости сгенерировать исключение.

    ExpandedWrap disabled
                  public class NullControlHandler : ICallHandler
          {
              public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
              {
                  for (int i = 0; i < input.Arguments.Count; ++i)
                  {
                      if (input.Arguments[i] == null)
                      {
                          ParameterInfo parameterInfo = input.Arguments.GetParameterInfo(i);
                          ArgumentNullException ex = new ArgumentNullException(parameterInfo.Name);
                          return input.CreateExceptionMethodReturn(ex);
                      }
                  }
       
                  return getNext()(input, getNext);
              }
       
              public int Order
              {
                  get;
                  set;
              }
          }


    При обращению к методу Print прокси объект делегирует свою работу соответствующему обработчику. Из парамметров метода мы может получить всю необходимую информацию об аргументах. Т.к. обработчиков может быть много, напрмиер один проверяющий аргументы на null, а второй производящий логирование соответствующего метода, нам нужно вызывать их по цепочке, пока мы не получим результат выполнения который будет возвращен прокси и преобразован им в результат вызова метода. Если не говорить заумно, то до строки getNext()(input, getNext); мы можем писать код который должен выполниться перед непосредственным вызовом метода с аргументами (в нашем случае организовать проверки и при надобности сгенерировать исключение) или модифицировать парамметры вобщем любую необходимую работу, а после вызова метода мы вольны модифицировать результат (или просто вернуть его), выполнить нужную нам работу (к примеру записать в лог сообщение о том, что метод был успешно вызыван и.т.д.).

    Реализуем аттрибут. Нет ничего тривиальней:

    ExpandedWrap disabled
          public class NullControlAttribute : HandlerAttribute
          {
              public override ICallHandler CreateHandler(IUnityContainer container)
              {
                  return new NullControlHandler();
              }
          }


    Переопределяем метод CreateHandler возвращая соответствующий экземпляр.

    Осталось применить аттрибут к методу Print:

    ExpandedWrap disabled
              [NullControl]
              public void Print(string line)
              {
                  Console.WriteLine(line);
                  Console.WriteLine("Was written {0} symbols", line.Length);
              }


    После этого все готово к тестированию:

    ExpandedWrap disabled
             class Program
          {
              static void Main(string[] args)
              {
                  // Создаем контейнер
                  IUnityContainer container = new UnityContainer();
       
                  // Регистрируем тип Printer, указываем Unity, хотим использовать перехватчик
                  // Указываем, что для получения прокси для Printer собираемся использовать TransparentProxyInterceptor
                  container.RegisterType<Printer>();
                  container.AddNewExtension<Interception>()
                      .Configure<Interception>()
                      .SetInterceptorFor<Printer>(new TransparentProxyInterceptor());
       
                  // Получаем прокси
                  Printer printer = container.Resolve<Printer>();
       
                  // Наслаждаемся результатом
                  printer.Print(null);
              }
          }


    Цитата
    Результат:

    Unhandled Exception: System.ArgumentNullException: Value cannot be null.
    Parameter name: line


    Тут можно заметить, что всю настройку мы можем выполнять в конфигурационных файлах, но конфигурирование Unity в DisignTime тема отдельной статьи.

    Рассмотрим второй сценарий применения TransparentProxyIntercepotor. Предположим, что у нас есть класс вида:

    ExpandedWrap disabled
          public class Printer : IPrinter
          {
              [NullControl]
              public void Print(string line)
              {
                  Console.WriteLine(line);
                  Console.WriteLine("Was written {0} symbols", line.Length);
              }
          }


    , где

    ExpandedWrap disabled
          public interface IPrinter
          {
              void Print(string line);
          }


    Немного изменив реализацию метода Main мы добъемся того, что сможем использовать TransparentProxyInterceptor для перехвата обращений к методам являющимися реализацией интерфейса IPrinter.

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

    InterfaceInterceptor
    Рассмотрим еще один перехватчик предоставляемый нам Unity. Это еще один перехватчик на основе экземпляров, но существенным отличием от упомянутого TransparentProxyInterceptor является то, что он для построения прокси использует динамическую генерацию кода. Его удобно использовать в тех случаях когда мы мапим типы на интерфейсы с использованием Unity, так как мы делали в во втором примере для TransparentProxyInterceptor. Код будет отличаться только типом перехватчика указываемого Unity, при настройке. Для удобства я привожу полный код примера ниже:

    ExpandedWrap disabled
      using System;
      using System.Collections.Generic;
      using System.Linq;
      using System.Text;
      using System.Reflection;
      using Microsoft.Practices.Unity;
      using Microsoft.Practices.Unity.InterceptionExtension;
       
      namespace InterfaceInterceptorSample
      {
          public interface IPrinter
          {
              void Print(string line);
          }
       
          public class Printer : IPrinter
          {
              [NullControl]
              public void Print(string line)
              {
                  Console.WriteLine(line);
                  Console.WriteLine("Was written {0} symbols", line.Length);
              }
          }
       
          public class NullControlHandler : ICallHandler
          {
              public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
              {
                  for (int i = 0; i < input.Arguments.Count; ++i)
                  {
                      if (input.Arguments[i] == null)
                      {
                          ParameterInfo parameterInfo = input.Arguments.GetParameterInfo(i);
                          ArgumentNullException ex = new ArgumentNullException(parameterInfo.Name);
                          return input.CreateExceptionMethodReturn(ex);
                      }
                  }
       
                  return getNext()(input, getNext);
              }
       
              public int Order
              {
                  get;
                  set;
              }
          }
       
          public class NullControlAttribute : HandlerAttribute
          {
              public override ICallHandler CreateHandler(Microsoft.Practices.Unity.IUnityContainer container)
              {
                  return new NullControlHandler();
              }
          }
       
          class Program
          {
              static void Main(string[] args)
              {
                  IUnityContainer container = new UnityContainer();
                  container.RegisterType<IPrinter, Printer>();
                  container.AddNewExtension<Interception>()
                      .Configure<Interception>()
                      .SetInterceptorFor<IPrinter>(new InterfaceInterceptor());
       
                  IPrinter printer = container.Resolve<IPrinter>();
                  printer.Print(null);
              }
          }
      }


    Цитата
    Результат:

    Unhandled Exception: System.ArgumentNullException: Value cannot be null.
    Parameter name: line



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

    VirtualMethodInterceptor
    Третий тип перехватчика, как и для InterfaceInterceptor, используется динамическая генерация кода, тем не менее он строит прокси отталкиваясь от типа. Этот перехватчик перехватывает обращения к виртуальным методам, что накладывает на нас требования делать все методы которые мы желаем перехватить виртуальными.

    ExpandedWrap disabled
         public class Printer
          {
              [NullControl]
              public virtual void Print(string line)
              {
                  Console.WriteLine(line);
                  Console.WriteLine("Was written {0} symbols", line.Length);
              }
          }


    Настройка перехватчика подобна первому примеру для TransparentProxyIntercepotor:

    ExpandedWrap disabled
                  IUnityContainer container = new UnityContainer();
                  container.RegisterType<Printer>();
                  container.AddNewExtension<Interception>()
                      .Configure<Interception>()
                      .SetInterceptorFor<Printer>(new VirtualMethodInterceptor());


    После вызова метода Print c нулевым аргументом мы увидим уже знамое, по предыдущим примерам сообщение.

    В заключение прошу заметить, что использование обработчиков Unity не ограничивается только обработкой исключений. Вы можете реализовать с их помощью любой «сквозной» функционал в вашей программе. Это может быть логирование или преобразование возвращаемых методом результатов, обработка транзакций или встаивание инструментального кода.

    В следующий раз я обязательно напишу и о других замечательных особеностях Unity контейнера о реализации паттернов Inversion of Сontrol и DependencyInjection в ваших приложениях.

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

      Первый вопрос прозвучал примерно так: А что если я не хочу создавать точки перехвата посредством аттрибутов? Или я хочу сам сконфигурировать перехват для дебаг версии, а для релиза отключить? Использование потенциально мешает этому. Ответ заключается в том, что можно сконфигурировать политику перехвата обойдясь при этом вообще без использования аттрибутов. Ниже пример runtime настройки для TransparentProxyInterceptor (можно использовать второй пример где используется интерфейс в связке с TransparentProxyInterceptor ):

      ExpandedWrap disabled
                    IUnityContainer container = new UnityContainer();                   // создаем контейнер
                    container.RegisterType<IPrinter, Printer>()                         // мапим IPrinter на Printer
                        .AddNewExtension<Interception>()                                // сообщаем контейнеру о том, что собираемся пользоваться соответствующим расширением
                        .Configure<Interception>()                                      // разрешаем зависимость на расширения для последующей настройки
                        .SetInterceptorFor<IPrinter>(new TransparentProxyInterceptor()) // устанавливаем тип перехватчика для IPrinter
                        .AddPolicy("printerPolicy")                                     // добавляем новую политику
                        .AddCallHandler(typeof(NullControlHandler))                     // ассоциируем с политикой наш обработчик (политика может содержать множество обработчиков)
                        .AddMatchingRule(new TypeMatchingRule(typeof(IPrinter)));       // добавляем правило согласно с которым для типов которые разрешаются через IPrinter, будет применяться соответствующая политика
                        
                        IPrinter printer = container.Resolve<IPrinter>();
                        printer.Print(null);


      Можем смело удалить аттрибут с метода Print и запустить пример. :)

      Другим вопросом было: Можно ли применять аттрибут не к методу, а к классу? Можно. Равноценно тому, что каждый метод класса был бы помечен аттрибутом.

      Третий вопрос: Для всего ли можно использовать конфигурацию в DesignTime вместо RunTime? Мне кажется, что да :) Покрайней мере я планирую описать процесс конфигурирования Unity в одной из последующих статей.

      Четвертый вопрос: Он плавно вытекает из первого. Можно ли цеплять обработчики не только по типу? Ответ: Можно. Unity предлагает набор предопределенных правил сопоставления по: сборке, аттрибуту, имени члена, сигнатуре метода, пространству имен, типу параметра, свойству, возвращаемому значению, по аттрибуту Tag, типу (TypeMatchingRule использовался при ответе на вопрос 1). Для всех "специальных" ситуаций Unity предлагает создавать кастомные правила посредством реализации IMatchingRule.
      Ни один победитель не верит в случайность.
        juice
        Привет, я наверное уже не первый, но все таки хочу поблагодарить Тебя(даж зарегился для этого :rolleyes:) за эту статью:

        "Unity Framework, как реализация подмножества AOП"

        очень доходчивое объяснение.
        Большое-большое Спасибо!

        Обязательно почитаю все Твои статьи!
        :good:
        0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
        0 пользователей:


        Рейтинг@Mail.ru
        [ Script Execution time: 0,3830 ]   [ 17 queries used ]   [ Generated: 23.09.17, 11:03 GMT ]