Фабрики объектов в 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)); } }