Фабрики объектов в Unity
В этом посте я описал как с помощью LifetimeManager можно научить Unity использовать фабрику для создания объектов.
На самом деле так делать не стоит. В составе Unity есть сборка Microsoft.Practices.Unity.StaticFactory.dll в которой находится расширение контейнера для использования фабрики объектов.
Регистрация фабрики происходит методом RegisterFactory, который принимает делегат FactoryDelegate. Этот делегат принимает параметром IUnityContainer, и возвращает object.
Пример
var r = new Random();
var container = new UnityContainer();
container
.AddNewExtension<StaticFactoryExtension>()
.Configure<StaticFactoryExtension>()
.RegisterFactory<int>("random", c => r.Next());
Чтобы этим всем было удобнее пользоваться можно написать extension-методы
public static class UnityStaticFactoryExtensions
{
private static IStaticFactoryConfiguration GetExtension(IUnityContainer container)
{
var ext = container.Configure<StaticFactoryExtension>();
if (ext == null)
{
container.AddNewExtension<StaticFactoryExtension>();
ext = container.Configure<StaticFactoryExtension>();
}
return ext;
}
public static IUnityContainer RegisterFactory<T>(
this IUnityContainer container,
Func<IUnityContainer, T> factoryMethod)
{
GetExtension(container).RegisterFactory<T>(c => factoryMethod(c));
return container;
}
public static IUnityContainer RegisterFactory<T>(
this IUnityContainer container,
string name,
Func<IUnityContainer, T> factoryMethod)
{
GetExtension(container).RegisterFactory<T>(name, c => factoryMethod(c));
return container;
}
}
Тогда код примера, показанного выше можно записать гораздо короче
var r = new Random();
var container = new UnityContainer();
container.RegisterFactory("random", c => r.Next());
AOP времени исполнения в Unity
Для тех кто не знает – AOP это Aspect-Oriented Programming, Аспектно-ориентированное программирование.
При написании любой программы программист производит функциональную декомпозицию, то есть разбивает большие блоки функциональности на более маленькие. Но всегда существуют так называемые cross-cutting concerns или сквозная функциональность, которая используется всеми остальными частями программы, которую невозможно выделить в отдельный модуль\класс\метод,
Чаще всего такой функциональностью является логгирование, разграничение доступа, управление транзакциями.
Концепция AOP заключается в том что сквозная функциональность выделяется в отдельные сущности , называемые аспектами, и декларативно задается использование аспектов в коде.
AOP для .NET может быть реализован двумя способами: изменение кода при компиляции инструментами типа PostSharp или макросами языка Nemerle, или перехват вызовов на стадии выполнения.
В составе Unity есть сборка Microsoft.Practices.Unity.Interception, которая содержит расширение контейнера Unity для перехвата вызовов объектов собираемых контейнером.
Чтобы перехватывать вызовы надо контейнеру сообщить что перехватывать, как перехватывать, и зачем перехватывать.
Что перехватывать задается политиками (Policy), как перехватывать определяют перехватчики (Interceptor), зачем перехватывать определяют обработчики вызовов (CallHandlers).
Эти три части механизма перехвата не зависят друг от друга.
Перехватчики – это классы, реализующие интерфейс IInterceptor. В библиотеке есть классы InterfaceInterceptor для перехвата методов интерфейса, VirtualMethodInterceptor – для перехвата виртуальных методов класса, TransparentProxyInterceptor – для перехвата с помощью прокси-классов, используемых для .NET Remoting.
Обработчики вызовов – это классы, которые реализуют интерфейс ICallHandler, в котором только один нужный метод – Invoke.
Политики бывают двух видов – управляемая атрибутами (AttributeDrivenPolicy) и управляемая правилами (RuleDrivenPolicy).
По-умолчанию используется AttributeDrivenPolicy, которая заключается в том что обработчики вызовов задаются атрибутами, унаследованными от HandlerAttribute, и перехватываются только те методы, для которых заданы эти атрибуты (или атрибуты заданы для классов).
RuleDrivenPolicy позволяет задавать какие методы будут перехватываться с помощью набора правил (IMatchingRule) и какие обработчики будут при этом вызываться.
Подробнее по этой ссылке http://msdn.microsoft.com/en-us/library/dd140045.aspx
Если вы сами пишете код, для которого нужен перехват, то атрибутов и стандартной политики вам будет достаточно. Если вы не можете менять код классов, но хотите их перехватывать, то это можно сделать с помощью задания политик, основанных на правилах заданных в коде или конфигурационном файле.
Пример
Сначала создадим обработчик, который просто выводит Hello, world на консоль
public sealed class HelloWorldAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler(IUnityContainer container)
{
return new HelloWorldCallHandler();
}
}
public class HelloWorldCallHandler: ICallHandler
{
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
Console.WriteLine("Hello, world");
return getNext()(input, getNext);
}
public int Order { get; set; }
}
Теперь создадим класс, унаследованный от MarshalByRefObject, чтобы его можно было перехватывать с помощью TransparentProxyInterceptor. Атрибут HelloWorld объявлен выше.
[HelloWorld]
public class SomeService1: MarshalByRefObject
{
public void Method1()
{
Console.WriteLine("SomeService1.Method1");
}
}
В Main напишем код
container
.AddNewExtension<Interception>()
.Configure<Interception>()
.SetDefaultInterceptorFor<SomeService1>(new TransparentProxyInterceptor());
var s = container.Resolve<SomeService1>();
s.Method1();
Console.ReadLine();
При выполнении будет выведено
Hello, world SomeService1.Method1
Предположим что SomeService1 нам недоступен и уберем атрибут HelloWorld. Чтобы получить такую же функциональность программы надо дописать несколько строк кода
container
.AddNewExtension<Interception>()
.Configure<Interception>()
.SetDefaultInterceptorFor<SomeService1>(new TransparentProxyInterceptor())
.AddPolicy("Policy")
.AddMatchingRule(new TypeMatchingRule(typeof(SomeService1)))
.AddCallHandler<HelloWorldCallHandler>();
var s = container.Resolve<SomeService1>();
s.Method1();
Console.ReadLine();
Аналогичные настройки можно задать в конфигурационном файле.
Что делать если Unity-контейнер надо передавать как зависимость в другие компоненты?
Правильный ответ – ничего.
var container = new UnityContainer(); var resolvedContainer = container.Resolve<IUnityContainer>();
При выполнении такого кода resolvedContainer получит ссылку на сам контейнер. Это значит что если в любом, собираемом из контейнера, классе есть зависимость типа IUnityContainer, то она автоматически будет получать ссылку на контейнер.
Конфигурация Unity
Существует четыре различных способа конфигурирования контейнера.
1)Использование соглашений. Фактически отсутствие явного конфигурирования. В Unity используется очень простое соглашение, что в классе должен быть один конструктор, который принимает все зависимости параметрами.
Такой способ подходит для 90% случаев если вы пишите код сами.
2)Указание зависимостей с помощью атрибутов. Для свойств есть DependencyAttribute, для конструктора указывается InjectionConstructorAttribute, для метода InjectionMethodAttribute. Для параметров конструктора и injection-методов также можно указывать DependencyAttribute.
При навешивании DependencyAttribute на свойство или параметр можно указать имя зависимости.
3)Задание конфигурации в коде при добавлении элемента в контейнер.
Последним параметром метода RegisterType является массив InjectionMember. В этом массиве можно передать объекты типа InjectionProperty, InjectionConstructor и InjectionMethod чтобы указать с помощью каких членов класса проводить инъекцию.
При указании InjectionConstructor и InjectionMethod для каждого параметра можно указать конкретное значение или попросить контейнер резолвить нужный параметр.
Такой способ дает гораздо более многословен, чем все остальные способы, но дает гораздо большую гибкость. С помощью конфигурации в коде можно использовать IoC с классами, к исходникам которых нет доступа.
4)Задание конфигурации в XML. Этот способ имеет смысл применять только если нужно изменять конфигурацию без перекомпиляции программы. Конфигурация в XML не позволяет описывать произвольные типы, поэтому обладает меньшей мощностью по сравнению с конфигурацией в коде.
Подробнее о возможностях конфигурирования Unity можно почитать по ссылкам
http://msdn.microsoft.com/en-us/library/dd203225.aspx
http://msdn.microsoft.com/en-us/library/dd203208.aspx
http://msdn.microsoft.com/en-us/library/dd203195.aspx
http://msdn.microsoft.com/en-us/library/dd203156.aspx
http://msdn.microsoft.com/en-us/library/dd203230.aspx
Overconfiguration или Темная сторона силы
Часто программисты, познакомившись с IoC-контейнерами и оценив их возможности декларативного конфигурирования, начинают использовать конфигурацию по любому поводу. Это приводит к тому что конфиги становятся монструозных размеров и абсолютно нечитаемые. При этом по коду программы абсолютно невозможно понять какой код когда выполняется. Любые изменения затрагивают не только код, но и конфигурацию, что может привести к проблемам при развертывании.
Поэтому при использовании любой технологии, которая может очень сильно конфигурироваться, предпочитайте пользоваться соглашениями (Convetions over Configuration) и размещайте конфигурацию как можно ближе к месту использования. Для этого используйте атрибуты и помещайте код конфигурирования в ту же сборку, где находятся конфигурируемые классы.
Управление временем жизни объектов в Unity
При регистрации класса или объекта в контейнере можно указать объект LifetimeManager, который будет управлять временем жизни экземпляров в контейнере.
Класс LifetimeManager очень простой, в нем всего три метода GetValue, SetValue и RemoveValue, причем последний не используется.
При помещении объекта в контейнер вызывается метод SetValue, а при необходимости получить объект вызывается GetValue и если он вернул null, то создается новый объект.
В библиотеку Microsoft.Practices.Unity входит несколько менеджеров.
TransientLifetimeManager – ничего не сохраняет, GetValue всегда возвращает null, поэтому объект создается каждый раз. Этот менеджер используется по-умолчанию при вызове RegisterType.
ContainerControlledLifetimeManager – сохраняет объект в локальной переменной. Поэтому объект живет столько же, сколько и контейнер. Этот (вернее другой, но с таким же поведением) менеджер используется по-умолчанию при вызове RegisterInstance.
ExternallyControlledLifetimeManager – сохраняет слабую ссылку (WeakReference) на объект. При использовании этого менеджера и вызове RegisterInstance сам вызывающий код должен управлять временем жизни объекта, помещенного в контейнер. Когда используется RegisterType этот менеджер будет выдавать уже существующий экземпляр объекта если он есть.
PerThreadLifetimeManager – сохраняет объекты в ThreadStatic словаре. Таким образом каждый поток в программе будет использовать свой набор объектов.
Для применения Unity в ASP.NET приложениях очень легко реализовать LifetimeManager, который сохраняет объект в контексте или в сессии.
Другие области применения LifetimeManager
В Unity нет возможности регистрации метода создания объектов, но это очень легко исправить с помощью своего LifetimeManager и пары extension-методов.
public class FactoryLifetimeManager<T>: LifetimeManager
{
Func<T> _factoryMethod;
LifetimeManager _baseManager;
public FactoryLifetimeManager(Func<T> factoryMethod, LifetimeManager baseManager)
{
_factoryMethod = factoryMethod;
_baseManager = baseManager;
}
public override object GetValue()
{
var obj = _baseManager.GetValue();
if (obj == null)
{
obj = _factoryMethod();
SetValue(obj);
}
return obj;
}
public override void RemoveValue()
{
_baseManager.RemoveValue();
}
public override void SetValue(object newValue)
{
_baseManager.SetValue(newValue);
}
}
public static class UnityFactoryMethodExtensions
{
public static IUnityContainer RegisterFactory<T>(this IUnityContainer container, Func<T> factoryMethod)
{
return container.RegisterFactory<T>(factoryMethod, new TransientLifetimeManager());
}
public static IUnityContainer RegisterFactory<T>(this IUnityContainer container, Func<T> factoryMethod, LifetimeManager lifetimeManager)
{
return container.RegisterType<T>(new FactoryLifetimeManager<T>(factoryMethod, lifetimeManager));
}
public static IUnityContainer RegisterFactory<T>(this IUnityContainer container, Func<T> factoryMethod, string name)
{
return container.RegisterFactory<T>(factoryMethod, name, new TransientLifetimeManager());
}
public static IUnityContainer RegisterFactory<T>(this IUnityContainer container, Func<T> factoryMethod, string name, LifetimeManager lifetimeManager)
{
return container.RegisterType<T>(name, new FactoryLifetimeManager<T>(factoryMethod, lifetimeManager));
}
}
