Поделиться через


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

Встраивание политики в Unity

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

В прошлых двух статьях я представил аспектно-ориентированное программирование (AOP) с применением Microsoft Unity 2.0. AOP, формализованное в 90-е годы прошлого века как один из вариантов расширения объектно-ориентированного программирования, было недавно пересмотрено и обновлено, и теперь его поддерживают многие библиотеки инверсии управления (Inversion of Control, IoC). И Unity не является исключением. Основная цель AOP — дать возможность разработчикам эффективнее справляться с задачами, горизонтально пересекающими иерархию (crosscutting concerns). По сути, AOP отвечает на следующий вопрос: как при проектировании объектной модели приложения принимать во внимание такие аспекты кода, как безопасность, кеширование или протоколирование? Эти аспекты являются ключевыми в реализации, но, строго говоря, не относятся к объектам в создаваемой вами модели. Стоит ли портить дизайн включением аспектов, не относящихся к бизнес-задачам? Или лучше дополнить бизнес-ориентированные классы дополнительными аспектами? Если вы выбираете второй вариант, AOP предоставляет синтаксис для определения и подключения таких аспектов.

Аспект (aspect) — это реализация задачи, горизонтально пересекающей иерархию. В определении аспекта нужно указать ряд вещей. Во-первых, вам нужно предоставить код данного аспекта для соответствующей задачи. На жаргоне AOP, такой код называется подачей (advice). Подача применяется к определенной точке кода — телу метода, аксессорам get/set свойства или, возможно, к обработчику исключений. Такая точка называется точкой слияния (join point). Наконец, на том же жаргоне AOP, вы находите точки среза. Точка среза (pointcut) представляет набор точек слияния. Обычно точки среза определяются по критерию с использованием имен методов и подстановочных символов. В конечном счете исполняющая среда AOP встраивает код подачи до, после и вокруг точки слияния. После этого подача сопоставляется с точкой среза.

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

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

Как мы видели в прошлом месяце, все перехватываемые методы целевого объекта будут выполняться согласно логике, выраженной в методе Invoke объекта поведения. Базовый API перехвата не позволяет различать методы и не поддерживает специфические правила соответствия (matching rules). Чтобы добиться своей цели, вы можете прибегнуть к API встраивания политики (policy injection API).

Встраивание политики и PIAB

Если вы пользовались версиями Microsoft Enterprise Library (EntLib) до выхода новейшей версии — 5.0, то, вероятно, слышали о таком модуле, как Policy Injection Application Block (PIAB), и все шансы за то, что вы также использовали его преимущества в некоторых из своих приложений. В EntLib 5.0 по-прежнему имеется модуль PIAB. Так в чем же разница между встраиванием политики в Unity и EntLib PIAB?

В EntLib 5.0 модуль PIAB существует в основном по соображениям совместимости. Содержимое сборки PIAB в новой версии изменено. В частности, вся начинка механизма перехвата теперь является частью Unity, а все предоставляемые на системном уровне обработчики вызовов в ранних версиях EntLib перемещены в другие сборки, как показано в табл. 1.

Табл. 1. Рефакторинг обработчиков вызовов в Microsoft Enterprise Library 5.0

Обработчик вызова Новая сборка в Enterprise Library 5.0
Обработки авторизации Microsoft.Practices.EnterpriseLibrary.Security.dll
Обработки кеширования Удален из PIAB
Обработки исключений Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.dll
Обработки протоколирования Microsoft.Practices.EnterpriseLibrary.Logging.dll
Обработки счетчиков производительности Microsoft.Practices.EnterpriseLibrary.PolicyInjection.dll
Обработки проверки на соответствие Microsoft.Practices.EnterpriseLibrary.Validation.dll

Как видно из табл. 1, каждый обработчик вызова перемещен в сборку соответствующего блока приложения (application block). Поэтому обработчик вызова обработки исключений перемещен в блок приложения, отвечающий за обработку исключений, а обработчик вызова обработки проверки — в блок приложения, относящийся к таким операциям, и т. д. Единственным исключением из этого правила был обработчик счетчиков производительности, который перенесли в сборку PolicyInjection. Хотя сборки изменились, пространство имен классов осталось тем же. Также стоит отметить, что по соображениям безопасности обработчик вызова обработки кеширования, ранее включенный в PIAB, был удален из EntLib 5.0 и стал доступен только на сайте EntLib Contrib CodePlex по ссылке bit.ly/gIcP6H. Итоговый эффект от этих изменений заключается в том, что теперь PIAB состоит из устаревших компонентов, которые доступны только для обратной совместимости и которые все равно требуют некоторых изменений в коде для компиляции с версией 5.0. Если у вас в коде нет обширных зависимостей от этих устаревших компонентов, рекомендуется обновить свои уровни встраивания политики для использования преимуществ нового (и, в общем-то, аналогичного) API встраивания политики, помещенного в блоки приложения Unity. Давайте подробнее рассмотрим встраивание политики в Unity.

Базовые сведения о встраивании политики

Код встраивания политики представляет собой уровень, расширяющий базовый API перехвата в Unity для добавления правил сопоставления (mapping rules) и вызова обработчиков индивидуально для каждого метода. Реализованный в виде специального поведения перехвата, этот уровень состоит из двух частей: периода инициализации и времени выполнения.

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

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

Обработчики вызовов

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

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

Любопытно, что в Unity нет готовых обработчиков вызовов. Можно лишь создавать свои обработчики или ссылаться на блоки приложения из EntLib 5.0 и использовать любой из обработчиков, перечисленных в табл. 1.

Обработчик вызова — это класс, реализующий интерфейс ICallHandler:

public interface ICallHandler
{
  IMethodReturn Invoke(

    IMethodInvocation input, 

    GetNextHandlerDelegate getNext);
  int Order { get; set; }
}

Свойство Order задает приоритет этого обработчика по отношению ко всем остальным. Метод Invoke возвращает экземпляр класса, который содержит любое значение, возвращаемое методом.

Реализация обработчика вызова довольно проста в том смысле, что от него ожидается выполнение своих задач и передача управления следующему обработчику в конвейере. Для передачи управления обработчик вызывает параметр getNext, полученный им от исполняющей среды Unity. Параметр getNext является делегатом, определенным так:

public delegate InvokeHandlerDelegate GetNextHandlerDelegate();

В свою очередь, InvokeHandlerDelegate определяется следующим образом:

public delegate IMethodReturn InvokeHandlerDelegate(

  IMethodInvocation input,

  GetNextHandlerDelegate getNext);

В документации Unity дана четкая схема, иллюстрирующая процесс перехвата. На рис. 2 показана слегка измененная схема, представляющая архитектуру встраивания политики.

image: The Call Handler Pipeline in the Unity Policy Injection

Рис. 2. Конвейер обработчиков вызовов в инфраструктуре встраивания политики в Unity

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

Правила соответствия

С помощью правила соответствия вы указываете, где именно применяется ваша логика перехвата. При использовании поведений этот код применяется к объекту в целом, а с помощью одного или более правил соответствия можно определить фильтр. Правило соответствия (matching rule) задает критерий выбора объектов и членов, к которым Unity будет подключать конвейер обработчиков. В терминологии AOP, правило соответствия является критерием, применяемым при определении точек среза. В табл. 3 перечислены правила соответствия, изначально поддерживаемые Unity.

Табл. 3. Список поддерживаемых правил соответствия в Unity 2.0

Правило соответствия Описание
AssemblyMatchingRule Выбирает целевые объекты на основе типов в указанной сборке
CustomAttributeMatchingRule Выбирает целевые объекты на основе собственного атрибута на уровне членов
MemberNameMatchingRule Выбирает целевые объекты на основе имени члена
MethodSignatureMatchingRule Выбирает целевые объекты на основе сигнатуры
NamespaceMatchingRule Выбирает целевые объекты на основе пространства имен
ParameterTypeMatchingRule Выбирает целевые объекты на основе имени типа параметра для члена
PropertyMatchingRule Выбирает целевые объекты на основе имен членов; допускается использование подстановочных символов
ReturnTypeMatchingRule Выбирает целевые объекты на основе типа возвращаемого значения
TagMatchingRule Выбирает целевые объекты на основе значения, присвоенного специальному атрибуту Tag
TypeMatchingRule Выбирает целевые объекты на основе имени типа

Правило соответствия — это класс, реализующий интерфейс IMatchingRule. Вооружившись этим знанием, посмотрим, как происходит встраивание политики. В целом, существует три способа определения политик: с помощью атрибутов, текучего кода и через конфигурацию.

Добавление политик и атрибуты

На рис. 4 показан пример обработчика вызова, который генерирует исключение, если результат операции оказывается отрицательным. Я буду использовать этот обработчик в разных сценариях.

Рис. 4. Класс NonNegativeCallHandler

public class NonNegativeCallHandler : ICallHandler
{
  public IMethodReturn Invoke(IMethodInvocation input,
                              GetNextHandlerDelegate getNext)
  {
    // Perform the operation
    var methodReturn = getNext().Invoke(input, getNext);
    // Method failed, go ahead
    if (methodReturn.Exception != null)
    return methodReturn;
    // If the result is negative, then throw an exception
    var result = (Int32) methodReturn.ReturnValue;
    if (result <0)
    {
      var exception = new ArgumentException("...");
      var response = input.CreateExceptionMethodReturn(exception);
      // Return exception instead of original return value
      return response;
    }
    return methodReturn;
  }
  public int Order { get; set; }
}

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

public class NonNegativeCallHandlerAttribute : HandlerAttribute
{
  public override ICallHandler CreateHandler(

    IUnityContainer container)
  {
    return new NonNegativeCallHandler();
  }
}

Вот пример класса Calculator, который дополняется политиками на основе атрибута:

public class Calculator : ICalculator 
{
  public Int32 Sum(Int32 x, Int32 y)
  {
    return x + y;
  }

  [NonNegativeCallHandler]
  public Int32 Sub(Int32 x, Int32 y)
  {
    return x - y;
  }
}

Результат заключается в том, что вызовы метода Sum выполняются как обычно независимо от возвращаемого значения, а вызовы метода Sub будут давать исключение при возврате отрицательного числа.

Использование текучего кода

Если вам не нравятся атрибуты, ту же логику можно выразить через API текучего кода (fluent API). В этом случае вам придется позаботиться о гораздо большем количестве деталей. Давайте посмотрим, как выразить идею насчет того, что нам нужно встраивать код только в методы, возвращающие Int32 и именуемые Sub.Для конфигурирования контейнера Unity используется API текучего кода (рис. 3).

Рис. 5. Текучий код для определения набора правил соответствия

public static UnityContainer Initialize()
{
  // Creating the container
  var container = new UnityContainer();
  container.AddNewExtension<Interception>();

  // Adding type mappings
  container.RegisterType<ICalculator, Calculator>(
    new InterceptionBehavior<PolicyInjectionBehavior>(),
    new Interceptor<TransparentProxyInterceptor>());

  // Policy injection
  container.Configure<Interception>()
    .AddPolicy("non-negative")
    .AddMatchingRule<TypeMatchingRule>(
      new InjectionConstructor(
        new InjectionParameter(typeof(ICalculator))))
    .AddMatchingRule<MemberNameMatchingRule>(
      new InjectionConstructor(
        new InjectionParameter(new[] {"Sub", "Test"})))
    .AddMatchingRule<ReturnTypeMatchingRule>(
      new InjectionConstructor(
        new InjectionParameter(typeof(Int32))))
    .AddCallHandler<NonNegativeCallHandler>(
      new ContainerControlledLifetimeManager(),
        new InjectionConstructor());

  return container;
}

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

Этот код приводит к тому, что любой конкретный тип, который реализует ICalculator (т. е. сконфигурирован на перехват и разрешается через Unity), будет отбирать двух потенциальных кандидатов для встраивания: методы Sub и Test. Однако после применения следующего правила соответствия «выживут» лишь те методы, которые возвращают значения типа Int32. А значит, метод Test будет исключен, так как он возвращает значение типа Double.

Добавление политик через конфигурацию

Наконец, ту же концепцию можно выразить с помощью конфигурационного файла. На рис. 6 показано необходимое содержимое раздела <unity>.

Рис. 6. Подготовка встраивания политики в конфигурационном файле

public class NonNegativeCallHandler : ICallHandler
{
  public IMethodReturn Invoke(IMethodInvocation input, 
                              GetNextHandlerDelegate getNext)
   {
     // Perform the operation  
     var methodReturn = getNext().Invoke(input, getNext);

     // Method failed, go ahead
     if (methodReturn.Exception != null)
       return methodReturn;

     // If the result is negative, then throw an exception
     var result = (Int32) methodReturn.ReturnValue;
     if (result <0)
     {
       var exception = new ArgumentException("...");
       var response = input.CreateExceptionMethodReturn(exception);

       // Return exception instead of original return value
       return response;   
     }

     return methodReturn;
   }

    public int Order { get; set; }
}
<unity xmlns="https://schemas.microsoft.com/practices/2010/unity">
  <assembly name="PolicyInjectionConfig"/>
  <namespace name="PolicyInjectionConfig.Calc"/>
  <namespace name="PolicyInjectionConfig.Handlers"/>

  <sectionExtension  ... />

  <container>
    <extension type="Interception" />

    <register type="ICalculator" mapTo="Calculator">
      <interceptor type="TransparentProxyInterceptor" />
      <interceptionBehavior type="PolicyInjectionBehavior" />
    </register>

    <interception>
      <policy name="non-negative">
        <matchingRule name="rule1" 
          type="TypeMatchingRule">
          <constructor>
             <param name="typeName" value="ICalculator" />
          </constructor>
        </matchingRule>
        <matchingRule name="rule2" 
          type="MemberNameMatchingRule">
          <constructor>
            <param name="namesToMatch">
              <array type="string[]">
                <value value="Sub" />
              </array>
            </param>
          </constructor>
        </matchingRule>
        <callHandler name="handler1" 
          type="NonNegativeCallHandler">
          <lifetime type="singleton" />
        </callHandler>                    
      </policy>
    </interception>
            
  </container>
</unity>

Когда в одной политике содержится несколько правил соответствия, конечный результат формируется применением к ним булева оператора AND (т. е. все правила должны быть true). Если вы определили несколько политик, тогда каждая из них оценивается независимо. Таким образом, вы получаете возможность применять обработчики из разных политик.

Заключение

Кратко напомню, что перехват — это подход, с помощью которого большинство IoC-инфраструктур в пространстве Microsoft .NET Framework реализуют ориентацию на аспекты. За счет перехвата вы получаете шанс на выполнение своего кода до или после любого конкретного метода в любом нужном типе из любой сборки. В прошлом с этой целью в EntLib был специфический блок приложения, PIAB. В EntLib 5.0 нижележащий механизм PIAB был перемещен в Unity и реализован как специальное поведение для низкоуровневого API перехвата, о котором я рассказывал в двух предыдущих статьях. Поведение встраивания политики требует использования Unity-контейнера и не будет работать, если вы применяете только низкоуровневый API перехвата.

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

Дино Эспозито (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)