На переднем крае

Аспектно-ориентированное программирование, перехват и Unity 2.0

Дино Эспозито

image: Dino EspositoНесомненно, ориентация на объекты является «мейнстримовой» парадигмой программирования, которая отлично показывает себя, когда систему нужно разбить на компоненты и описывать процессы в терминах компонентов. Объектно-ориентированная (ОО) парадигма также превосходит другие, если вы имеете дело с задачами компонента, специфичными для бизнес-логики. Однако парадигма OO не столь эффективна, когда дело доходит до задач, горизонтально пересекающих иерархию (cross-cutting concerns). В целом, такая задача влияет на несколько компонентов в системе.

Чтобы максимизировать степень повторного использования сложного кода бизнес-логики, вы, как правило, создаете иерархию классов вокруг базовых функций системы, относящихся главным образом к бизнес-логике. Но что делать с другими задачами, не имеющими отношения к бизнес-логике, но требующими горизонтального пересечения иерархии классов? Где размещать такую функциональность, как кеширование, защиту и протоколирование? Весьма вероятно, что в конечном счете их придется повторять в каждом объекте, для которого нужна подобная функциональность.

Не будучи специфической ответственностью конкретного компонента или семейства компонентов, задача, горизонтально пересекающая иерархию, является аспектом системы, с которым нужно работать на другом логическом уровне — вне рамок прикладных классов. По этой причине довольно давно была определена другая парадигма: аспектно-ориентированное программирование (aspect-oriented programming, AOP). Кстати, концепция AOP была разработана в лабораториях Xerox PARC в 90-х годах прошлого века. Та же группа создала и первый (до сих пор популярный) AOP-язык: AspectJ.

Хотя почти все соглашаются с преимуществами AOP, оно до сих пор не получило широкого распространения. На мой взгляд, основная причина этого заключается главным образом в том, что отсутствуют необходимые инструментальные средства. Я совершенно уверен, что тот день, когда в Microsoft .NET Framework появится поддержка AOP (хотя бы частичная), станет переломным в истории AOP. Сегодня вы можете использовать AOP в .NET только с применением специфических инфраструктур.

Самое мощное средство для AOP в .NET — PostSharp, его можно найти по адресу: sharpcrafters.com. PostSharp предоставляет полную инфраструктуру AOP, где можно использовать все основные концепции теории AOP. Однако следует отметить, что некоторые возможности AOP поддерживают многие инфраструктуры введения зависимостей (dependency injection, DI).

Например, средства AOP есть в Spring.NET, Castle Windsor и, конечно, в Microsoft Unity. Для решения относительно простых задач, таких как трассировка, кеширование и дополнение компонентов на прикладном уровне, возможностей DI-инфраструктур обычно хватает. Но, когда дело доходит до объектов предметной области и UI-объектов, работать с DI-инфраструктурами становится проблематично. Задачу, горизонтально пересекающую иерархию, определенно можно рассматривать как внешнюю зависимость, и методики, принятые в DI-инфраструктурах, безусловно, позволяют вводить внешние зависимости в некий класс.

Проблема в том, что DI скорее всего потребует специфической, заранее продуманной архитектуры или рефакторинга. Иначе говоря, если вы уже используете DI-инфраструктуру, то внести в код некоторые средства AOP будет несложно. И наоборот, если в вашей системе нет DI, применение DI-инфраструктуры может потребовать немалой работы. Такое не всегда возможно в крупных проектах или при обновлении унаследованной (устаревшей) системы. При классическом подходе AOP вы вместо этого обертываете любые задачи, горизонтально пересекающие иерархию, в новый компонент, называемый аспектом (aspect). В этой статье я сначала дам краткий обзор аспектно-ориентированной парадигмы, а затем перейду к рассмотрению AOP-возможностей в Unity 2.0.

Краткий обзор AOP

Проект на основе объектно-ориентированной парадигмы состоит из нескольких файлов исходного кода, в каждом из которых реализуется один или более классов. Проект также включает классы, представляющие задачи, горизонтально пересекающие иерархию, например протоколирование или кеширование. Все классы обрабатываются компилятором, и на выходе создается исполняемый код. В AOP аспект — это повторно используемый компонент, который инкапсулирует поведение, необходимое множеству классов в проекте. Как именно обрабатываются аспекты, зависит от конкретной технологии AOP. В целом, можно сказать, что аспекты не обрабатываются компилятором напрямую. В этом случае требуется дополнительный инструмент, специфичный для конкретной технологии, который модифицирует исполняемый код, принимая во внимание аспекты. Давайте кратко рассмотрим, что происходит в случае AspectJ — Java-компилятора AOP, который был первым AOP-инструментом.

В AspectJ вы пишете свои классы на языке программирования Java, а аспекты — на языке AspectJ. Последний поддерживает собственный синтаксис, с помощью которого вы указываете ожидаемое поведение аспекта. Например, в аспекте протоколирования можно задать, что запись в журнал будет осуществляться до и после вызова определенного метода. Потом аспекты неким способом включаются в обычный исходный код, и создается промежуточная версия исходного кода, которая в конечном счете компилируется в исполняемый формат. На жаргоне AspectJ компонент, который предварительно обрабатывает аспекты (препроцессор) и объединяет их с исходным кодом, называется ткачом (weaver). Он формирует вывод, который компилятор преобразует в исполняемый код.

Таким образом, аспект описывает повторно используемый кусок кода, который вам нужно встроить в существующие классы, не трогая их исходный код. В других инфраструктурах AOP (например в .NET PostSharp) вы не найдете инструмента-ткача. Однако содержимое аспекта всегда обрабатывается инфраструктурой и в результате приводит к какой-либо форме встраивания кода.

В связи с этим заметьте, что встраивание кода отличается от введения зависимости. Встраивание кода относится к способности инфраструктуры AOP к вставке вызовов в открытых конечных точках аспекта в определенные точки в теле классов, дополняемых данным аспектом. Например, инфраструктура PostSharp позволяет писать аспекты как .NET-атрибуты, которые вы потом подключаете к методам своих классов. Атрибуты PostSharp обрабатываются компилятором PostSharp (его даже можно было бы назвать ткачом) на этапе после компиляции основного кода. Результат заключается в том, что ваш код расширяется для включения некоторого кода, содержащегося в атрибутах. Но точки встраивания разрешаются автоматически, а от разработчика требуется лишь написать самодостаточный компонент-аспект и подключить его к открытому методу класса. Писать такой код легко, а поддерживать еще легче.

Чтобы покончить с кратким обзором AOP, позвольте мне представить несколько специфических терминов и пояснить их смысл. Точка слияния (join point) указывает точку в исходном коде целевого класса, куда вам нужно встроить код аспекта. Срез точек (pointcut) представляет набор точек слияния. Подача (advice) относится к коду, встраиваемому в целевой класс. Код можно вставлять до, после и вокруг точки слияния. Подача сопоставляется со срезом точек. Эти термины взяты из исходного определения AOP и могут не соответствовать терминологии конкретной инфраструктуры AOP, используемой вами. Рекомендуется освоить концепции, стоящие за этими терминами (основополагающими для AOP), а затем использовать это знание, чтобы лучше разбираться в деталях конкретной инфраструктуры.

Краткий обзор Unity 2.0

Unity — это блок приложения, доступный как часть проекта Microsoft Enterprise Library, а также в отдельном виде. Microsoft Enterprise Library — это набор блоков приложения, которые снимают часть проблем, связанных с горизонтальным пересечением иерархии и характерных в разработке .NET-приложений (протоколирование, кеширование, шифрование, обработка исключений и др.). Версия новейшей Enterprise Library — 5.0, она выпущена в апреле 2010 г. и полностью поддерживает Visual Studio 2010 (подробности см. на сайте Patterns & Practices Developer Center по ссылке msdn.microsoft.com/library/ff632023).

Unity — один из блоков приложения Enterprise Library. Unity, также доступный для Silverlight, в основном представляет собой DI-контейнер с дополнительной поддержкой механизма перехвата, благодаря которому вы можете делать свои классы чуть более аспектно-ориентированными.

Перехват в Unity 2.0

Основная концепция перехвата в Unity позволяет разработчикам настраивать цепочку вызовов, необходимых для запуска какого-либо метода некоего объекта. Иначе говоря, механизм перехвата Unity захватывает вызовы, выдаваемые для настройки объектов, и изменяет поведение целевых объектов, добавляя дополнительный код до, после и вокруг обычного кода методов. Перехват — фактически очень гибкий подход к добавлению нового поведения для объекта в период выполнения, не затрагивающий его исходный код и не влияющий на поведение классов в той же цепочке наследования. Перехват в Unity — способ реализации популярного проектировочного шаблона Decorator, разработанного для расширения функциональности объекта в период выполнения и в момент его использования. Декоратор (decorator) — это объект-контейнер, который принимает (и поддерживает ссылку на) экземпляр целевого объекта и дополняет его возможности.

Механизм перехвата в Unity 2.0 поддерживает перехват как экземпляра, так и типа. Более того, перехват работает независимо от того, как создается экземпляр объекта — через контейнер Unity или как известный экземпляр. В последнем случае вы можете использовать другой, полностью автономный API. Однако тогда вы теряете поддержку конфигурационных файлов. На рис. 1 показана архитектура механизма перехвата в Unity с детализацией того, он работает применительно к конкретному экземпляру объекта, который разрешается не через контейнер. (Эта иллюстрация является слегка переработанной версией аналогичной иллюстрации в документации MSDN.)

image: Object Interception at Work in Unity 2.0

Рис. 1. Перехват объекта в действии (в Unity 2.0)

Подсистема перехвата состоит из трех основных элементов: перехватчика (или прокси), конвейера поведений (behavior pipeline) и поведения или аспекта. На противоположных границах подсистемы вы обнаружите клиентское приложение и целевой объект, т. е. объекту назначается дополнительное поведение, не «зашитое» в его исходный код. Как только клиентское приложение конфигурируется на использование API перехвата из Unity применительно к определенному экземпляру, любой вызов метода проходит через прокси-объект — перехватчик. Этот прокси-объект просматривает список зарегистрированных поведений и вызывает их через внутренний конвейер. Каждому сконфигурированному поведению дается шанс на выполнение до или после обычного вызова метода объекта. Прокси вводит входные данные в конвейер и принимает любое возвращаемое значение как изначально сгенерированное целевым объектом, а затем модифицированное имеющимися поведениями.

Настройка перехвата

Рекомендованный способ использования перехвата в Unity 2.0 отличается от того, что был рекомендован в более ранних версиях, хотя подход, применяемый в этих версиях, по-прежнему полностью поддерживается для обратной совместимости. В Unity 2.0 перехват реализуется простым добавлением нового расширения к контейнеру, чтобы описать, как будет разрешаться объект. Вот код, который понадобится вам, если вы захотите сконфигурировать перехват через динамический код:

var container = new UnityContainer();
container.AddNewExtension<Interception>();

Контейнеру нужно найти информацию о перехватываемых типах и добавляемых поведениях.Эту информацию можно добавить, используя либо динамический код (fluent code), либо конфигурационный файл. На мой взгляд конфигурационный файл обеспечивает более высокую гибкость, так как позволяет вносить изменения, не трогая приложение и обходясь без дополнительного этапа компиляции. Давайте последуем подходу на основе конфигурационного файла.

Для начала вы должны добавить в конфигурационный файл:

<sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.
  Configuration.InterceptionConfigurationExtension, 
  Microsoft.Practices.Unity.Interception.Configuration"/>

Цель этого сценарного кода — расширение схемы конфигурации новыми элементами и псевдонимами, специфичными для подсистемы перехвата. Еще одно добавление выглядит так:

<container> 
  <extension type="Interception" /> 
  <register type="IBankAccount" mapTo="BankAccount"> 
    <interceptor type="InterfaceInterceptor" /> 
    <interceptionBehavior type="TraceBehavior" /> 
  </register> 
</container>

Чтобы добиться того же с помощью динамического кода, вам пришлось бы вызвать AddNewExtension<T> и RegisterType<T> объекта-контейнера.

Давайте повнимательнее рассмотрим конфигурационный сценарий. Элемент <extension> добавляет в контейнер перехватчик. Заметьте, что «Interception», используемый в этом сценарии, является одним из псевдонимов, определенных в разделе расширения. Тип интерфейса IBankAccount преобразуется в конкретный тип BankAccount (это классическая работа DI-контейнера) и сопоставляется с нужным типом перехватчика. Unity предоставляет два основных типа перехватчиков: экземпляра (instance interceptors) и типа (type interceptors). В следующей статье перехватчики будут рассмотрены подробнее. А пока достаточно сказать, что перехватчик экземпляра создает прокси, который фильтрует входящие вызовы, адресованные перехватываемому экземпляру. Перехватчики типов просто имитируют тип перехватываемого объекта и работают с экземпляром производного типа. (Подробнее о перехватчиках см. по ссылке msdn.microsoft.com/library/ff660861(PandP.20).)

Перехватчик интерфейса (interface interceptor) — это перехватчик экземпляра, ограниченный в своих действиях до прокси только одного интерфейса объекта. Такой перехватчик создает класс прокси с помощью генерации динамического кода. Элемент поведения interception в конфигурации указывает внешний код, который должен выполняться вокруг перехватываемого экземпляра объекта. Класс TraceBehavior нужно конфигурировать декларативно, чтобы контейнер мог разрешать его и любые его зависимости. Чтобы сообщить контейнеру о классе TraceBehavior и его конструкторе вы используете элемент <register>:

<register type="TraceBehavior"> 
   <constructor> 
     <param name="source" dependencyName="interception" /> 
   </constructor> 
</register>

Фрагмент класса TraceBehavior приведен на рис. 2.

Рис. 2. Пример Unity-поведения

class TraceBehavior : IInterceptionBehavior, IDisposable
{
  private TraceSource source;

  public TraceBehavior(TraceSource source)
  {
    if (source == null) 
      throw new ArgumentNullException("source");

    this.source = source;
  }
   
  public IEnumerable<Type> GetRequiredInterfaces()
  {
    return Type.EmptyTypes;
  }

  public IMethodReturn Invoke(IMethodInvocation input, 
    GetNextInterceptionBehaviorDelegate getNext)
  {
     // BEFORE the target method execution 
     this.source.TraceInformation("Invoking {0}",
       input.MethodBase.ToString());

     // Yield to the next module in the pipeline
     var methodReturn = getNext().Invoke(input, getNext);

     // AFTER the target method execution 
     if (methodReturn.Exception == null)
     {
       this.source.TraceInformation("Successfully finished {0}",
         input.MethodBase.ToString());
     }
     else
     {
       this.source.TraceInformation(
         "Finished {0} with exception {1}: {2}",
         input.MethodBase.ToString(),
         methodReturn.Exception.GetType().Name,
         methodReturn.Exception.Message);
     }

     this.source.Flush();
     return methodReturn;
   }

   public bool WillExecute
   {
     get { return true; }
   }

   public void Dispose()
   {
     this.source.Close();
   }
 }

Класс поведения реализует IInterceptionBehavior, который в основном состоит из метода Invoke. Этот метод содержит всю логику, нужную для любого метода, который находится под контролем перехватчика. Если вы хотите сделать что-то до вызова целевого метода, то делаете это в начале метода. Когда вам требуется перейти к целевому объекту (или, точнее, к следующему поведению, зарегистрированному в конвейере), вы вызываете делегат getNext, предоставляемый инфраструктурой. Наконец, вы можете использовать любой код для постобработки целевого объекта. Метод Invoke должен возвращать ссылку на следующий элемент в конвейере; если он возвращает null, цепочка прерывается и остальные поведения не вызываются.

Гибкость конфигурирования

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

В этой статье я лишь поверхностно рассмотрел AOP применительно к .NET. В следующих статьях я подробнее расскажу о перехвате в Unity и вообще об AOP.

Дино Эспозито (Dino Esposito) — автор книги «Programming ASP.NET MVC» (Microsoft Press, 2010) и соавтор «Microsoft .NET: Architecting Applications for the Enterprise» (Microsoft Press, 2008). Проживает в Италии и часто выступает на отраслевых мероприятиях по всему миру. С ним можно связаться через его блог weblogs.asp.net/despos.

Выражаю благодарность за рецензирование этой статьи эксперту: Крис Таварес (Chris Tavares)