вторник, 14 сентября 2010 г.

AOP с Application Unity: практическое применение

Аспектно-ориентированное программирование (проектирование) – относительно новая методология, применяемая при разработке клиентского и серверного ПО. Несмотря на это, в основе ее лежит старый принцип «перехвата» методов – код как бы ижектируется до и после вызываемого метода.
К сожалению, C# не поддерживат АОП на языковом уровне (а жаль, мог бы). Однако есть расширения для DI контейнера Application Unity (который входит в состав Microsoft Enterprise Library), позволяющий реализовывать АОП.

Есть хорошие статьи для начала по использованию Application Unity для АОП. Читаем:
http://habrahabr.ru/blogs/net/50845
http://codebetter.com/blogs/david.hayden/archive/2008/11/07/unity-and-aop-example.aspx
http://www.codewrecks.com/blog/index.php/2009/01/31/unity-and-aop-in-enterprise-library/

Application Unity генерирует класс-прозрачный прокси, который вызывает методы целевого класса. Он позволяет выполнить перехват методов либо на уровне связи интерфейса с реализацией, либо у самого класса, если он наследует MarshalByRefObject.
В обоих случаях выглядит это так: вы пишете атрибуты, ставите их над методом, и атрибут обрабатывает вызов метода, позволяя выполнять код до и после метода, или не вызывать настоящий метод вообще.
Каждый из случаев имеет свои особенности, имеет свои преимущества и недостатки.
Перехват на уровне связи интерфейса с классом-реализацией:
+ выполняется быстро
+ регистрируется только на интерфейс
- атрибуты ставятся только в интерфейсе; как следствие, АОП может применяться только к публичным методам
- из-за атрибутов в интерфейсе частично раскрывает особенности реализации
- AOP не будет работать в реализации без интерфейса

Перехват методов у класса реализации:
+ позволяет расставить атрибуты над любыми методами (в т.ч. private и protected)
+ атрибуты расставляются только в классе-реализации: не раскрываются особенности АОП в интерфейсе
- работает медленно из за MarshalByRefObject
- требует регистрации на все классы-реализации интерфейса

Оба случая завязывают АОП на использование DI контейнера Application Unity, поскольку требуют получения экземпляра класса его средствами.

Как показала практика, в реальной жизни АОП удобно использовать для:
1. Логгирования на входе и выходе из метода (классический пример, чаще всего встречается в публикациях)
2. Многопоточной синхронизации
3. Проверки входных аргументов метода
4. Обработки исключений
5. Реализация ступенчатой инициализации

Кроме логгирования, практически все остальные случаи требуют привязки АОП к реализации, поэтому выберем метод с наследованием MarshalByRefObject.

Например, возьмем пример реализации многоступенчатой инициализации. После вызова конструктора перед вызовом всех методов должен быть единажды вызван метод Initialize. Если была произведена попытка вызова метода до инициализации, должно быть сгенерировано исключение, как и в случае повторного вызова Initialize. Как это выглядит без АОП:


public interface IInitializable
{
bool IsInitialized { get; }
}

public class MyClass : IInitializable, IDisposable
{
public bool IsInitialized { get; private set; }

public void Initialize()
{
if (IsInitialized)
throw new Exception("duplicate initialization");

IsInitialized = true;
}

public void FirstUsefulMethod()
{
if (!IsInitialized)
throw new Exception("object must be initialized first");
}

public void SecondUsefulMethod()
{
if (!IsInitialized)
throw new Exception("object must be initialized first");
}

public void Dispose()
{
IsInitialized = false;
}
}

Недостатки очевидны: в каждом методе нужно добавлять проверку на инициализацию.
Используем декларативный подход с АОП:


public class MyClass : MarshalByRefObject, IInitializable, IDisposable
{
public bool IsInitialized { get; private set; }

[Initializer]
public void Initialize()
{
IsInitialized = true;
}

[AfterInitialize]
public void FirstUsefulMethod()
{
}

[AfterInitialize]
public void SecondUsefulMethod()
{
}

public void Dispose()
{
IsInitialized = false;
}
}

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

public void FirstUsefulMethod(object parameter)
{
if (!IsInitialized)
throw new Exception("object must be initialized before using this method");

if (parameter == null)
throw new ArgumentNullException("parameter");

_logger.Debug("Enter to FirstUsefulMethod");

try
{
_locker.EnterWriteLock();

<ПОЛЕЗНЫЙ КОД>
}
catch(Exception exception)
{
DisplayMessageBox(exception.Message);
throw;
}
finally
{
_locker.ExitWriteLock();
_logger.Debug("Exit from FirstUsefulMethod");
}
}

и с АОП:

[AfterInitialize]
[ArgumentsNotNull]
[DisplayError]
[Log]
[ExclusiveLock]
public void UsefulMethod(object parameter)
{
<ПОЛЕЗНЫЙ КОД>
}

Для примера приведу исходный код пары атрибутов, Initializer и AfterInitialize:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public class InitializerAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler(IUnityContainer container)
{
return new InitializerCallHandler();
}
}

public class InitializerCallHandler : ICallHandler
{
public int Order { get; set; }

public InitializerCallHandler()
{
}

public InitializerCallHandler(int order)
{
Order = order;
}

#region ICallHandler Members

public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
var initializableObject = (IInitializable)input.Target;

if (initializableObject == null)
{
throw new Exception(
"[Initializer] attribute can be used only for classes that released IInitializable interface");
}

if (initializableObject.IsInitialized)
{
throw new InvalidOperationException(
string.Format("Object of type '{0}' already initialized",
initializableObject.GetType().FullName));
}

return getNext().Invoke(input, getNext);
}

#endregion
}

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public class AfterInitializeAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler(IUnityContainer container)
{
return new AfterInitializeCallHandler();
}
}

public class AfterInitializeCallHandler : ICallHandler
{
public int Order { get; set; }

public AfterInitializeCallHandler()
{
}

public AfterInitializeCallHandler(int order)
{
Order = order;
}

#region ICallHandler Members

public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
var initializableObject = (IInitializable) input.Target;

if (initializableObject == null)
{
throw new Exception(
"[AfterInitialize] attribute can be used only for classes that released IInitializable interface");
}

if (!initializableObject.IsInitialized)
{
throw new InvalidOperationException(
string.Format("Object of type '{0}' must be initialized before using",
initializableObject.GetType().FullName));
}

return getNext().Invoke(input, getNext);
}

#endregion
}

Регистрация типа в контейнере:

unityContainer.RegisterType<IMyClass, MyClass>();
unityContainer.Configure<Interception>()
.SetInterceptorFor<MyClass>(new TransparentProxyInterceptor())

Перед этим нужно включить расширение Interception

unityContainer.AddNewExtension<Interception>();


P.S. Атрибуты АОП не работают для конструкторов

Комментариев нет:

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