Главная страница   /   14.2. Управление жизненным циклом (Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Марк Симан

14.2. Управление жизненным циклом

В главе 8 обсуждался процесс управления жизненным циклом, в том числе наиболее универсальные стили существования, к примеру, Singleton и Transient. Unity поддерживает множество различных стилей существования и позволяет конфигурировать жизненные циклы всех сервисов. Продемонстрированные в таблице 14-2 стили существования являются частью API контейнера Unity.

В контейнере Unity реализации стилей существования Transient, Per Graph и Singleton эквивалентны основным стилям существования, описанным в главе 8. Поэтому я не буду тратить время на рассмотрение этих стилей существования.

Предупреждение

Несмотря на то, что стиль существования Per Resolve совпадает с описанием, приведенным в разделе 8.3.3 "Per Graph", он имеет некоторые известные дефекты, которые делают менее предпочтительным для использования.

Таблица 14-2: Стили существования Unity
Название Комментарии
Transient Этот стиль существования используется по умолчанию. Экземпляры контейнером не отслеживаются.
Container Controlled В Unity это название используется для обозначения стиля Singleton.
Per Resolve В Unity это название используется для обозначения стиля Per Graph. Экземпляры контейнером не отслеживаются.
Hierarchical Связывает жизненные циклы компонентов с дочерним контейнером (см. раздел 14.2.1).
Per Thread Для одного потока создается один экземпляр. Экземпляры контейнером не отслеживаются.
Externally Controlled Разновидность стиля существования Singleton, при котором сам контейнер содержит только хрупкую ссылку на экземпляр, позволяющую уничтожать его сборщиком мусора в случае неиспользования.

Подсказка

Используемый по умолчанию стиль существования Transient является самым безопасным вариантом, но не всегда самым эффективным. Наиболее подходящий стиль существования для потоко-безопасных сервисов – стиль Singleton, но при этом нужно не забывать явным образом регистрировать эти сервисы.

В этом разделе вы познакомитесь со способами определения стилей существования для компонентов – как с помощью кода, так и посредством XML. Кроме того, чтобы продемонстрировать, что мы не ограничены встроенными стилями существования контейнера Unity, мы рассмотрим процесс реализации пользовательского стиля существования. После прочтения этого раздела вы уже сможете использовать стили существования контейнера Unity в своем собственном приложении.

Перед тем как приступить к разработке пользовательского стиля существования, необходимо рассмотреть способы конфигурирования и использования стилей существования.

Конфигурирование стиля существования

В этом разделе мы рассмотрим способы управления стилями существования компонентов, которые применяются в контейнере Unity. Жизненный цикл компонента конфигурируется в рамках регистрации компонентов и может задаваться как в коде, так и в XML. Мы поочередно рассмотрим каждый из этих способов.

Конфигурирование стиля существования с помощью кода

Стиль существования конфигурируется с помощью перегрузки метода RegisterType, которая используется для регистрации компонентов в целом. По своей простоте она равносильна следующему коду:

container.RegisterType<SauceBéarnaise>(
	new ContainerControlledLifetimeManager());

В этом примере конкретный класс SauceBéarnaise конфигурируется в виде Singleton таким образом, что всякий раз при запрашивании SauceBéarnaise возвращается один и тот же экземпляр. Если нам необходимо преобразовать абстракцию в конкретный класс, имеющий конкретный стиль существования, можно воспользоваться еще одной похожей перегрузкой метода RegisterType:

container.RegisterType<IIngredient, SauceBéarnaise>(
	new ContainerControlledLifetimeManager());

В этом примере IIngredient преобразуется в SauceBéarnaise и конфигурируется в виде Singleton. В двух предыдущих примерах вы использовали перегрузки метода RegisterType, которые в качестве аргумента принимали экземпляр LifetimeManager. Вместо ContainerControlledLifetimeManager вы можете использовать любой другой класс, унаследованный от абстрактного класса LifetimeManager. В контейнере Unity для каждого стиля существования, описанного в таблице 14-2, есть свой LifetimeManager. Но, как вы впоследствии увидите в разделе 14.2.2, можно создать и свой собственный LifetimeManager.

Несмотря на то, что стиль Transient используется по умолчанию, мы можем задать это явным образом. Приведенные ниже примеры эквивалентны:

container.RegisterType<SauceBéarnaise>();
container.RegisterType<SauceBéarnaise>(
	new TransientLifetimeManager());

Благодаря использованию технологии конфигурирования в коде мы можем регистрировать компоненты, имеющие различные стили существования, любым способом. Несмотря на то, что это намного более гибкий способ конфигурирования компонентов, иногда для получения позднего связывания нам необходимо прибегать к XML. В этом случае мы также можем объявлять стили существования.

Конфигурирование стиля существования с помощью XML

Когда нам нужно определять компоненты в XML, нам также может понадобиться возможность конфигурировать в этом же месте их стили существования. Это можно легко сделать в рамках XML-схемы, которая уже рассматривалась в разделе 14.1.2 "Конфигурирование контейнера". Для объявления стиля существования можно использовать и необязательный элемент lifetime:

<register type="IIngredient" mapTo="Steak">
	<lifetime type="ContainerControlledLifetimeManager" />
</register>

Отличие от примера из раздела 14.1.2 "Конфигурирование контейнера" заключается в том, что теперь вы добавили необязательный элемент lifetime для того, чтобы определить, какой из LifetimeManager должен использоваться для регистрации. Чтобы сконфигурировать компонент в виде Singleton, вы устанавливаете атрибут типа, равным псевдониму ContainerControlledLifetimeManager, но вместо него могли бы использовать и квалифицированное имя типа сборки или пользовательский псевдоним, если бы вам нужно было присвоить пользовательский LifetimeManager.

Конфигурировать жизненные циклы легко как в коде, так и в XML. В обоих случаях конфигурирование жизненных циклов выполняется в довольно декларативной манере. Несмотря на то, что конфигурация выполняется довольно просто, вы не должны забывать о том, что некоторые стили существования содержат объекты-долгожители, которые пользуются ресурсами на протяжении всего своего жизненного цикла.

Высвобождение компонентов

Как уже говорилось в разделе 8.2.2 "Управление устраняемыми зависимостями", важно высвободить объекты после завершения работы с ними, чтобы каждый устраняемый экземпляр можно было бы устранить по истечении его жизненного цикла. Это возможно, но в рамках контейнера Unity сделать это довольно-таки трудно.

Предупреждение

Контейнер Unity не уничтожает устраняемые зависимости до тех пор, пока кто-нибудь явно не прикажет ему это сделать.

IUnityContainer определяет метод Teardown, который с первого взгляда кажется похожим на эквивалентный метод Release контейнера Castle Windsor. Мы можем попробовать использовать его таким же образом:

container.Teardown(ingredient);

Однако независимо от того, какой из встроенных стилей существования мы выбрали, ни один из компонентов не уничтожается. Это приводит к нарушению принципа "наименьшего удивления" (Principle of Least Surprise).

Предупреждение

Метод Teardown не уничтожает устраняемые зависимости.

Несмотря на то, что метод Teardown не выполняет (по умолчанию) то, что нам хотелось бы, нам, тем не менее, доступны некоторые другие варианты. Один из таких вариантов – реализовать пользовательский стиль существования (что вы и сделаете в следующем разделе). Еще один вариант – использовать комбинацию дочерних контейнеров и стиля существования Hierarchical.

Сущность стиля существования Hierarchical заключается в том, что в рамках дочернего контейнера он выступает в роли Singleton, но при этом каждый дочерний контейнер обладает своим собственным локальным Singleton.

Примечание

Комбинация дочерних контейнеров и стиля существования Hierarchical аналогична областям применения контейнера Autofac, описанным в разделе 13.2.1 "Конфигурирование областей применения экземпляров".

Дочерний контейнер – это копия родительского контейнера. При создании дочернего контейнера из родительского дочерний контейнер наследует из него всю конфигурацию, но впоследствии мы можем изменять дочерний контейнер, не влияя при этом на родительский. Такая возможность может быть полезной, если нам необходимо переопределить только небольшую часть конфигурации родительского контейнера. Дочерний контейнер чаще всего имеет более ограниченную область применения. Как показывает рисунок 14-4, он также определяет границу, в пределах которой могут повторно использоваться компоненты.

Рисунок 14-4: Дочерние контейнеры могут совместно использовать компоненты в течение ограниченного периода или для ограниченного круга целей. Компонент Hierarchical по существу играет роль Singleton в рамках этого контейнера. Независимо от того, сколько раз мы запрашиваем у дочернего контейнера этот компонент, мы получаем один и тот же экземпляр. Другой дочерний контейнер будет получать свой собственный экземпляр, а родительский контейнер управляет совместно используемыми Singleton'ами. Transient-компоненты нельзя использовать совместно.

При создании нового дочернего контейнера он наследует все Singleton'ы, которыми управляет родительский контейнер, но при этом выступает в роли контейнера "локальных Singleton'ов". Когда из дочернего контейнера запрашивается компонент Hierarchical, мы всегда получаем один и тот же экземпляр. Отличие от истинных Singleton'ов заключается в том, что, если мы запросим компонент Hierarchical у второго дочернего контейнера, то получим совсем другой экземпляр.

Однако Transient-компоненты функционируют так, как и должны, независимо от того, разрешаем ли мы их из родительского или из дочернего контейнера.

Подсказка

Дочерние контейнеры и стиль существования Hierarchical можно использовать в качестве еще одного варианта опускания стиля существования Web Request Context: создайте новый дочерний контейнер в начале каждого веб-запроса и используйте его для разрешения компонентов. После завершения запроса уничтожьте дочерний контейнер.

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

using (var child = container.CreateChildContainer()
{
	var meal = child.Resolve<IMeal>();
}

Строка 3: Уничтожение обеда

Новый дочерний контейнер создается из container посредством вызова метода CreateChildContainer. Возвращаемое значение реализует интерфейс IDisposable, поэтому вы можете поместить его в директиву using. Получаем новый экземпляр IUnityContainer. В связи с этим вы можете использовать child для того, чтобы разрешать компоненты точно таким же способом, как и при использовании родительского контейнера.

После окончания работы с дочерним контейнером вы можете уничтожить его. При использовании директивы using дочерний контейнер автоматически уничтожается при выходе из этой директивы. Но, безусловно, вы можете сделать это и, явно уничтожив дочерний контейнер посредством вызова метода Dispose. При уничтожении child вы также высвобождаете все компоненты, созданные дочерним контейнером. В случае приведенного выше примера это означает, что вы высвобождаете диаграмму объекта meal.

Примечание

Не забывайте, что высвобождение устраняемого компонента и его уничтожение – это не одно и то же. Это сигнал контейнеру о том, что срок эксплуатации этого компонента завершился. Если это Hierarchical-компонент, то он уничтожится автоматически, а если Singleton, то он не будет уничтожен автоматически.

Предупреждение

Устраняемые объекты со стилями существования Transient или Per Graph не уничтожаются при уничтожении дочернего контейнера. Это может привести к утечкам памяти.

Ранее в этом разделе вы уже видели, как сконфигурировать компоненты в виде Singleton или Transient. Конфигурирование компонента в виде Hierarchical выполняется аналогичным образом:

container.RegisterType<IIngredient, SauceBéarnaise>(
	new HierarchicalLifetimeManager());

При регистрации компонента с определенным стилем существования всегда используется перегрузка метода RegisterType, принимающая в качестве аргумента LifetimeManager. Чтобы использовать стиль существования Hierarchical, вы передаете в метод экземпляр HierarchicalLifetimeManager.

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

container.Dispose();

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

Стили существования, встроенные в контейнер Unity, могут показаться довольно исчерпывающим набором стилей существования, удовлетворяющим практически всем поседневным нуждам. Но когда дело доходит до высвобождения компонентов, могут возникать проблемы в виде дефектов, а также проблемы несовместимости. С другой стороны, в контейнере Unity присутствует достаточное количество Seam'ов, чтобы можно было справиться с этими проблемами посредством разработки пользовательских стилей существования.

Разработка пользовательского стиля существования

В большинстве случаев мы должны уметь выходить из разных ситуаций с помощью исчерпывающего набора стилей существования, уже предоставляемых контейнером Unity, но если у нас имеются особые нужды или нам необходимо справиться с проблемами прекращения срока эксплуатации, можно реализовать пользовательский стиль существования. В данном разделе вы увидите, как это сделать. Мы рассмотрим Seam'ы, которые делают возможным создание пользовательского стиля существования, и часть времени потратим на рассмотрение примера, перемежая теорию с практикой.

Понимание API LifetimeManager

В разделе 14.2.1 "Конфигурирование стиля существования" мы уже мельком рассматривали API стилей существования контейнера Unity. Несколько перегрузок метода RegisterType принимают в качестве параметра экземпляр абстрактного класса LifetimeManager, который моделирует процесс взаимодействия стилей существования с остальной частью контейнера Unity. На рисунке 14-5 продемонстрирована небольшая иерархия типов, связанная с классом LifetimeManager.

Рисунок 14-5: SomeLifetimeManager реализует пользовательский стиль существования посредством наследования от абстрактного класса LifetimeManager, который, в свою очередь, реализует интерфейс ILifetimePolicy, унаследованный от интерфейса IBuilderPolicy. Пользовательский стиль существования может реализовать IDisposable, чтобы внедрить функциональность постобработки, в результате которой уничтожается контейнер.

При реализации пользовательского стиля существования важнейшим типом является абстрактный класс LifetimeManager. Даже если LifetimeManager реализует ILifetimePolicy, это нас никоим образом не касается, поскольку перегрузки метода RegisterType принимают в качестве параметра только экземпляры LifetimeManager, а не экземпляры ILifetimePolicy или IBuilderPolicy.

Мы можем реализовать IDisposable, чтобы внедрить функциональность постобработки, но по умолчанию эта функциональность работает не так, как нам бы хотелось. Метод Dispose вызывается не всегда. Позднее мы еще вернемся к рассмотрению этого вопроса.

Предупреждение

То, что мы реализуем IDisposable, еще не гарантирует, что будет вызван метод Dispose.

При разрешении компонента контейнер Unity взаимодействует с LifetimeManager, что проиллюстрировано на рисунке 14-6.

Рисунок 14-6: Контейнер Unity взаимодействует с интерфейсом ILifetimePolicy, вызывая сначала метод GetValue. Если policy возвращает какое-то значение, то это значение незамедлительно используется. Если значение не возвращается, то Unity создает новое значение и устанавливает его в policy перед тем, как вернуть это значение.

Примечание

Механизм, проиллюстрированный на рисунке 14-6, аналогичен взаимодействию StructureMap с IObjectCache, которое продемонстрировано на рисунке 11-5.

Сначала Unity пытается получить запрашиваемый экземпляр из метода GetValue. Если этот метод возвращает null, то Unity создает запрашиваемый экземпляр и добавляет его в policy с помощью метода SetValue перед тем, как вернуть это значение. Таким образом, один экземпляр ILifetimePolicy управляет одним компонентом.

Предупреждение

Метод RemoveValue никогда не вызывается контейнером Unity.

Несмотря на то, что методы GetValue и SetValue принимают участие в процессе разрешения запроса контейнером Unity, метод RemoveValue никогда не вызывается контейнером. Объяснение того, почему метод Teardown не работает так, как нам бы хотелось, займет слишком времени. Мы могли бы оставить реализацию пустой, но, оказывается, мы можем изменить назначение метода. Перед детальным рассмотрением этого вопроса изучение примера, охватывающего самые основы, могло бы прояснить некоторые моменты.

Разработка стиля существования Caching

В приведенном ниже примере мы будем разрабатывать стиль существования Caching, который уже создавали для контейнеров Castle Windsor и StructureMap в разделах 10.2.3 "Разработка пользовательского стиля существования" и 11.2.2 "Разработка пользовательского стиля существования". Если кратко, то этот стиль существования кэширует и повторно в течение некоторого времени использует экземпляры перед тем, как их высвободить.

Несмотря на то, что вы можете добавить некоторую дополнительную линию поведения, реализовав IDisposable и применив некоторые уловки, вам нужно будет реализовать, как минимум, три абстрактных метода, определенных LifetimeManager. Это продемонстрировано в следующем листинге.

Листинг 14-3: Реализация пользовательского LifetimeManager
public partial class CacheLifetimeManager :
	LifetimeManager, IDisposable
{
	private object value;
	private readonly ILease lease;
	public CacheLifetimeManager(ILease lease)
	{
		if (lease == null)
		{
			throw new ArgumentNullException("lease");
		}
		this.lease = lease;
	}
	public override object GetValue()
	{
		this.RemoveValue();
		return this.value;
	}
	public override void RemoveValue()
	{
		if (this.lease.IsExpired)
		{
			this.Dispose();
		}
	}
	public override void SetValue(object newValue)
	{
		this.value = newValue;
		this.lease.Renew();
	}
}

Строка 14-18: Получение значения

Строка 19-25: Удаление значения

Строка 26-30: Установка значения

Чтобы реализовать стиль существования Caching, необходимо унаследовать класс CacheLifetimeManager от абстрактного класса LifetimeManager. Кроме того, класс CacheLifetimeManager реализует IDisposable, но мы немного повременим с изучением реализации, поэтому в листинге 14-3 метод Dispose пропущен.

CacheLifetimeManager для получения экземпляра ILease использует паттерн Constructor Injection. Интерфейс ILease – это локальный вспомогательный интерфейс, который вводится для реализации необходимой функциональности. Впервые этот интерфейс был введен в разделе 10.2.3 "Разработка пользовательского стиля существования" и никак не влияет на контейнер Unity или любой другой DI-контейнер.

Примечание

Пример реализации ILease можно увидеть в разделе 10.2.3 "Разработка пользовательского стиля существования".

Метод GetValue сначала вызывает метод RemoveValue, чтобы обезопасить себя от недействительного срока аренды, а затем возвращает значение поля value. Поле value может иметь null-значение, но, как демонстрирует рисунок 14-6, это ожидаемый сценарий. С другой стороны, в поле может содержаться значение, если сначала был вызван метод SetValue и при этом срок аренды не просрочен.

Несмотря на то, что метод RemoveValue никогда не вызывается самим Unity, это все равно отличное место для реализации кода, позволяющего высвободить компонент. Поскольку целью CacheLifetimeManager является кэширование значения на некоторое время, вы устраняете компонент только по окончании срока аренды. В противном случае вы храните его несколько дольше. Метод Dispose не включен в листинг 14-3, но мы скоро к нему вернемся.

Метод SetValue сохраняет значение в поле value и продляет срок аренды. Согласно схеме, приведенной на рисунке 14-6, метод SetValue вызывается только тогда, когда Unity создает новое значение для рассматриваемого компонента, причем в этом случае уместно продлять срок аренды.

Примечание

Сравните конструктор из листинга 14-3 с более сложным кодом, приведенным в листинге 10.2. Это сравнение отчетливо демонстрирует превосходство паттерна Constructor Injection над Method Injection.

Все это реализует ключевую функциональность, необходимую для LifetimeManager. Хотя нам все равно нужно обсудить реализацию IDisposable и то, что под ним подразумевается, нам следует для начала вкратце рассмотреть то, как CacheLifetimeManager сопоставляется с экземпляром UnityContainer.

Регистрация компонентов с пользовательским стилем существования

Применять CacheLifetimeManager в рамках компонента довольно легко и делается это наподобие определения всех остальных стилей существования:

var lease = new SlidingLease(TimeSpan.FromMinutes(1));
var cache = new CacheLifetimeManager(lease);
container.RegisterType<IIngredient, SauceBéarnaise>(cache);

В этом примере кода контейнер конфигурируется таким образом, что он с одноминутной задержкой использует CacheLifetimeManager для экземпляра IIngredient. В рамках одноминутного промежутка вы можете запрашивать какое угодно количество диаграмм объектов, и всякий раз, когда диаграмма содержит экземпляр IIngredient, вы будете получать один и тот же SauceBéarnaise. По истечении одноминутной задержки при последующих запросах будет возвращаться новый экземпляр SauceBéarnaise.

Разрешение компонентов с пользовательским стилем существования выполняется обычным образом. Сюрпризы начинаются только, когда мы пытаемся высвободить разрешенные диаграммы объектов.

Высвобождение компонентов с пользовательским стилем существования

Как я уже упоминал, метод RemoveValue никогда не вызывается контейнером Unity. Тем не менее, если нам необходимо добавить функциональность постобработки для пользовательского LifetimeManager, мы можем заставить LifetimeManager реализовать IDisposable. Это приведет к тому, что пользовательский LifetimeManager будет уничтожен при уничтожении контейнера, являющегося владельцем этого LifetimeManager.

Однако могут возникать и другие сюрпризы, но не будем заострять на них внимание. В листинге 14-3 вы уже видели, что CacheLifetimeManager реализует IDisposable, но в следующем листинге вы впервые увидите саму реализацию.

Листинг 14-4: Уничтожение LifetimeManager
public void Dispose()
{
	GC.SuppressFinalize(this);
	this.Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
	if (disposing)
	{
		var d = this.value as IDisposable;
		if (d != null)
		{
			d.Dispose();
		}
		this.value = null;
	}
}

Строка 10-15: Уничтожение устраняемого объекта

Класс CacheLifetimeManager реализует IDisposable, руководствуясь при этом стандартным паттерном Dispose. Если полученное значение реализует IDisposable, вы его уничтожаете. Но в любом случае вы устанавливаете в поле value значение null, чтобы сборщик мусора мог уничтожить этот компонент.

Согласно теории, если вы регистрируете и разрешаете устраняемый компонент с помощью CacheLifetimeManager, компонент должен быть уничтожен вместе с контейнером следующим образом:

var lease = new SlidingLease(TimeSpan.FromMinutes(1));
var cache = new CacheLifetimeManager(lease);
container.RegisterType<IIngredient, Parsley>(cache);
var ingredient = container.Resolve<IIngredient>();
container.Dispose();

Согласно документации, которая прилагается к контейнеру Unity, при устранении контейнера также выполняется высвобождение ingredient. Как известно, петрушку нельзя разогревать, поэтому класс Parsley, очевидно, является устраняемым. При уничтожении контейнера уничтожается и экземпляр Parsley. Пока все идет так, как надо.

Тем не менее, при создании и уничтожении дочернего контейнера вы будете рассчитывать на то, что устраняемый LifetimeManager будет работать так же, как и HierarchicalLifetimeManager:

IIngredient ingredient;
using (var child = container.CreateChildContainer())
{
	ingredient = child.Resolve<IIngredient>()
}

Строка 4: Ingredient не уничтожается

При наличии конфигурации компонента Parsley, аналогичной той, что продемонстрирована в предыдущем примере, вы рассчитываете на то, что ingredient будет устранен при уничтожении дочернего контейнера. Увы, этого не происходит. CacheLifetimeManager.Dispose никогда не вызывается.

Предупреждение

Даже когда LifetimeManager реализует IDisposable, метод Dispose вызывается только при уничтожении контейнера-владельца.

Как такое может происходить, если в аналогичном коде, который вы уже видели в разделе 14.2.1 "Конфигурирование стиля существования", использовался HierarchicalLifetimeManager? Оказывается, контейнер Unity обладает соответствующим BuilderStrategy, который содержит особую логику HierarchicalLifetimeManager, позволяющую использовать эту функциональность. Хорошая новость – мы можем сделать то же самое.

Реализация пользовательской LifetimeStrategy

Причиной того, что HierarchicalLifetimeManager работает должным образом, является тот факт, что Unity обладает BuilderStrategy, который создает копию HierarchicalLifetimeManager, содержащейся в родительском контейнере, и связывает ее с дочерним контейнером. Это позволяет дочернему контейнеру уничтожать LifetimeManager, когда он сам устраняется. То же самое мы можем сделать, реализовав пользовательский BuilderStrategy, продемонстрированный в следующем листинге.

Листинг 14-5: Реализация пользовательского LifetimeStrategy
public class CacheLifetimeStrategy : BuilderStrategy
{
	public override void PreBuildUp(
		IBuilderContext context)
	{
		if (context == null)
		{
			throw new ArgumentNullException("context");
		}
		IPolicyList policySource;
		var lifetimePolicy = context
			.PersistentPolicies
			.Get<ILifetimePolicy>(context.BuildKey,
				out policySource);
		if (object.ReferenceEquals(policySource,
				context.PersistentPolicies))
		{
			return;
		}
		var cacheLifetime =
				lifetimePolicy as CacheLifetimeManager;
		if (cacheLifetime == null)
		{
			return;
		}
		var childLifetime = cacheLifetime.Clone();
		context
			.PersistentPolicies
			.Set<ILifetimePolicy>(childLifetime,
				context.BuildKey);
		context.Lifetime.Add(childLifetime);
	}
}

Строка 3-4: Переопределение PreBuildUp

Строка 10-14: Получение стратегии жизненного цикла

Строка 15-19: Проверка принадлежности

Строка 20-25: Проверка типа

Строка 26: Создание копии

Строка 27-31: Смена стратегии жизненного цикла

CacheLifetimeStrategy наследуется от абстрактного класса BuilderStrategy и реализует метод PreBuildUp, который вызывается всякий раз, когда контейнер Unity создает новый экземпляр. Это дает вам возможность изменить контекст до создания объекта.

Первое, что вам необходимо сделать, – получить текущий ILifetimePolicy для компонента. Контекст может предоставить эту информацию, а также информацию об источнике стратегии. Экземпляр policySource косвенным образом сообщает вам о том, где определен стиль существования. Если источником является родительский контейнер, но в настоящий момент вы выполняете настройку в рамках дочернего контейнера, то предполагается, что источник стиля существования будет отличаться от текущего контекста, если стиль существования первоначально был определен в родительском контейнере. Это то, что нам надо, поэтому вы преждевременно выходите из метода.

Эта конкретная реализация затрагивает только CacheLifetimeManager, поэтому если в качестве стиля существования используется какой-то другой стиль, вы также выходите из метода. С другой стороны, если в качестве стиля существования используется стиль CacheLifetimeManager, то вы создаете его копию, которая будет использоваться в дочернем контейнере.

Если вы точно уверены в том, что на данный момент выполняете настройку в рамках дочернего контейнера, то добавляете копию CacheLifetimeManager обратно в контекст, фактически, заменяя унаследованный из родительского контейнера стиль существования на стиль, необходимый для этого конкретного дочернего контейнера.

Это достаточно сложно, и потом, вы еще даже не все до конца выполнили. Несмотря на то, что вы реализовали пользовательский BuilderStrategy, вы еще не сообщили об этом Unity. К счастью, как демонстрирует следующий листинг, это намного проще, чем реализация CacheLifetimeStrategy.

Листинг 14-6: Расширение Unity с помощью CacheLifetimeStrategy
public class CacheLifetimeStrategyExtension : UnityContainerExtension
{
	protected override void Initialize()
	{
		this.Context.Strategies
			.AddNew<CacheLifetimeStrategy>(
				UnityBuildStage.Lifetime);
	}
}

Чтобы добавить CacheLifetimeStrategy в контейнер Unity, вы создаете новое расширение контейнера. Помните, как вы использовали расширения контейнера для пакетирования конфигурации в разделе 14.1.3 "Пакетирование конфигурации"? Здесь же представлен еще один пример, возможно, более идиоматического применения расширения контейнера.

В методе Initialize вы добавляете контекст CacheLifetimeStrategy, наряду с информацией о том, что этот конкретный BuilderStrategy предназначен для управления жизненным циклом.

Наконец, выполнив все это, вы можете расширить контейнер Unity таким образом, что CacheLifetimeManager теперь функционирует точно так же, как и HierarchicalLifetimeManager:

container.AddNewExtension<CacheLifetimeStrategyExtension>();

После добавления этого расширения контейнера сценарий, который ранее не работал, в конце концов, становится работоспособным: для высвобождения объектов со стилем CacheLifetimeManager вы можете воспользоваться дочерними контейнерами. Теперь, когда вы изучили BuilderStrategy, мы можем закончить цикл обучения и реализовать поддержку метода Teardown.

Реализация поддержки метода Teardown

Когда мы в разделе 14.2.1 "Конфигурирование стиля существования" приступали к обсуждению процесса высвобождения компонентов, мы сразу же опустили метод Teardown, поскольку он не высвобождал компоненты должным образом. С другой стороны, это не должно было привести вас к мысли о том, что метод Teardown бесполезен. Напротив, он вызывает различные методы зарегистрированных BuilderStrategy. Это означает, что мы можем реализовать пользовательский BuilderStrategy, который должным образом высвобождает компоненты в рамках Teardown.

Было бы хорошо, если бы Teardown поддерживал CacheLifetimeManager. К счастью, несмотря на то, что поддержка CacheLifetimeManager подразумевает создание еще одного BuilderStrategy (или расширение уже созданного нами BuilderStrategy), листинг 14-7 демонстрирует, что это намного проще, чем реализация CacheLifetimeStrategy, приведенного в листинге 14-5.

Листинг 14-7: Реализация стратегии высвобождения
public class CacheReleasingLifetimeStrategy : BuilderStrategy
{
	public override void PostTearDown(
		IBuilderContext context)
	{
		if (context == null)
		{
			throw new ArgumentNullException("context");
		}
		var lifetimes = context
			.Lifetime.OfType<CacheLifetimeManager>();
		foreach (var lifetimePolicy in lifetimes)
		{
			lifetimePolicy.RemoveValue();
		}
	}
}

Строка 3-4: Реализация PostTearDown

Строка 6-9: Граничный оператор

Строка 10-15: Высвобождение значения

Вместо того, чтобы переопределять метод PreBuildUp, как вы поступали в листинге 14-5, вы переопределяете метод PostTearDown, который вызывается из метода TearDown после того, как списывается большинство остальных ресурсов рассматриваемого компонента.

Контекст имеет целый список объектов, определяющих стили существования, но, предполагается, что в большинстве случаев контекст содержит только один единственный экземпляр CacheLifetimeManager. Все-таки, для чистоты эксперимента, вы делаете вид, будто в контексте может присутствовать любое количество таких объектов, и вызываете метод RemoveValue для каждого из них. Как вы можете помнить из реализации CacheLifetimeManager, приведенной в листинге 14-3, метод RemoveValue удаляет и устраняет соответствующее значение только по истечении срока аренды.

И вот вы уже близки к завершению цикла. Можете добавить CacheReleasingLifetimeStrategy к CacheLifetimeStrategyExtension, приведенному в листинге 14-6:

this.Context.Strategies
		.AddNew<CacheLifetimeStrategy>(
			UnityBuildStage.Lifetime);
this.Context.Strategies
		.AddNew<CacheReleasingLifetimeStrategy>(
			UnityBuildStage.Lifetime);

В конце концов, это позволяет вам высвободить кэшированные компоненты с помощью метода Teardown:

container.AddNewExtension<CacheLifetimeStrategyExtension>();
var lease = new SlidingLease(TimeSpan.FromTicks(1));
var cache = new CacheLifetimeManager(lease);
container.RegisterType<IIngredient, Parsley>(cache);
var ingredient = container.Resolve<IIngredient>();
container.Teardown(ingredient);

После создания такой инфраструктуры переменная ingredient, которая на самом деле является экземпляром устраняемого класса Parsley, должным образом высвобождается при вызове метода Teardown. По истечении срока аренды экземпляр уничтожается, тем не менее, если срок аренды не истек, то ничего не происходит.

После того как мы добавили все эти LifetimeManager, BuilderStrategy и расширения контейнера Unity наконец-то начинает функционировать так, как нам и нужно, то есть, та его часть, которая относится к стилю существования cache. Вспомните, что стили Transient и Per Graph все равно не ведут себя так, как нам бы того хотелось.

Подсказка

Несмотря на то, что ни TransientLifetimeManager, ни PerResolveLifetimeManager не реализуют IDisposable и в их методах RemoveValue не исполняется никакой логики, они, по крайней мере, не изолированы. Если мы хотим, чтобы они должным образом высвобождали компоненты, то можем выполнить наследование от этих классов, а затем не забыть реализовать соответствующие BuilderStrategy.

В заключении, мы должны признать, что хотя Unity и обладает множеством Seam'ов, необходимых для желаемой нами реализации пользовательских стилей существования, сделать это очень сложно. С другой стороны, это хотя бы возможно, чего мы не можем сказать о некоторых других DI-контейнерах.

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

На этом наш обзор механизма управления жизненным циклом контейнера Unity подошел к концу. По сравнению с остальными разделами, посвященными конкретным DI-контейнерам, этот раздел довольно объемный. Частично это зависит от множества различных возможностей, доступных нам при реализации пользовательских стилей существования. С другой стороны, такой объем объясняется некоторыми уникальными ловушками, связанными с механизмом управления жизненным циклом, которые мне хотелось бы рассмотреть. Конфигурировать компоненты можно и посредством сочетания различных стилей существования. Это справедливо и при регистрации составных реализаций одной и той же абстракции. Мы уже рассматривали процесс работы с составными компонентами, но в следующем разделе этот вопрос обсуждается более углубленно. Unity позволяет нам более подробно изучить этот процесс, поскольку он поддерживает механизм перехвата. Следующий раздел можно рассматривать как расширение обсуждения Decorator'ов.