7 мая 2010 г.

Расширение возможностей Unity - 2

В этом посте я покажу еще один пример расширения возможностей Unity и как с помощью Unity получать параметры для наших объектов.
В предыдущем посте мы сделали набор реализаций IPersistence для сохранения объектов:
  • FilePersistence - сохраняет в файл;
  • DbPersistence - в базу данных;
  • WsPersistence - отправляет на web-сервис.
Для получения необходимой нам реализации мы используем Unity-контейнер.

Очевидно, что для работы наши persistence должны обладать наборами параметров - имя файла для FilePersistence, строка соединения для DbPersistence и т.д. Параметры должны храниться в конфиг-файле.

Можно возложить обязанность получения параметров на сам persistence. Но есть способ лучше - применить инверсию и возложить получение параметров на Unity.

Получение параметров через Unity

Рассмотрим на примере DbPersistence.
У DbPersistence есть один параметр - ConnectionString, передающийся в конструктор.

В Unity есть стандартное расширение InjectedMembers, которое предназначено для инъекции значение в создаваемый объект. Оно может вносить как значения, требующий дальнейшего разрешения, так и константные значения.
Пример при описании контейнера в xml-конфиге:

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

Но что будет, если параметры в конфиге изменятся в процессе работы приложения? Т.к. параметры считывались один раз при запуске, то для применения их необходимо перезапустить приложение.
Попытаемся добиться того, чтобы обращение к конфигу происходило при каждом разрешении интерфейса IPersistence.

Поддержка изменения параметров в Runtime

Нам нужна возможность передавать в качестве значения в InjectedMembers.ConfigureInjectionFor делегат, который при своем вызове вернет значение из конфига.

Посмотрим на работу InjectedMembers.
InjectedMembers.ConfigureInjectionFor принимает в параметрах список инъекций. Инъекции могут быть двух типов: InjectionConstructor (для конструктора) и InjectionProperty (для свойств). Нас интересует конструктор.
InjectionConstructor принимает список объектов (количество должно быть равно количеству параметров конструктора реализации). Каждый объект может быть или непосредственно значением параметра (как в примере выше) или экземпляром наследника InjectionParameterValue (таким образом например реализуются параметры, которые требуют разрешения).

Стандартного способа для создания инъекции с делегатом нет. Поэтому напишем свою реализацию.
Сначала опишем делегат:

Теперь собственного наследника от InjectionParameterValue. Сделаем его generic-типом. TValue - тип параметра для которого мы будем получать значение.

Получение значения параметра происходит через механизм политик. Про него можно прочитать в предыдущем посте. Мы должны создать свою политику IDependencyResolverPolicy и в ее методе Resolve вызвать наш делегат.

В DelegatedInjectionValue переопределим абстрактный метод GetResolverPolicy и вернем нашу политику:


Результат

В результате мы используем это так:

При каждом разрешении типа IPersistence будет создаваться объект DbPersistence, вызываться наш делегат и его результат передаваться в конструктор DbPersistence.
Progg it

29 апр. 2010 г.

Расширение возможностей Unity

В этом посте я покажу пример того, как можно расширить стандартные возможности IoC-контейнера Unity. Покажу как создается объект в Unity "изнутри". Расскажу про Unity Extensions, Strategies & Policies.

Допустим в нашем приложении есть компонент Persistence, который отвечает за сохранении объектов. Он описывается интерфейсом IPersistence и имеет реализации - FilePersistence, DbPersistence, WsPersistence, InMemoryPersistence.

В классическом варианте мы в начале приложения регистрируем нужную реализацию в Unity и далее, вызывая Resolve для IPersistence, всегда получаем ее.


Но что делать, если необходимая реализация может меняться в процессе работы приложения. Например она задается в конфиг-файле, или при недоступности сети надо автоматически использовать FilePersistence?

В Unity есть возможность регистрировать зависимости по имени. Пример:


Осталось добиться, чтобы при получении реализации без имени, Unity каким-то образом определял какую нам нужно реализацию.
Пусть у нас будет делегат, который мы передадим Unity, определявший нужное имя реализации.
Пример:


Стандартного способа в Unity для этого нет. Но мы решим проблему написанием своего расширения.

Unity Extensions
Расширение Unity - это класс, унаследованный от UnityContainerExtension. У него есть контекст расширения (ExtensionContext) и виртуальные методы Initialize() и Remove() (соответственно вызываются при инициализации и удалении расширения).
Добавляются расширения через методы контейнера AddNewExtension и AddExtension, удаляются через RemoveAllExtensions.


Чтобы расширение можно было конфигурировать, оно должно реализовывать интерфейс-конфигуратор, унаследованный от IUnityContainerExtensionConfigurator. Конфигурирование происходит через метод контейнера Configure.


Strategy
Каждый зарегистрированный тип в Unity имеет свой build-ключ (buildKey). Он состоит из зарегистрированного типа и имени, под которым он был зарегистрирован.

Процесс Resolve в Unity реализован при помощи стратегий.
Стратегия - это класс реализующий интерфейс IBuilderStrategy. Он имеет четыре метода: PreBuildUp, PostBuildUp, PreTearDown, PostTearDown.
При вызове Resolve:
  1. Создается список зарегистрированных стратегий;
  2. Формируется build-ключ искомого типа и контекст построения (BuilderContext);
  3. Контекст последовательно обрабатывается стратегиями до тех пор, пока одна из них не установит флаг BuildComplete в true.
В Unity есть 4 предопределенных стратегии, которые вызываются для каждого Resolve:
  • BuildKeyMappingStrategy. Заменяет build-ключ в контексте с искомого типа на ключ реализации. По сути весь resolving тут и происходит;
  • LifetimeStrategy. Проверяет наличие реализации в Lifetime-менеджере;
  • ArrayResolutionStrategy. Разрешение зависимостей-массивов;
  • BuildPlanStrategy. Создании экземпляра реализации (если он еще не создан) и автоматическое разрешение его зависимостей.

Мы напишем свою стратеги, которая будет подменять в build-ключе пустое имя на имя нужной реализации.


Т.к. поиск реализации по build-ключу происходит в стратегии BuildKeyMappingStrategy, то мы должны зарегистрировать свою стратегию так, чтобы она выполнилась раньше BuildKeyMappingStrategy. Стратегии сортируются в зависимости от этапа, который был указан при регистрации.
Всего есть 7 этапов - Setup, TypeMapping, Lifetime, PreCreation, Creation, Initialization, PostInitialization. BuildKeyMappingStrategy регистрируется на этап TypeMapping, значит нашу стратегию зарегистрируем на Setup. Регистрация будет происходить в методе Initialize нашего расширения.


Policies
Еще одним важным механизмом в Unity являются политики.
Политика - это интерфейс, наследуемый от IBuilderPolicy и класс его реализующий.
В интерфейсе политики можно определять методы для любых действий. Сам IBuilderPolicy пустой.
Стратегия может получить из BuilderContext политику для заданного типа по build-ключу.
Создадим свою политику для получения нового имя по build-ключу.

Используем ее в нашей стратегии.

Добавлять стратегии можно в расширении через context. Политика добавляется для конкретного ключа, или как политика по-умолчанию.
Реализуем политику получения имя через делегат:
* для интерфейса политики IResolveNamePolicy может быть несколько реализация, например через делегат, через интерфейс, через обращение к конфигу.

Добавлять политику для конкретного build-ключа будем при конфигурировании нашего расширения.


Результат
Наше расширение готово.
Теперь мы можем делать так:
* можно создать class helper для IUnityContainer чтобы можно было писать "SetNameResolver", как в начале и хотели.

Теперь при вызове Resolve:
  • Первой запускается наша стратегия;
  • Она получает политику для искомого build-ключа;
  • Если для этого build-ключа существует политика, то build-ключ заменяется в контексте на ключ с именем из политики;
  • Дальше Resolve работает так же как и раньше, но создает объект уже не для безымянного ключа, а для ключа с новым именем.

Исходный код - тут
Progg it

26 апр. 2010 г.