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

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

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

Марк Симан

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

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

Таблица 10-2: Стили существования Castle Windsor
Название Комментарии
Singleton Этот стиль существования используется в Castle Windsor по умолчанию.
Transient Каждый раз создается новый экземпляр, но экземпляр все равно отслеживается контейнером.
PerThread На один поток создается один экземпляр.
PerWebRequest Необходима регистрация в web.config (см. раздел 10.2.2 "Использование продвинутых стилей существования").
Pooled Чаще всего будет уместно конфигурировать размер пула (см. раздел 10.2.2).
Custom Создайте свой собственный пользовательский стиль существования (см. раздел 10.2.3 "Разработка пользовательского стиля существования").

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

Примечание

Стиль существования, используемый в Castle Windsor по умолчанию, – это Singleton. Это отличает его от многих других контейнеров. Как мы уже обсуждали в главе 8, Singleton – самый эффективный, но, тем не менее, не всегда самый безопасный из всех стилей существования. В Castle Windsor по умолчанию эффективность имеет приоритет над безопасностью.

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

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

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

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

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

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

container.Register(Component
	.For<SauceBéarnaise>()
	.LifeStyle.Transient);

Обратите внимание на то, что вы определяете стиль существования при помощи свойства Lifestyle. В данном примере вы устанавливаете в качестве стиля существования стиль Transient. Таким образом, каждый раз, когда разрешается SauceBéarnaise, возвращается новый экземпляр.

Вы все равно можете явным образом определить стиль существования Singleton, даже если он и является используемым по умолчанию. Два примера, приведенные ниже, являются эквивалентными:

container.Register(Component
	.For<SauceBéarnaise>()
	.LifeStyle.Singleton);

и

container.Register(Component
	.For<SauceBéarnaise>());

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

Точно так же, как мы можем конфигурировать компоненты в коде и XML, мы можем конфигурировать стили существования и в коде, и в XML.

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

В разделе 10.1.2 "Конфигурирование контейнера" вы видели, как конфигурировать компоненты с помощью XML, но не применяли никакого стиля существования. Как и в случае с конфигурированием компонента при помощи Fluent Registration API, Singleton является используемым по умолчанию стилем существования, но при необходимости вы можете явно определить другой стиль существования:

<component id="ingredient.sauceBéarnaise"
				service="IIngredient"
				type="Steak"
				lifestyle="transient" />

Единственное отличие от примера из раздела 10.1.2 "Конфигурирование контейнера" заключается в добавленном атрибуте lifestyle. Как вы видите, определение стиля существования легко выполняется и с помощью кода, и при помощи XML.

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

Как мы уже обсуждали в разделе 8.2.2 "Управление устраняемыми зависимостями", важно высвободить объекты после того, как мы завершили работу с ними. Это также просто, как и вызов метода Release:

container.Release(ingredient);

Данный код будет высвобождать экземпляр, предоставленный в метод Release (переменная ingredient из предыдущего примера), а также все те зависимости экземпляра, жизненный цикл которых завершился. То есть, если экземпляр обладает Transient зависимостью, то эта зависимость будет высвобождена (и возможно уничтожена), тогда как Singleton зависимость останется в контейнере.

Подсказка

Castle Windsor отслеживает все, даже Transient компоненты, поэтому важно, не забывать высвобождать все разрешенные экземпляры для того, чтобы избежать утечек памяти.

Подсказка

Высвобождайте явно то, что вы явно разрешаете.

Подсказка

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

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

Использование продвинутых стилей существования

В данном разделе мы рассмотрим два стиля существования Castle Windsor, для которых необходимо больше конфигурации, нежели простое объявление: Pooled и PerWebRequest.

Использование стиля существования Pooled

В разделе 8.3.5 "Pooled" мы рассматривали общую концепцию стиля существования Pooled. В данном разделе мы увидим, как использовать реализацию Windsor. Стиль существования Pooled в Castle Windsor уже идет с заданным по умолчанию размером пула, но, поскольку оптимальный размер пула всецело зависит от обстоятельств, вам следует явно сконфигурировать размер пула. Вы можете определить стиль существования Pooled с заданными по умолчанию размерами таким же способом, как вы конфигурируете любой другой стиль существования:

container.Register(Component
	.For<IIngredient>()
	.ImplementedBy<SauceBéarnaise>()
	.LifeStyle.Pooled);

Тем не менее, данный код не передает размер пула. Несмотря на то, что я не смог найти какую-либо документацию, в которой указано, каковы значения пула по умолчанию, исходный код Castle Windsor 2.1.1 показывает, что по умолчанию первоначальный размер пула равен 5, а максимальный размер – 15. Для меня эти значения кажутся довольно произвольными, что является еще одной причиной определения размера явным образом.

Для того чтобы явным образом сконфигурировать размеры пула, вы можете использовать метод PooledWithSize:

container.Register(Component
	.For<IIngredient>()
	.ImplementedBy<SauceBéarnaise>()
	.LifeStyle.PooledWithSize(10, 50));

Данный пример устанавливает первоначальный размер – 10, а максимальный размер – 50. Пулы Castle Windsor имеют два конфигурационных значения: первоначальный размер и максимальный размер. Первоначальный размер, очевидно, регулирует первоначальный размер пула, а максимальный размер – максимальный размер пула, но в крайних случаях поведение может быть неожиданным. Рисунок 10-4 демонстрирует, как эволюционирует размер пула в течение жизненного цикла контейнера.

Рисунок 10-4: Прогрессия размера пула с первоначальным размером, равным 3, и максимальным размером, равным 5. Даже если первоначальный размер равен 3, пул остается пустым до тех пор, пока не разрешается первый экземпляр. На данном этапе созданы все три экземпляра для первоначального размера, и один из них непосредственно используется. Когда экземпляр высвобождается, он возвращается в пул. Пул увеличивается в размере, если необходимо больше экземпляров, чем для первоначального размера. Обратите внимание на то, что можно превысить максимальный размер, но что излишние экземпляры не используются повторно при высвобождении.

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

container.Release(ingredient);

Примечание

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

Несмотря на то, что стиль существования Pooled более продвинут, чем Singleton или Transient, его все равно легко использовать. Единственное дополнительное усилие, которое вам необходимо сделать – передать два дополнительных числа для того, чтобы сконфигурировать размеры пула. Стиль существования PerWebRequest не сильно отличается от Pooled, но его несколько сложнее конфигурировать.

Использование стиля существования PerWebRequest

Как и подразумевает его имя, работа стиля существования PerWebRequest заключается в создании экземпляра для веб-запроса. Объявление этого стиля столь же просто, как и объявление стиля существования Transient:

container.Register(Component
	.For<IIngredient>()
	.ImplementedBy<SauceBéarnaise>()
	.LifeStyle.PerWebRequest);

Тем не менее, если мы попытаемся использовать его без дальнейшей конфигурации, то получим исключение:

Looks like you forgot to register the HTTP module Castle.MicroKernel.Lifestyle.PerWeb-RequestLifestyleModule

Add ‘<add name="PerRequestLifestyle" type="Castle.MicroKernel.Lifestyle.PerWebRequest-LifestyleModule, Castle.Windsor" />’ to the <httpModules> section on your web.config. If you’re running IIS7 in Integrated Mode you will need to add it to <modules> section under <system.webServer>

(Похоже, вы забыли зарегистрировать HTTP-модуль Castle.MicroKernel.Lifestyle.PerWebRequestLifestyleModule

Добавьте ‘<add name="PerRequestLifestyle" type="Castle.MicroKernel.Lifestyle.PerWebRequestLifestyleModule, Castle.Windsor" />’ в раздел <httpModules> вашего файла web.config. Если IIS7 запущен в интегрированном режиме, вам нужно будет добавить этот код в раздел <modules> после <system.webServer>)

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

В любом случае, важно отметить, что стиль существования PerWebRequest использует HTTP-модуль для того, чтобы указать, какой веб-запрос на данный момент выполняется. Одно из последствий этого – нам необходимо зарегистрировать HTTP-модуль так, как я это описывал выше, другое – данный конкретный стиль существования работает только в рамках веб-запросов. Если мы попытаемся использовать его в других типах приложений, мы получим то же самое исключение, что и в предыдущем примере.

Как и стиль существования Pooled, так и стиль существования PerWebRequest, требуют использования немного большего разнообразия задач, нежели простое объявление, но при этом их все равно легко конфигурировать и использовать. Встроенные стили существования Castle Windsor предоставляют обширный и полезный набор стилей существования, который удовлетворяет большинству сценариев. Однако если ни один из этих стилей существования не удовлетворяет специализированным нуждам, мы можем создать пользовательский стиль существования.

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

В большинстве случаев для наших нужд должны подходить встроенные стили существования Castle Windsor, но если нам нужно что-то особенное, мы можем создать пользовательский стиль существования.

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

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

Вы можете создать пользовательский стиль существования путем реализации интерфейса ILifestyleManager:

public interface ILifestyleManager : IDisposable
{
	void Init(IComponentActivator componentActivator,
		IKernel kernel, ComponentModel model);
	bool Release(object instance);
	object Resolve(CreationContext context);
}

Одним из немного странных условий реализации ILifestyleManager является тот факт, что он должен иметь конструктор по умолчанию. То есть использовать Constructor Injection при реализации ILifestyleManager запрещено. Вместо этого нам предоставляется одна из относительно редких возможностей использовать Method Injection. Будет вызван метод Init, предоставляя среди прочих параметров экземпляр IKernel, который можно использовать в качестве Service Locator. Это мне совершенно не подходит, и когда мы будем рассматривать некоторые примеры кода, вы увидите, что такой подход в большей степени усложняет реализацию, чем, если бы было возможным использование Constructor Injection.

Другие методы интерфейса ILifestyleManagerResolve и Release, но нам следует использовать их в качестве перехватчиков, а не предоставлять свои собственные реализации Resolve и Release – за это отвечает IComponentActivator, переданный нам в методе Init. Рисунок 10-5 демонстрирует, что для обеспечения возможности управления жизненным циклом каждого компонента мы должны использовать эти методы только для перехвата вызовов Resolve и Release.

Рисунок 10-5: ILifestyleManager выступает в роли некоторого рода перехватчика, который вызывается вместо упомянутого выше IComponentActivator. Предполагается, что реализация ILifestyleManager использует предоставленный IComponentActivator для создания экземпляров объектов. Поскольку ILifestyleManager располагается посередине, он получает возможность перехватывать каждый вызов и исполнять свою собственную логику стиля существования. Можно повторно использовать экземпляры вместо того, чтобы каждый раз вызывать IComponentActivator.

Castle Windsor обеспечивает реализацию по умолчанию ILifestyleManager в виде класса AbstractLifestyleManager. Он реализует интерфейс и предоставляет обоснованную реализацию по умолчанию для большинства методов. Именно этот класс вы будете использовать для реализации шаблонного стиля существования.

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

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

Примечание

Стиль существования Caching – выдуманный пример. Существуют лучшие способы реализации функциональности кэширования, поскольку обычно вы хотите кэшировать не сервисы, а данные, которыми управляют эти сервисы.

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

Продемонстрированный в данном разделе шаблонный код не принимает во внимание потоко-безопасность, но реальная реализация ILifestyleManager должна быть потоко-безопасной.

Самый легкий способ реализации пользовательского стиля существования – наследование от AbstractLifestyleManager, что продемонстрировано в следующем листинге.

Листинг 10-2: Определение пользовательского стиля существования
public partial class CacheLifestyleManager :
	AbstractLifestyleManager
{
	private ILease lease;
	public ILease Lease
	{
		get
		{
			if (this.lease == null)
			{
				this.lease = this.ResolveLease();
			}
			return this.lease;
		}
	}
	private ILease ResolveLease()
	{
		var defaultLease = new SlidingLease(TimeSpan.FromMinutes(1));
		if (this.Kernel == null)
		{
			return defaultLease;
		}
		if (this.Kernel.HasComponent(typeof(ILease)))
		{
			return this.Kernel.Resolve<ILease>();
		}
		return defaultLease;
	}
}

Строка 2: Наследование от AbstractLifestyleManager

Строка 9-13: "Ленивая" загрузка

Строка 23-26: Пытается определить местоположение ILease

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

Для того чтобы реализовать функциональность, которая решает, когда заканчивается время кэшированного объекта, вам нужен экземпляр ILease. Если бы вы могли использовать Constructor Injection, то вы бы запросили ILease с помощью конструктора, состоящего из трех строк кода (включая граничный оператор). На данный момент вам необходимо 12 строк кода, поскольку вам приходится иметь дело со множеством потенциальных состояний CacheLifestyleManager: вызывался ли уже метод Init? Обладает ли Kernel экземпляром ILease?

Вы справляетесь с этим при помощи свойства Lease, имеющего отложенную загрузку (lazy-loaded property). При первом его прочтении он вызывает метод ResolveLease, который выясняет, каким должен быть срок аренды. Он использует срок аренды, заданный по умолчанию, но пытается искать альтернативный срок посредством Kernel – если Kernel вообще существует. Я думаю, что это довольно хорошая иллюстрация недостатков Method Injection. Обратите внимание на то, что, если кто-либо прочитает свойство Lease до вызова метода Init, то будет использоваться срок аренды по умолчанию даже в случае, если Kernel содержит компонент ILease. Тем не менее, поскольку Castle Windsor ничего не знает о свойстве Lease, при обычном его применении этого не происходит.

Примечание

Интерфейс ILease, используемый в данном примере, является пользовательским интерфейсом, определенным для конкретной цели. Это не System.Runtime.Remoting.Lifetime.ILease, который имеет аналогичное, но не намного более сложное API.

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

Листинг 10-3: Реализация метода Resolve
private object obj;
public override object Resolve(CreationContext context)
{
	if (this.Lease.IsExpired)
	{
		base.Release(this.obj);
		this.obj = null;
	}
	if (this.obj == null)
	{
		this.Lease.Renew();
		this.obj = base.Resolve(context);
	}
	return this.obj;
}

Строка 4-8: Выносит объект на просрочку

Строка 11: Продляет срок аренды

Каждый раз, когда CacheLifestyleManager просят разрешить компонент, он начинает с проверки того, просрочен ли текущий срок аренды. Если срок аренды просрочен, CacheLifestyleManager высвобождает текущий кэшированный экземпляр и обнуляет его. Метод Release явным образом вызывается для базового класса и через него – для IComponentActivator, что продемонстрировано на рисунке 10-5. Это важно выполнить, потому что это дает вышеупомянутой реализации возможность уничтожить экземпляр, если он реализует IDisposable.

Следующее, что необходимо сделать, – проверить, имеет ли кэшированный экземпляр значение null. Он может иметь null, если он был высвобожден перед этим, но также может иметь null при первом вызове метода Resolve. В обоих случаях вы продлите срок аренды и просите базовую реализацию разрешить компонент для вас. Именно здесь базовый класс вызывает соответствующий метод для IComponentActivator.

В данном пользовательском стиле существования вы переопределяете метод Release так, чтобы он ничего не делал:

public override bool Release(object instance)
{
	return false;
}

Это может показаться странным, но является вполне нормальным. Вы должны принять во внимание тот факт, что метод Release – это Hook-метод, который является частью Seam стиля существования Castle Windsor. Вы проинформированы о том, что компонент может быть высвобожден, но это не означает, что вам придется это делать. Для примера стиль существования Singleton по определению никогда не высвобождает свой экземпляр, поэтому он имеет такую же реализацию метода Release, как и продемонстрированная ранее.

В случае с CacheLifestyleManager вы время от времени высвобождаете кэшированный экземпляр, но, как показано в листинге 10-3, вы делаете это в рамках метода Resolve по необходимости.

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

Реализация Lease

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

Примечание

Интерфейс ILease и класс SlidingLease, продемонстрированные в данном разделе, ничего не делают в рамках Castle Windsor, но я хотел продемонстрировать их ради полноты знаний. Вы можете пропустить этот раздел и приступить к чтению информации о том, как зарегистрировать пользовательский жизненный цикл, если вас не интересует SlidingLease.

Следующий листинг демонстрирует реализацию SlidingLease.

Листинг 10-4: Реализация SlidingLease
public class SlidingLease : ILease
{
	private readonly TimeSpan timeout;
	private DateTime renewed;
	public SlidingLease(TimeSpan timeout)
	{
		this.timeout = timeout;
		this.renewed = DateTime.Now;
	}
	public TimeSpan Timeout
	{
		get { return this.timeout; }
	}
	public bool IsExpired
	{
		get
		{
			return DateTime.Now >
				this.renewed + this.timeout;
		}
	}
	public void Renew()
	{
		this.renewed = DateTime.Now;
	}
}

Строка 16-17: Выносит на просрочку в случае истечения времени ожидания

Строка 22: Продление срока

Класс SlidingLease реализует ILease путем отслеживания того, когда был продлен срок аренды. Всякий раз, когда вы запрашиваете у SlidingLease информацию о том, был ли просрочен срок аренды, он сравнивает текущее время со временем продления и ожидания. При продлении срока аренды SlidingLease устанавливает текущее время в качестве времени продления. Я мог бы использовать TimeProvider Ambient Context из раздела 4.4.4 вместо DateTime.Now, но решил упростить все, насколько это возможно.

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

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

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

container.Register(Component
	.For<IIngredient>()
	.ImplementedBy<SauceBéarnaise>()
	.LifeStyle.Custom<CacheLifestyleManager>());

Вы применяете generic-перегрузку метода Custom для того, чтобы определить, какой тип ILifestyleManager использовать. Но существует также перегрузка, которая принимает экземпляр Type в качестве входного параметра.

До тех пор, пока вы не будете забывать регистрировать также и ILease, CacheLifestyleManager будет использовать заданный по умолчанию срок аренды SlidingLease с одноминутным таймаутом. Ниже приведен способ регистрации пользовательского ILease:

container.Register(Component
	.For<ILease>()
	.Instance(new SlidingLease(TimeSpan.FromHours(1))));

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

Разработка пользовательского стиля существования для Castle Windsor не особенно сложна. В большинстве случаев класс AbstractLifestyleManager предоставляет хорошую стартовую точку, и нам нужно только переопределить методы, которые нам особенно важны. Чаще всего это будет метод Resolve, хотя для некоторых других методов мы можем оставить их реализации по умолчанию. Только в редких случаях нам нужно будет создавать пользовательский стиль существования, поскольку стандартный набор стилей существования Castle Windsor довольно исчерпывающий.

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