Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Аспектно-ориентированное программирование с применением класса RealProxy
Продукты и технологии:
Microsoft .NET Framework, аспектно-ориентированное программирование (АОП)
В статье рассматриваются:
- основы аспектно-ориентированного программирования (AOП);
- проектировочный шаблон Decorator;
- применение класса RealProxy для создания динамического прокси;
- фильтрация аспектов;
- различия между RealProxy и PostSharp.
Приложение с тщательно продуманной архитектурой имеет раздельные уровни, чтобы свести к необходимому минимуму взаимодействие между компонентами с разными обязанностями. Вообразите, что вы проектируете свободно связанное и сопровождаемое приложение, но посреди разработки видите, что некоторые требования не укладываются в эту архитектуру, например:
- в приложении должна быть система аутентификации, используемая до любого запроса или обновления;
- данные следует проверять до записи в базу данных;
- в приложении должны быть средства аудита и протоколирования конфиденциальных операций;
- приложение должно вести журнал отладки, чтобы проверять успешность операций;
- необходимо замерять производительность некоторых операций, чтобы быть уверенным, что она находится в желательных пределах.
Любое из этих требований влечет за собой уйму работы и, более того, дублирование кода. Вы должны добавить один и тот же код во многие части системы, что идет вразрез с принципом DRY («don’t repeat yourself», «не повторяйтесь») и еще больше затрудняет сопровождение. Любое изменение в требованиях приводит к массе изменений в программе. Когда мне приходится добавлять нечто вроде этого в свое приложение, я думаю: «Почему компилятор не способен добавить этот повторяющийся код во множество мест за меня?» или «Хотел бы я иметь параметр наподобие ˮДобавить протоколирование в этот методˮ».
Хорошая новость — нечто подобное уже существует: аспектно-ориентированное программирование (АОП). В нем универсальный код отделяется от аспектов, которые пересекают границы объекта или уровня. Например, журнал приложения не привязан ни к какому уровню приложения. Он применяется к программе в целом и должен присутствовать везде. Это называют поперечной обязанностью (crosscutting concern).
АОП, согласно Википедии, — это «парадигма программирования, которая нацелена на увеличение модульности за счет разделения поперечных обязанностей». Речь идет о функциональности, которая встречается во многих частях системы и отделяется от базовой части приложения, что улучшает степень разделения обязанностей (separation of concerns), в то же время предотвращая дублирование кода и его жесткое связывание.
В этой статье я поясню основы АОП, а затем подробно опишу, как упростить его, используя динамический прокси через класс RealProxy из Microsoft .NET Framework.
Реализация АОП
Самое крупное преимущество АОП в том, что вы должны позаботиться о конкретном аспекте лишь в одном месте: один раз запрограммировать, а потом применять везде, где это нужно. АОП находит много областей применения, например:
- реализация протоколирования в приложении;
- использование аутентификации до какой-либо операции (скажем, некоторые операции разрешаются только аутентифицированным пользователям);
- реализация проверки или уведомления для set-аксессоров свойств (вызов события PropertyChanged, когда некое свойство изменилось, для классов, реализующих интерфейс INotifyPropertyChanged interface);
- изменение поведения некоторых методов.
Как видите, у АОП много областей применения, но его следует использовать с осторожностью. Часть кода будет находиться вне вашего поля зрения и выполняться при каждом вызове, где присутствует аспект. В нем могут быть ошибки; кроме того, этот код может сильно ухудшить производительность приложения. Незначительный дефект в аспекте может вылиться во много часов отладки. Если ваш аспект не используется во множестве мест, то иногда лучше добавлять его напрямую в код.
Реализации АОП используют некоторые общие методики:
- добавление исходного кода с применением препроцессора, такого как в C++;
- использование постпроцессора для добавления инструкций в скомпилированный двоичный код;
- применение специального компилятора, который добавляет код в процессе компиляции;
- использование перехватчика кода в период выполнения, который добавляет нужный код.
В .NET Framework из этих методик чаще всего применяют постобработку и перехват кода. Первая из них используется PostSharp (postsharp.net), а вторая — контейнерами встраивания зависимостей (dependency injection, DI) вроде Castle DynamicProxy (bit.ly/JzE631) и Unity (unity.codeplex.com). Эти средства обычно используют проектировочный шаблон Decorator или Proxy для перехвата кода.
Проектировочный шаблон Decorator
Decorator решает распространенную проблему: у вас есть класс и вы хотите добавить в него некую функциональность. Сделать это можно несколькими способами.
- Вы могли бы напрямую добавить новую функциональность в класс. Однако это придает классу новую обязанность и идет вразрез с принципом одной обязанности (single responsibility principle).
- Вы могли бы создать новый класс, который выполняет эту функциональность, и вызывать его из старого класса. Здесь возникает новая проблема: как быть, если вам понадобится использовать класс без новой функциональности?
- Вы могли бы унаследовать новый класс и добавить новую функциональность, но это может привести к необходимости создания множества новых классов. Например, у вас есть класс репозитария для CRUD-операций (create, read, update and delete) над базой данных, и вы хотите добавить поддержку аудита. Потом вам потребовалось добавить проверку данных, чтобы быть уверенным в корректном обновлении данных. После этого возникла необходимость в аутентификации доступа, чтобы к этим классам могли обращаться только авторизованные пользователи. Это весьма крупные проблемы: вы могли бы иметь некоторые классы, реализующие все три аспекта, другие — только два из них или даже лишь один. Сколько классов у вас появилось бы в итоге?
- Вы можете дополнить класс аспектом, создав новый класс, который использует этот аспект, а затем вызывает старый класс. Тем самым, если вам нужен один аспект, вы дополняете класс одним аспектом. В случае двух аспектов вы дополняете его дважды и т. д. Предположим, вы заказываете игрушку (поскольку мы все повернуты на технике, пусть это будет Xbox или смартфон). Его дисплей нужно поместить в упаковку в магазине для защиты от повреждения. Потом вы заказываете его в подарочной упаковке (второе дополнение), чтобы украсить коробку подарочной бумагой, лентами и карточками. Магазин отправляет эту игрушку в третьей упаковке — в коробке, заполненной пенопластом для защиты. Итого получается три дополнения, причем каждое обеспечивает свою функциональность, и каждое из них независимо друг от друга. Вы можете купить свою игрушку без подарочной упаковки, забрать из магазина без внешней коробки или даже купить вообще безо всякой защитной упаковки (со специальной скидкой!). Таким образом, ваша игрушка может быть с любой комбинацией дополнений, но они никак не меняют ее базовую функциональность.
Теперь, когда вы кое-что знаете о шаблоне Decorator, я покажу, как реализовать его на C#.
Сначала создайте интерфейс IRepository<T>:
public interface IRepository<T>
{
void Add(T entity);
void Delete(T entity);
void Update(T entity);
IEnumerable<T> GetAll();
T GetById(int id);
}
Реализуйте его с помощью класса Repository<T>, как показано на рис. 1.
Рис. 1. Класс Repository<T>
public class Repository<T> : IRepository<T>
{
public void Add(T entity)
{
Console.WriteLine("Adding {0}", entity);
}
public void Delete(T entity)
{
Console.WriteLine("Deleting {0}", entity);
}
public void Update(T entity)
{
Console.WriteLine("Updating {0}", entity);
}
public IEnumerable<T> GetAll()
{
Console.WriteLine("Getting entities");
return null;
}
public T GetById(int id)
{
Console.WriteLine("Getting entity {0}", id);
return default(T);
}
}
Используйте класс Repository<T> для добавления, обновления, удаления и получения элементов класса Customer:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
}
Программа могла бы выглядеть примерно так, как показано на рис. 2.
Рис. 2. Основная программа без протоколирования
static void Main(string[] args)
{
Console.WriteLine("***\r\n Begin program - no logging\r\n");
IRepository<Customer> customerRepository =
new Repository<Customer>();
var customer = new Customer
{
Id = 1,
Name = "Customer 1",
Address = "Address 1"
};
customerRepository.Add(customer);
customerRepository.Update(customer);
customerRepository.Delete(customer);
Console.WriteLine("\r\nEnd program - no logging\r\n***");
Console.ReadLine();
}
Запустив этот код, вы увидите нечто вроде того, что представлено на рис. 3.
Рис. 3. Вывод программы без протоколирования
Вообразите, что ваш начальник попросил вас добавить в этот класс протоколирование. Вы можете создать новый класс, который дополнит IRepository<T>. Он принимает класс для создания и реализует тот же интерфейс (рис. 4).
Рис. 4. Класс LoggerRepository
public class LoggerRepository<T> : IRepository<T>
{
private readonly IRepository<T> _decorated;
public LoggerRepository(IRepository<T> decorated)
{
_decorated = decorated;
}
private void Log(string msg, object arg = null)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(msg, arg);
Console.ResetColor();
}
public void Add(T entity)
{
Log("In decorator - Before Adding {0}", entity);
_decorated.Add(entity);
Log("In decorator - After Adding {0}", entity);
}
public void Delete(T entity)
{
Log("In decorator - Before Deleting {0}", entity);
_decorated.Delete(entity);
Log("In decorator - After Deleting {0}", entity);
}
public void Update(T entity)
{
Log("In decorator - Before Updating {0}", entity);
_decorated.Update(entity);
Log("In decorator - After Updating {0}", entity);
}
public IEnumerable<T> GetAll()
{
Log("In decorator - Before Getting Entities");
var result = _decorated.GetAll();
Log("In decorator - After Getting Entities");
return result;
}
public T GetById(int id)
{
Log("In decorator - Before Getting Entity {0}", id);
var result = _decorated.GetById(id);
Log("In decorator - After Getting Entity {0}", id);
return result;
}
}
Этот новый класс обертывает методы для дополняемого класса и добавляет механизм протоколирования. Код основной программы нужно немного изменить, чтобы вызывать класс протоколирования (рис. 5).
Рис. 5. Основная программа, использующая LoggerRepository
static void Main(string[] args)
{
Console.WriteLine("***\r\n Begin program - logging with decorator\r\n");
// IRepository<Customer> customerRepository =
// new Repository<Customer>();
IRepository<Customer> customerRepository =
new LoggerRepository<Customer>(new Repository<Customer>());
var customer = new Customer
{
Id = 1,
Name = "Customer 1",
Address = "Address 1"
};
customerRepository.Add(customer);
customerRepository.Update(customer);
customerRepository.Delete(customer);
Console.WriteLine("\r\nEnd program - logging with decorator\r\n***");
Console.ReadLine();
}
Вы просто создаете новый класс и передаете его конструктору экземпляр старого класса как параметр. При выполнении этой программы вы видите, что у нее есть средства протоколирования (рис. 6).
Рис. 6. Протоколирование программы с Decorator
Возможно, вы думаете: «О'кей, идея хорошая, но трудоемкая: я должен реализовать все классы и добавить аспект ко всем методам. Это будет сложно в сопровождении. Нет ли другого способа сделать то же самое?». В .NET Framework можно использовать отражение, чтобы получать все методы и выполнять их. В библиотеке базовых классов (BCL) есть даже класс RealProxy (bit.ly/18MfxWo), который осуществляет реализацию за вас.
Создание динамического прокси с помощью RealProxy
Класс RealProxy предоставляет базовую функциональность для прокси. Это абстрактный класс, который нужно наследовать переопределением его метода Invoke и добавлением новой функциональности. Этот класс находится в пространстве имен System.Runtime.Remoting.Proxies. Чтобы создать динамический прокси, используйте код, аналогичный показанному на рис. 7.
Рис. 7. Класс DynamicProxy
class DynamicProxy<T> : RealProxy
{
private readonly T _decorated;
public DynamicProxy(T decorated)
: base(typeof(T))
{
_decorated = decorated;
}
private void Log(string msg, object arg = null)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(msg, arg);
Console.ResetColor();
}
public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
var methodInfo = methodCall.MethodBase as MethodInfo;
Log("In Dynamic Proxy - Before executing '{0}'",
methodCall.MethodName);
try
{
var result = methodInfo.Invoke(_decorated,methodCall.InArgs);
Log("In Dynamic Proxy - After executing '{0}' ",
methodCall.MethodName);
return new ReturnMessage(result, null, 0,
methodCall.LogicalCallContext, methodCall);
}
catch (Exception e)
{
Log(string.Format(
"In Dynamic Proxy- Exception {0} executing '{1}'", e),
methodCall.MethodName);
return new ReturnMessage(e, methodCall);
}
}
}
В конструкторе класса вы должны вызвать конструктор базового класса, передав тип дополняемого класса. Затем вы должны переопределить метод Invoke, принимающий параметр IMessage. Он содержит словарь со всеми параметрами, переданными методу. Параметр IMessage приводится к типу IMethodCallMessage, так что вы можете извлечь параметр MethodBase (с типом MethodInfo).
Следующие шаги — добавление аспекта, который должен срабатывать до вызова метода, вызов исходного метода через methodInfo.Invoke и добавление аспекта после вызова.
Вызывать свой прокси напрямую нельзя, так как DynamicProxy<T> не является IRepository<Customer>. То есть вызов наподобие следующего недопустим:
IRepository<Customer> customerRepository =
new DynamicProxy<IRepository<Customer>>(
new Repository<Customer>());
Чтобы использовать дополненный репозитарий, вызывайте метод GetTransparentProxy, который вернет экземпляр IRepository<Customer>. Каждый вызываемый метод этого экземпляра пройдет через метод Invoke прокси. Для упрощения этого процесса можно создать класс Factory, чтобы создавать прокси и возвращать экземпляр для репозитария:
public class RepositoryFactory
{
public static IRepository<T> Create<T>()
{
var repository = new Repository<T>();
var dynamicProxy = new DynamicProxy<IRepository<T>>(repository);
return dynamicProxy.GetTransparentProxy() as IRepository<T>;
}
}
Тогда основная программа будет выглядеть, как на рис. 8.
Рис. 8. Основная программа с динамическим прокси
static void Main(string[] args)
{
Console.WriteLine("***\r\n Begin program - logging with dynamic proxy\r\n");
// IRepository<Customer> customerRepository =
// new Repository<Customer>();
// IRepository<Customer> customerRepository =
// new LoggerRepository<Customer>(
// new Repository<Customer>());
IRepository<Customer> customerRepository =
RepositoryFactory.Create<Customer>();
var customer = new Customer
{
Id = 1,
Name = "Customer 1",
Address = "Address 1"
};
customerRepository.Add(customer);
customerRepository.Update(customer);
customerRepository.Delete(customer);
Console.WriteLine("\r\nEnd program - logging with dynamic proxy\r\n***");
Console.ReadLine();
}
Запустив эту программу, вы получите результат, аналогичный предыдущему (рис. 9).
Рис. 9. Выполнение программы с динамическим прокси
Как видите, вы создали динамический прокси, позволяющий добавлять в код аспекты без необходимости повторять их. Если вам нужно добавить новый аспект, достаточно создать новый класс, производный от RealProxy, и использовать его для дополнения первого прокси.
Если ваш начальник снова зайдет к вам и попросит добавить в код авторизацию, чтобы доступ к репозитарию был только у администраторов, вы сможете создать новый прокси, как показано на рис. 10.
Рис. 10. Прокси аутентификации
class AuthenticationProxy<T> : RealProxy
{
private readonly T _decorated;
public AuthenticationProxy(T decorated)
: base(typeof(T))
{
_decorated = decorated;
}
private void Log(string msg, object arg = null)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(msg, arg);
Console.ResetColor();
}
public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
var methodInfo = methodCall.MethodBase as MethodInfo;
if (Thread.CurrentPrincipal.IsInRole("ADMIN"))
{
try
{
Log("User authenticated - You can execute '{0}' ",
methodCall.MethodName);
var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
return new ReturnMessage(result, null, 0,
methodCall.LogicalCallContext, methodCall);
}
catch (Exception e)
{
Log(string.Format(
"User authenticated - Exception {0} executing '{1}'", e),
methodCall.MethodName);
return new ReturnMessage(e, methodCall);
}
}
Log("User not authenticated - You can't execute '{0}' ",
methodCall.MethodName);
return new ReturnMessage(null, null, 0,
methodCall.LogicalCallContext, methodCall);
}
}
Фабрику репозитария следует изменить так, чтобы она вызывала оба прокси (рис. 11).
Рис. 11. RepositoryFactory, дополненная двумя прокси
public class RepositoryFactory
{
public static IRepository<T> Create<T>()
{
var repository = new Repository<T>();
var decoratedRepository =
(IRepository<T>)new DynamicProxy<IRepository<T>>(
repository).GetTransparentProxy();
// Создаем динамический прокси для уже дополненного класса
decoratedRepository =
(IRepository<T>)new AuthenticationProxy<IRepository<T>>(
decoratedRepository).GetTransparentProxy();
return decoratedRepository;
}
}
Изменив основную программу в соответствии с рис. 12 и запустив ее, вы получите вывод, показанный на рис. 13.
Рис. 12. Основная программа вызывает репозитарий с двумя пользователями
static void Main(string[] args)
{
Console.WriteLine(
"***\r\n Begin program - logging and authentication\r\n");
Console.WriteLine("\r\nRunning as admin");
Thread.CurrentPrincipal =
new GenericPrincipal(new GenericIdentity("Administrator"),
new[] { "ADMIN" });
IRepository<Customer> customerRepository =
RepositoryFactory.Create<Customer>();
var customer = new Customer
{
Id = 1,
Name = "Customer 1",
Address = "Address 1"
};
customerRepository.Add(customer);
customerRepository.Update(customer);
customerRepository.Delete(customer);
Console.WriteLine("\r\nRunning as user");
Thread.CurrentPrincipal =
new GenericPrincipal(new GenericIdentity("NormalUser"),
new string[] { });
customerRepository.Add(customer);
customerRepository.Update(customer);
customerRepository.Delete(customer);
Console.WriteLine(
"\r\nEnd program - logging and authentication\r\n***");
Console.ReadLine();
}
Рис. 13. Вывод программы, использующей два прокси
Программа выполняет методы репозитария дважды. В первый раз она запускается под учетной записью администратора и успешно вызывает методы, а во второй раз — под учетной записью обычного пользователя и пропускает методы.
Так гораздо легче, не правда ли? Заметьте, что фабрика возвращает экземпляр IRepository<T>, поэтому программа не знает, используется ли дополненная версия. Это соответствует принципу подстановки Лисков (Liskov Substitution Principle), который гласит, что если S является подтипом T, то объекты типа T могут быть заменены объектами типа S. В данном случае, используя интерфейс IRepository<Customer>, вы могли бы задействовать любой класс, реализующий этот интерфейс безо всяких изменений в программе.
Фильтрация функций
До сих пор фильтрации функций не было; аспект применялся к каждому вызываемому методу класса. Зачастую это нежелательно. Например, вам, возможно, не требуется протоколировать методы извлечения (GetAll и GetById). Один из способов добиться этого — фильтровать аспект по имени, как на рис. 14.
Рис. 14. Фильтрация методов для аспекта
public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
var methodInfo = methodCall.MethodBase as MethodInfo;
if (!methodInfo.Name.StartsWith("Get"))
Log("In Dynamic Proxy - Before executing '{0}'",
methodCall.MethodName);
try
{
var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
if (!methodInfo.Name.StartsWith("Get"))
Log("In Dynamic Proxy - After executing '{0}' ",
methodCall.MethodName);
return new ReturnMessage(result, null, 0,
methodCall.LogicalCallContext, methodCall);
}
catch (Exception e)
{
if (!methodInfo.Name.StartsWith("Get"))
Log(string.Format(
"In Dynamic Proxy- Exception {0} executing '{1}'", e),
methodCall.MethodName);
return new ReturnMessage(e, methodCall);
}
}
Программа проверяет, начинается ли имя метода с «Get». Если да, аспект не применяется. Это работает, но код фильтрации повторяется три раза. Помимо этого, фильтр находится внутри прокси, что вынудит вас изменять класс всякий раз, когда вам потребуется модифицировать прокси. Вы можете улучшить ситуацию, создав предикат IsValidMethod:
private static bool IsValidMethod(MethodInfo methodInfo)
{
return !methodInfo.Name.StartsWith("Get");
}
Теперь вам нужно внести изменение только в одном месте, но все равно придется модифицировать класс всякий раз, когда вы захотите изменить фильтр. Одно из решений — предоставлять фильтр как свойство класса, и тогда вы можете возложить обязанность по созданию фильтра на вызывающий код. Можно создать свойство Filter типа Predicate<MethodInfo> и использовать его для фильтрации данных, как показано на рис. 15.
Рис. 15. Прокси с фильтрацией
class DynamicProxy<T> : RealProxy
{
private readonly T _decorated;
private Predicate<MethodInfo> _filter;
public DynamicProxy(T decorated)
: base(typeof(T))
{
_decorated = decorated;
_filter = m => true;
}
public Predicate<MethodInfo> Filter
{
get { return _filter; }
set
{
if (value == null)
_filter = m => true;
else
_filter = value;
}
}
private void Log(string msg, object arg = null)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(msg, arg);
Console.ResetColor();
}
public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
var methodInfo = methodCall.MethodBase as MethodInfo;
if (_filter(methodInfo))
Log("In Dynamic Proxy - Before executing '{0}'",
methodCall.MethodName);
try
{
var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
if (_filter(methodInfo))
Log("In Dynamic Proxy - After executing '{0}' ",
methodCall.MethodName);
return new ReturnMessage(result, null, 0,
methodCall.LogicalCallContext, methodCall);
}
catch (Exception e)
{
if (_filter(methodInfo))
Log(string.Format(
"In Dynamic Proxy- Exception {0} executing '{1}'", e),
methodCall.MethodName);
return new ReturnMessage(e, methodCall);
}
}
}
Свойство Filter инициализируется как Filter = m => true. Это означает, что фильтр неактивен. Устанавливая свойство Filter, программа проверяет, равно ли его значение null, и, если да, сбрасывает фильтр. В методе Invoke программа проверяет результат фильтра и, если он true, применяет аспект. Теперь создание прокси в классе фабрики выглядит так:
public class RepositoryFactory
{
public static IRepository<T> Create<T>()
{
var repository = new Repository<T>();
var dynamicProxy = new DynamicProxy<IRepository<T>>(repository)
{
Filter = m => !m.Name.StartsWith("Get")
};
return dynamicProxy.GetTransparentProxy() as IRepository<T>;
}
}
Обязанность создания фильтра передана фабрике. Запустив программу, вы должны получить нечто вроде того, что показано на рис. 16.
Рис. 16. Вывод при использовании прокси с фильтрацией
Обратите внимание на рис. 16, что для последних двух методов, GetAll и GetById (представленных «Getting entities» и «Getting entity 1»), не выводится информация протоколирования. Вы можете еще больше расширить класс, предоставляя аспекты как события. Благодаря этому вам не придется изменять класс каждый раз, когда вам потребуется изменить аспект. Этот вариант показан на рис. 17.
Рис. 17. Гибкий прокси
class DynamicProxy<T> : RealProxy
{
private readonly T _decorated;
private Predicate<MethodInfo> _filter;
public event EventHandler<IMethodCallMessage> BeforeExecute;
public event EventHandler<IMethodCallMessage> AfterExecute;
public event EventHandler<IMethodCallMessage> ErrorExecuting;
public DynamicProxy(T decorated)
: base(typeof(T))
{
_decorated = decorated;
Filter = m => true;
}
public Predicate<MethodInfo> Filter
{
get { return _filter; }
set
{
if (value == null)
_filter = m => true;
else
_filter = value;
}
}
private void OnBeforeExecute(IMethodCallMessage methodCall)
{
if (BeforeExecute != null)
{
var methodInfo = methodCall.MethodBase as MethodInfo;
if (_filter(methodInfo))
BeforeExecute(this, methodCall);
}
}
private void OnAfterExecute(IMethodCallMessage methodCall)
{
if (AfterExecute != null)
{
var methodInfo = methodCall.MethodBase as MethodInfo;
if (_filter(methodInfo))
AfterExecute(this, methodCall);
}
}
private void OnErrorExecuting(IMethodCallMessage methodCall)
{
if (ErrorExecuting != null)
{
var methodInfo = methodCall.MethodBase as MethodInfo;
if (_filter(methodInfo))
ErrorExecuting(this, methodCall);
}
}
public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
var methodInfo = methodCall.MethodBase as MethodInfo;
OnBeforeExecute(methodCall);
try
{
var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
OnAfterExecute(methodCall);
return new ReturnMessage(
result, null, 0, methodCall.LogicalCallContext, methodCall);
}
catch (Exception e)
{
OnErrorExecuting(methodCall);
return new ReturnMessage(e, methodCall);
}
}
}
На рис. 17 три события — BeforeExecute, AfterExecute и ErrorExecuting — вызываются методами OnBeforeExecute, OnAfterExecute и OnErrorExecuting. Эти методы проверяют, определен ли обработчик событий, и, если определен, проверяют, передает ли фильтр вызванный метод. Если да, они вызывают обработчик, который применяет аспект. Класс фабрики теперь выглядит примерно, как на рис. 18.
Рис. 18. Фабрика репозитария, которая задает события аспекта и фильтр
public class RepositoryFactory
{
private static void Log(string msg, object arg = null)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(msg, arg);
Console.ResetColor();
}
public static IRepository<T> Create<T>()
{
var repository = new Repository<T>();
var dynamicProxy = new DynamicProxy<IRepository<T>>(repository);
dynamicProxy.BeforeExecute += (s, e) => Log(
"Before executing '{0}'", e.MethodName);
dynamicProxy.AfterExecute += (s, e) => Log(
"After executing '{0}'", e.MethodName);
dynamicProxy.ErrorExecuting += (s, e) => Log(
"Error executing '{0}'", e.MethodName);
dynamicProxy.Filter = m => !m.Name.StartsWith("Get");
return dynamicProxy.GetTransparentProxy() as IRepository<T>;
}
}
Теперь у вас есть гибкий класс прокси, и вы можете выбирать аспекты, применяемые перед выполнением операции, после ее выполнения или при возникновении ошибки; причем делать это можно избирательно — только для выбранных методов. Благодаря этому вы можете легко применять множество аспектов в своем коде без повторов.
RealProxy — не замена других средств АОП
С помощью АОП можно централизованно добавлять код во все уровни приложения без необходимости дублировать код. Я показал, как создать обобщенный динамический прокси на основе проектировочного шаблона Decorator, который применяет аспекты к вашим классам, используя события и предикаты для фильтрации функций.
Как видите, RealProxy — гибкий класс, который обеспечивает полный контроль над кодом без внешних зависимостей. Однако заметьте, что RealProxy не является заменой других средств АОП, таких как PostSharp. PostSharp использует совершенно другой подход. Он добавляет IL-код (intermediate language) на этапе после компиляции и не применяет отражение, поэтому должен обеспечивать более высокую производительность по сравнению с RealProxy. Кроме того, вам приходится выполнять больше работы для реализации аспекта с помощью RealProxy, чем в случае PostSharp. Используя PostSharp, вам нужно лишь создать класс аспекта и добавить атрибут к классу (или методу), где должен работать этот аспект, — вот и все.
С другой стороны, в случае RealProxy вы получаете полный контроль над своим исходным кодом, не вводите никаких внешних зависимостей и можете расширять и настраивать его как угодно. Например, если вы хотите применять некий аспект только к методам с атрибутом Log, вы могли бы сделать так:
public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
var methodInfo = methodCall.MethodBase as MethodInfo;
if (!methodInfo.CustomAttributes
.Any(a => a.AttributeType == typeof (LogAttribute)))
{
var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
return new ReturnMessage(result, null, 0,
methodCall.LogicalCallContext, methodCall);
}
...
Кроме того, методика, используемая RealProxy (перехват кода с последующей его заменой), очень мощная. Так, если вы хотите создать инфраструктуру имитации (mocking framework) для создания обобщенный имитаций и заглушек при тестировании, то можете задействовать класс RealProxy для перехвата всех вызовов и их замены своим поведением. Но это тема для другой статьи!
Бруно Соннино (Bruno Sonnino) — Microsoft Most Valuable Professional (MVP), проживает в Бразилии. Разработчик, консультант, автор пяти книг по Delphi (опубликованы на португальском языке издательством Pearson Education Brazil) и множества статей для бразильских и американских журналов и веб-сайтов.
Выражаю благодарность за рецензирование статьи экспертам Microsoft Research Джеймсу Маккафри (James McCaffrey), Карлосу Суарешу (Carlos Suarez) и Йоану Верви (Johan Verwey).