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

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

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

Марк Симан

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

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

Таблица 11-2: Стили существования StructureMap
Название Комментарии
PerRequest Название стиля существования Per Graph, используемое в рамках StructureMap. Этот стиль используется им по умолчанию. Экземпляры контейнером не отслеживаются.
Singleton Стандартный Singleton
HttpContext Название стиля существования Web Request Context, используемое в рамках StructureMap.
ThreadLocal На один поток создается один экземпляр.
Hybrid Комбинация HttpContext и ThreadLocal. HttpContext используется тогда, когда он доступен (например, когда контейнер размещается в веб-приложении), а ThreadLocal используется в качестве резерва.
HttpSession На одну HTTP-сессию создается один экземпляр. Используйте его с осторожностью.
HybridHttpSession Комбинация HttpSession и ThreadLocal. HttpSession используется тогда, когда он доступен (например, когда контейнер размещается в веб-приложении), а ThreadLocal используется в качестве резерва.
Unique Название стиля существования Transient, используемое в рамках StructureMap.

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

Подсказка

Стиль существования, используемый в StructureMap по умолчанию, – это Per Graph. Как мы уже обсуждали в разделе 8.3.3 "Per Graph", данный стиль предлагает наилучший баланс между эффективностью и безопасностью. Кроме того, если ваши сервисы потоко-безопасны, то наиболее эффективный стиль существования в этом случае – Singleton, но при этом вы должны не забывать конфигурировать такие сервисы.

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

После прочтения этого раздела вы должны будете уметь использовать стили существования StructureMap в своем собственном приложении.

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

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

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

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

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

container.Configure(r =>
	r.For<SauceBéarnaise>().Singleton());

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

container.Configure(r =>
	r.For<IIngredient>().Singleton().Use<SauceBéarnaise>());

Данный код преобразует IIngredient в SauceBéarnaise, а также конфигурирует его в виде Singleton. Существуют и другие методы, аналогичные методу Singleton, которые позволяют нам объявить множество других стилей существования. Но не все стили существования обладают таким методом. Все стили существования могут конфигурироваться при помощи универсального метода LifecycleIs. К примеру, стиль существования Unique не имеет такого метода, но может быть сконфигурирован следующим образом:

container.Configure(r => r
	.For<SauceBéarnaise>()
	.LifecycleIs(new UniquePerRequestLifecycle()));

Метод LifecycleIs принимает в качестве параметра экземпляр ILifecycle, поэтому вы можете передать его в любой класс, реализующий этот интерфейс. Как вы увидите в разделе 11.2.2 "Разработка пользовательского стиля существования", таким же способом мы конфигурируем компонент, имеющий пользовательский жизненный цикл.

Все встроенные в StructureMap стили существования обладают соответствующей реализацией ILifecycle, за исключением используемого по умолчанию стиля существования Per Graph. Этот стиль существования обычно неявным образом конфигурируется за счет опускания явного стиля существования. Во всех конфигурациях, которые вы видели в разделе 11.1 "Знакомство со StructureMap", использовался стиль существования Per Graph.

Подсказка

Опускание объявления стиля существования подразумевает Per Graph, который используется в StructureMap по умолчанию. Но null на месте экземпляра ILifecycle также подразумевает Per Graph.

Если мы создаем некоторого рода универсальный код, который принимает в качестве входной информации экземпляр ILifecycle и передает его в метод LifecycleIs, то можем использовать его для конфигурирования компонента с помощью стиля существования Per Graph. Наличие null подразумевает Per Graph, поэтому два приведенных ниже примера, эквивалентны:

container.Configure(r => r 
	.For<IIngredient>() 
	.LifecycleIs(null) 
	.Use<SauceBéarnaise>());

и

container.Configure(r => r
	.For<IIngredient>()
	.Use<SauceBéarnaise>());

Подсказка

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

В то время как API, раскрывающееся при помощи метода Configure и ConfigurationExpression, позволяет нам явным образом объявлять стиль существования, Scan API, основанное на соглашениях, не позволяет нам сделать это. В интерфейсе IAssemblyScanner нет ни одного метода, который дал бы нам возможность одним махом явно объявить стиль существования для набора компонентов.

Тем не менее, мы можем реализовать простой IRegistrationConvention, который может объявить стиль существования для набора компонентов одним махом. Ниже приведен пример использования экземпляра IRegistrationConvention под названием SingletonConvention:

container.Configure(r =>
	r.Scan(s =>
	{
		s.AssemblyContainingType<Steak>();
		s.AddAllTypesOf<IIngredient>();
		s.Convention<SingletonConvention>();
	}));

Обратите внимание на то, что здесь приведена такая же конфигурация, как и в первом примере автоматической регистрации из раздела 11.1.2 "Конфигурирование контейнера". Вы добавили только одну строку кода, которая добавляет SingletonConvention, продемонстрированный в приведенном ниже листинге.

Листинг 11-3: Реализация соглашения по объявлению стиля существования
public class SingletonConvention : IRegistrationConvention
{
	public void Process(Type type, Registry registry)
	{
		registry.For(type).Singleton();
	}
}

Если вы помните предыдущее обсуждение IRegistrationConvention из листинга 11-1, то помните и то, что метод Process вызывается в сборке операции Scan для каждого включенного типа. В этом случае единственное, что вам необходимо сделать – объявить стиль существования для каждого типа, используя метод Singleton. Таким образом, мы сконфигурируем каждый тип в виде Singleton.

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

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

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

<DefaultInstance PluginType="Ploeh.Samples.MenuModel.IIngredient,
								➥Ploeh.Samples.MenuModel"
								PluggedType="Ploeh.Samples.MenuModel.Steak,
								➥Ploeh.Samples.MenuModel"
								Scope="Singleton" />

Единственное отличие этого примера от примера из раздела 11.1.2 – добавленный атрибут, который конфигурирует экземпляр в виде Singleton. Когда вы ранее опускали атрибут Scope, автоматически применялся Per Graph, используемый в StructureMap по умолчанию.

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

Предотвращение утечек памяти

Как и любой другой DI-контейнер StructureMap создает для нас диаграмму объектов, но не отслеживает созданные объекты. Он может отслеживать эти объекты в своих собственных целях, но зависит это от жизненного цикла объекта. К примеру, для того чтобы реализовать область применения Singleton, StructureMap должен сохранять ссылку на созданный экземпляр. Это справедливо и для стиля существования HttpContext, в котором все экземпляры хранятся в HttpContext.Current.Items. Тем не менее, после завершения HTTP-запроса все эти экземпляры выходят за пределы области применения и могут быть уничтожены сборщиком мусора.

С другой стороны, стили существования Per Graph и Transient не отслеживают созданные StructureMap объекты. Как вы видели в листингах 8-7 и 8-8, экземпляры объектов создаются и возвращаются без внутреннего сопровождения. Это имеет некоторые преимущества и недостатки.

Поскольку StructureMap особо не держится за экземпляры, риск появления неумышленных утечек памяти в этом случае намного меньше. Для такого контейнера, как Castle Windsor, утечки памяти будут гарантированно возникать, если мы забудем вызвать метод Release для всех разрешенных диаграмм объектов. Такого не происходит со StructureMap, поскольку, как только объекты выйдут за рамки области применения, они будут уничтожены сборщиком мусора.

Недостаток заключается в том, что устраняемые объекты не могут детерминированно уничтожаться. Так как мы не можем явным образом высвобождать диаграмму объектов, мы не можем уничтожать какие-либо устраняемые объекты. Это означает, что обертывание устраняемых API в неустраняемые сервисы, которое обсуждалось в разделе 6.2.1, становится еще более значимым.

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

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

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

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

Понимание API стиля существования

В разделе 11.1.2 "Конфигурирование контейнера" вы уже получили некоторое представление об API стилей существования StructureMap. Метод LifecycleIs принимает в качестве параметра экземпляр интерфейса ILifecycle, который моделирует то, как стили существования взаимодействуют с остальной частью контейнера StructureMap:

public interface ILifecycle
{
	string Scope { get; }
	void EjectAll();
	IObjectCache FindCache();
}

Среди этих трех методов центральным методом является FindCache. Он возвращает кэш, который StructureMap использует для поиска и вставки объектов, имеющих конкретный стиль существования. Интерфейс ILifecycle, главным образом, выступает в роли абстрактной фабрики для экземпляров IObjectCache, в которых содержится реализация стиля существования. Этот интерфейс довольно-таки сложен, но его не столь сложно реализовать:

public interface IObjectCache
{
	object Locker { get; }
	int Count { get; }
	bool Has(Type pluginType, Instance instance);
	void Eject(Type pluginType, Instance instance);
	object Get(Type pluginType, Instance instance);
	void Set(Type pluginType, Instance instance, object value);
	void DisposeAndClear();
}

Большинство методов данного интерфейса имеют дело с поиском, передачей или удалением экземпляра на основании Type и Instance. Рисунок 11-5 иллюстрирует, каким образом StructureMap взаимодействует с реализацией IObjectCache.

Рисунок 11-5: StructureMap взаимодействует с интерфейсом IObjectCache, в первую очередь, вызывая метод Get для объекта-кэша. Если кэш возвращает значение, то данное значение используется незамедлительно. В противном случае StructureMap создает новое значение и добавляет это значение в кэш перед тем, как его вернуть.

Примечание

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

Сначала StructureMap пытается получить запрашиваемый экземпляр из метода Get. Если этот метод возвращает значение null для предоставленных Type и Instance, то StructureMap создает запрашиваемый экземпляр и перед тем, как его вернуть, добавляет этот экземпляр в кэш посредством метода Set.

Давайте на примере рассмотрим, как это работает.

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

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

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

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

Давайте начнем с самой простой составляющей. Следующий листинг демонстрирует реализацию интерфейса ILifecycle.

Листинг 11-4: Реализация ILifecycle
public partial class CacheLifecycle : ILifecycle
{
	private readonly LeasedObjectCache cache;
	public CacheLifecycle(ILease lease)
	{
		if (lease == null)
		{
			throw new ArgumentNullException("lease");
		}
		this.cache = new LeasedObjectCache(lease);
	}
	public void EjectAll()
	{
		this.FindCache().DisposeAndClear();
	}
	public IObjectCache FindCache()
	{
		return this.cache;
	}
	public string Scope
	{
		get { return "Cache"; }
	}
}

Строка 10: Сохраняет lease в пользовательском cache

Строка 12, 16, 20: Члены ILifecycle

Строка 18: Возвращает пользовательский cache

Класс CacheLifecycle, как и требуется, реализует интерфейс ILifecycle. Для получения экземпляра ILease он использует паттерн Constructor Injection. Интерфейс ILease – локальный вспомогательный (helper) интерфейс, который вводится для реализации CacheLifecycle. Первоначально этот интерфейс был введен в разделе 10.2.3 "Разработка пользовательского стиля существования" и не имеет никакого отношения ни к StructureMap, ни к любому другому DI-контейнеру.

Примечание

Чтобы увидеть пример реализации ILease, взгляните на раздел 10.2.3.

Вместо того чтобы сохранять экземпляр ILease напрямую в приватном поле, вы незамедлительно оборачиваете его в пользовательскую реализацию интерфейса IObjectCache под названием LeasedObjectCache. Это тот кэш, который возвращается методом FindCache.

Примечание

Сравните конструктор из листинга 11-4 с намного более сложным кодом листинга 10-2. Данное сравнение ясно иллюстрирует превосходство Constructor Injection над Method Injection.

Несмотря на то, что CacheLifecycle предоставляет исходный интерфейс ILifecycle, истинная реализация обеспечивается при помощи пользовательского класса LeasedObjectCache, который реализует интерфейс IObjectCache.

StructureMap уже предоставляет реализацию IObjectCache под названием MainObjectCache. К несчастью, MainObjectCache не имеет ни одного виртуального члена, который мы могли бы переопределить для того, чтобы реализовать стиль существования Caching. Вместо этого мы можем обернуть MainObjectCache пользовательским LeasedObjectCache. В следующем листинге продемонстрирован конструктор.

Листинг 11-5: Конструирование LeasedObjectCache
private readonly IObjectCache objectCache;
private readonly ILease lease;
public LeasedObjectCache(ILease lease)
{
	if (lease == null)
	{
		throw new ArgumentNullException("lease");
	}
	this.lease = lease;
	this.objectCache = new MainObjectCache();
}

В конструкторе LeasedObjectCache вы используете стандартный Constructor Injection для того чтобы внедрить экземпляр ILease. LeasedObjectCache – это Decorator для MainObjectCache, поэтому вы создаете экземпляр и присваиваете его приватному полю. Обратите внимание на то, что поле objectCache объявлено как IObjectCache, поэтому вы могли бы просто расширить класс LeasedObjectCache перегруженным конструктором, который позволял бы вам внедрять любую реализацию IObjectCache из вне.

Комбинация обернутого IObjectCache и члена ILease приближает реализацию класса LeasedObjectCache к тривиальной. Следующий листинг демонстрирует реализацию важных методов Get и Set, а остальная реализация руководствуется тем же самым проектом.

Листинг 11-6: Реализация методов Get и Set
public object Get(Type pluginType, Instance instance)
{
	this.CheckLease();
	return this.objectCache.Get(pluginType, instance);
}
public void Set(Type pluginType, Instance instance, object value)
{
	this.objectCache.Set(pluginType, instance, value);
	this.lease.Renew();
}
private void CheckLease()
{
	if (this.lease.IsExpired)
	{
		this.objectCache.DisposeAndClear();
	}
}

Когда StructureMap вызывает метод Get, вы, в первую очередь, удостоверяетесь в том, что кэш не содержит какие-либо просроченные экземпляры. При возврате этим методом результата вы можете быть уверены в том, что, если обернутый кэш содержит запрашиваемый экземпляр, вы сможете безопасно его вернуть.

Наоборот, при вызове метода Set вы тотчас же делегируете этот метод обернутому объекту-кэшу. Поскольку вы понимаете, что StructureMap использует IObjectCache так, как показано на рисунке 11-5, вы знаете, что метод Set вызывается только тогда, когда контейнер создает новый экземпляр, поскольку ни один экземпляр кэша недоступен. Это означает, что экземпляр, переданный при помощи параметра value, представляет собой только что созданный экземпляр, поэтому вы можете безопасно обновить срок аренды.

Вспомогательный метод CheckLease вызывается многими реализациями члена IObjectCache способами, аналогичными методу Get. Он игнорирует обернутый кэш, если срок его аренды истек.

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

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

Использовать CacheLifecycle при конфигурировании компонента легко, а выполняется это таким же самым способом, которым бы вы конфигурировали любой другой стиль существования:

var lease = new SlidingLease(TimeSpan.FromMinutes(1));
var cache = new CacheLifecycle(lease);
container.Configure(r => r
	.For<IIngredient>()
	.LifecycleIs(cache)
	.Use<SauceBéarnaise>());

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

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

container.Configure(r =>
{
	r.For<IIngredient>().LifecycleIs(cache).Use<Steak>();
	r.For<ICourse>().LifecycleIs(cache).Use<Course>();
});

Это будет приводить к тому, что экземпляры ICourse и IIngredient будут просрочены и вновь созданы в одно и то же время. Иногда это может быть нужно, а иногда и нет. Альтернативный вариант – использовать два отдельных экземпляра CacheLifecycle. Как показывает следующий листинг, такая возможность также позволяет вам использовать два различных таймаута.

Листинг 11-7: Использование разных стилей существования Cache для каждого Instance
container.Configure(r => r
	.For<IIngredient>()
	.LifecycleIs(
		new CacheLifecycle(
			new SlidingLease(
				TimeSpan.FromHours(1))))
	.Use<Steak>());
container.Configure(r => r
	.For<ICourse>()
	.LifecycleIs(
		new CacheLifecycle(
			new SlidingLease(
				TimeSpan.FromMinutes(15))))
	.Use<Course>());

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

Cache для ICourse – это другой экземпляр, сконфигурированный с 15-минутным таймаутом. В течение этих 15 минут вы будете получать один и тот же экземпляр, но когда они истекут, будет использоваться новый экземпляр. Стоит отметить, что даже когда время ICourse истекает, IIngredient продолжает существовать благодаря своему более длительному сроку аренды. Несмотря на то, что ICourse и IIngredient используют один и тот же стиль существования type, они имеют разные расписания.

В листинге 11-7 вы использовали разные таймауты, но при этом в обоих случаях применяли тип SlidingLease. Это не является обязательным условием, вы могли бы использовать две совершенно разные реализации ILease для каждого экземпляра.

Реализация пользовательского стиля существования для StructureMap не столь сложна. В теории она может казаться довольно сложной, но если бы вы рассмотрели этот процесс в интегрированной среде разработки (IDE), то быстро поняли бы, что реализация пользовательского стиля существования состоит всего лишь из двух классов, в которых самый сложный метод (CheckLease) имеет только один оператор if и состоит из двух строк кода.

Все-таки необходимость реализации пользовательского стиля существования для StructureMap должна встречаться довольно редко. Исчерпывающий набор встроенных стилей существования должен удовлетворять нашим повседневным нуждам.

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