Главная страница   /   8.3. Каталог стилей существования объектов (Внедрение зависимостей в .NET

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

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

Марк Симан

8.3. Каталог стилей существования объектов

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

Примечание

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

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

Таблица 8-2: Паттерны стилей существования, рассматриваемые в данном разделе
Название Описание
Singleton Один экземпляр постоянно повторно используется
Transient Всегда используются новые экземпляры
Per Graph Один экземпляр повторно используется в пределах каждой диаграммы объектов
Web Request Context В большинстве случаев для одного веб-запроса используется один экземпляр каждого типа
Pooled Используются экземпляры из пула готовых объектов
Lazy Зависимость, требующая больших затрат, создается и используется в замедленном темпе
Future Зависимость станет доступна в будущем

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

Singleton

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

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

Не путайте стиль существования Singleton с паттерном проектирования Singleton.

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

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

Подсказка

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

Поскольку используется только один экземпляр, стиль существования Singleton в основном потребляет минимальное количество памяти. Единственный момент, когда так не происходит, – это когда экземпляр редко используется, но потребляет чрезмерное количество памяти. В таких случаях стиль существования Lazy с примыкающим экземпляром Transient может стать наилучшей конфигурацией (но я долго пытался найти разумные примеры таких ситуаций).

Когда использовать Singleton

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

Все сервисы, которые не сохраняют свое состояние, по определению являются потоко-безопасными, как и неизменные типы, и очевидные классы, специально созданные потоко-безопасными. В таких случаях нет причины не делать их Singleton'ами.

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

Давайте поближе рассмотрим репозиторий, находящийся в оперативной памяти.

Пример: Использование потоко-безопасного репозитория, находящегося в оперативной памяти

Давайте еще раз обратим наше внимание на реализацию ICommerceServiceContainer таким образом, как это описано в разделах "Пример: подключение сервиса управления продуктами", "Управление жизненным циклом с помощью контейнера" и "Управление устраняемыми зависимостями". Вместо использования ProductRepository, базирующегося на SQL Server, мы могли бы решить, использовать потоко-безопасную реализацию в оперативной памяти. Для того чтобы хранилище данных, находящееся в оперативной памяти, имело какой-либо смысл, оно должно совместно использоваться всеми запросами, поэтому оно должно быть потоко-безопасным, как это проиллюстрировано на рисунке 8-10.

Рисунок 8-10: Когда составные экземпляры ProductManagementService, идущие отдельными потоками, обращаются к совместно используемому ресурсу, например, находящемуся в оперативной памяти ProductRepository, мы должны убедиться, что совместно используемый ресурс является потоко-безопасным.

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

Листинг 8-6 демонстрирует, как контейнер может возвращать новые экземпляры всякий раз, когда его просят разрешить IProductManagementService, в то время, как ProductRepository используется всеми экземплярами совместно.

Листинг 8-6: Управление Singleton'ами
public class SingletonContainer : ICommerceServiceContainer
{
	private readonly ProductRepository repository;
	private readonly IContractMapper mapper;
	public SingletonContainer()
	{
		this.repository =
			new InMemoryProductRepository();
		this.mapper = new ContractMapper();
	}
	public IProductManagementService
		ResolveProductManagementService()
	{
		return new ProductManagementService(
			this.repository, this.mapper);
	}
	public void Release(object instance) { }
}

Строка 3-4: Экземпляры Singleton

Строка 7-9: Создает Singleton'ы

Строка 14-15: Создает сервис

Строка 17: Ничего не делает

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

Всякий раз, когда контейнер просят разрешить экземпляр IProductManagementService, он создает Transient экземпляр с внедренными в него Singleton'ами. В данном примере и repository, и mapper являются Singleton'ами, но при желании вы можете смешивать стили существования.

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

Еще одним простым для реализации стилем существования является стиль Transient.

Transient

Стиль существования Transient включает в себя возврат нового экземпляра всякий раз, когда этот экземпляр запрашивается. До тех пор пока возвращаемый экземпляр не реализует IDisposable, следить не за чем. Наоборот, когда экземпляр реализует IDisposable, Composer должен иметь его ввиду и явным образом избавляться от него, когда его просят высвободить подходящую диаграмму объектов.

Стоит заметить, что в настольных и простых приложениях мы стремимся разрешать полноценную иерархию объектов всего единожды: при запуске приложения. Это означает, даже для Transient компонентов, что будет создано только несколько экземпляров, и что они могут существовать длительное время. В худшем случае, когда на одну зависимость приходится только один потребитель, конечный результат разрешения диаграммы истинных Transient компонентов эквивалентен разрешению диаграммы истинных Singleton'ов или любого их сочетания. Это все потому, что диаграмма разрешается всего лишь раз, поэтому разницы в поведении никогда не возникает.

Когда использовать Transient

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

В большинстве случаев мы можем безопасно менять стиль существования Transient на такой контекстно-ограниченный стиль существования, как Web Request Context, в котором доступ к зависимости также гарантированно сериализируется, но это зависит от среды выполнения (использование Web Request Context в настольных приложениях не имеет смысла).

Пример: разрешение разнообразных репозиториев

Ранее в этой главе вы видели несколько примеров использования стиля существования Transient. В листинге 8-2 repository создается и внедряется в методе, выполняющем разрешение, а контейнер не содержит ссылок на него. Затем в листингах 8-4 и 8-5 вы увидели, как работать с устраняемым Transient компонентом.

В этих примерах вы могли заметить, что mapper повсюду остается Singleton'ом. Это истинный, не сохраняющий свое состояние сервис, поэтому нет необходимости создавать новый экземпляр для каждого вновь созданного ProductManagementService. Что действительно заслуживает внимания, так этот тот факт, что вы можете сочетать зависимости с различными стилями существования.

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

Листинг 8-7: Разрешение Transient DiscountRepositorys
public IController ResolveHomeController()
{
	var connStr = ConfigurationManager
		.ConnectionStrings["CommerceObjectContext"]
		.ConnectionString;
	var discountCampaign =
		new DiscountCampaign(
			new SqlDiscountRepository(connStr));
	var discountPolicy =
		new RepositoryBasketDiscountPolicy(
			new SqlDiscountRepository(connStr));
	return new HomeController(
		discountCampaign, discountPolicy);
}

Строка 8: Новый экземпляр SqlDiscountRepository

Строка 11: Еще один экземпляр SqlDiscountRepository

Как для класса DiscountCampaign, так и для класса RepositoryBasketDiscountPolicy нужна зависимость DiscountRepository. В случае, когда DiscountRepository является Transient, каждый потребитель получает свой собственный приватный экземпляр, поэтому DiscountCampaign получает один экземпляр, а RepositoryBasketDiscountPolicy – другой.

Стиль существования Transient означает, что каждый потребитель получает приватный экземпляр зависимости даже в тех случаях, когда разнообразные потребители в одной и той же диаграмме объектов обладают одной и той же зависимостью (как в случае с листингом 8-7). Если множество потребителей совместно используют одну и ту же зависимость, то данный подход будет неэффективным, но если реализация является потоко-безопасной, то в этом случае наиболее эффективный стиль существования Singleton не подходит. В таких ситуациях может больше подойти стиль существования Per Graph.

Per Graph

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

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

Когда использовать Per Graph

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

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

По сравнению с Transient, при использовании Per Graph не возникает никаких дополнительных издержек, поэтому мы часто можем использовать его в качестве замены Transient. Несмотря на то, что издержки отсутствуют, нам также не гарантировано и какое-либо преимущество. Мы получаем только некоторое усиление эффективности в случае, если диаграмма единичного объекта содержит многочисленных потребителей одной и той же зависимости. В этом случае мы можем разделить экземпляр между этими потребителями; но если совместно используемые зависимости отсутствуют, то будет нечего делить, и поэтому не будет никаких преимуществ.

Примечание

В большинстве случаев Per Graph лучше Transient, но не многие DI-контейнеры его поддерживают.

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

Пример: Совместное использование Repository в рамках диаграммы

В листинге 8-7 вы видели, как каждый потребитель получал свой собственный приватный экземпляр SqlDiscountRepository. Этот класс не является потоко-безопасным, поэтому вам не следует конфигурировать его в виде Singleton. Но вы не рассчитываете на то, что разнообразные потоки будут обращаться к индивидуальным экземплярам HomeController, поэтому разделение экземпляра SqlDiscountRepository между двумя потребителями будет безопасным. Следующий листинг демонстрирует, как создать единичный экземпляр Per Graph в методе ResolveHomeController.

Листинг 8-8: Разрешение единичного per graph repository
public IController ResolveHomeController()
{
	var connStr = ConfigurationManager
		.ConnectionStrings["CommerceObjectContext"]
		.ConnectionString;
	var repository =
		new SqlDiscountRepository(connStr);
	var discountCampaign =
		new DiscountCampaign(repository);
	var discountPolicy =
		new RepositoryBasketDiscountPolicy(repository);
	return new HomeController(discountCampaign, discountPolicy);
}

Строка 6-7: Совместно используемый экземпляр SqlDiscountRepository

Строка 8-11: Внедрение совместно используемого экземпляра

Вместо того чтобы создавать отдельные экземпляры для всех потребителей, вы создаете один экземпляр, который можете разделить между всеми потребителями. Вы внедряете этот единственный экземпляр как в DiscountCampaign, так и в RepositoryBasketDiscountPolicy. Обратите внимание на то, что по сравнению с Singleton'ами, в которых совместно используемый экземпляр является приватным членом контейнера, экземпляр repository является локальным по отношению к методу ResolveHomeController; при следующем вызове метода будет создан новый экземпляр, а затем он будет разделен между двумя потребителями.

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

Web Request Context

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

Чтобы решить эту проблему, веб-приложения управляют запросами одновременно. .NET инфраструктура защищает нас от этого, позволяя каждому запросу выполняться в своем собственном контексте и со своим собственным экземпляром Controller (если вы используете ASP.NET MVC) или Page (если вы используете ASP.NET Web Forms).

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

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

Рисунок 8-11 демонстрирует, как работает стиль существования Web Request Context. Зависимости ведут себя как Singleton'ы в пределах одного запроса, но не разделены между запросами. Каждый запрос имеет свой собственный набор связанных зависимостей.

Рисунок 8-11: Стиль существования Web Request Context указывает на то, что мы создаем не более одного экземпляра на один запрос. Экземпляр DiscountRepository совместно используется BasketDiscountPolicy и DiscountCampaign, но только в рамках запроса 1. Запрос 2 использует ту же самую конфигурацию, но экземпляры ограничены этим запросом.

Любые устраняемые компоненты должны быть уничтожены после окончания запроса.

Когда использовать Web Request Context

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

Стиль существования Web Request Context предпочтительнее Transient, но стиль существования Singleton все еще более эффективен. Используйте Web Request Context только в тех ситуациях, в которых Singleton не работает.

Примечание

Если вы следуете общему совету разрешать только одну диаграмму объектов для одного веб-запроса, то стили существования Web Request Context и Per Graph функционально эквивалентны.

Подсказка

Если вам когда-нибудь понадобится сформировать в веб-запросе Entity Framework ObjectContext, то самый лучший для этого стиль существования – Web Request Context. Экземпляры ObjectContext не являются потоко-безопасными, но в данном случае на один веб-запрос должен быть только один ObjectContext.

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

Подсказка

Некоторые DI-контейнеры позволяют вам создавать свои собственные расширения стилей существования, поэтому, возможно, это вариант в тех случаях, если выбранный вами контейнер не поддерживает стиль существования Web Request Context. К тому же это может быть не тривиальной затеей.

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

Пример: компоновка HomeController с Repository, разделенным между веб-запросами

В данном примере вы увидите, как компоновать ASP.NET MVC экземпляр HomeController с зависимостями, если для двух этих зависимостей необходим DiscountRepository. Эта ситуация обрисована на рисунке 8-11: для HomeController нужны BasketDiscountPolicy и DiscountCampaign, а для этих двух зависимостей, в свою очередь, нужен DiscountRepository.

Примечание

Шаблонный код данного раздела сложнее того, что гарантируется одноразовым решением. Я не думаю, что вам когда-нибудь придется писать пользовательский код жизненного цикла Web Request Context, подобный данному, но мне хочется показать вам, как он работает. Вместо написания такого кода используйте DI-контейнер, который поддерживает этот стиль существования.

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

Листинг 8-9: Компоновка HomeController
public IController ResolveHomeController()
{
	var discountPolicy =
		new RepositoryBasketDiscountPolicy(
			this.ResolveDiscountRepository());
	var campaign = new DiscountCampaign(
		this.ResolveDiscountRepository());
	return new HomeController(
		campaign, discountPolicy);
}

Строка 5,7: Делегирует резолюцию репозитория

Строка 8-9: Возвращает сформированный HomeController

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

При запросе разрешения DiscountRepository контейнер должен проверить, существует ли уже экземпляр, связанный с веб-запросом. Если он существует, то возвращается данный экземпляр; иначе экземпляр создается и связывается с веб-запросом перед тем, как будет возвращен. Как показывает следующий листинг, в ASP.NET (как в MVC, так и в Web Forms) вы можете использовать текущий HttpContext для поддержания этой связи в работоспособном состоянии.

Листинг 8-10: Разрешение зависимости, ограниченной контекстом веб-запроса
protected virtual DiscountRepository ResolveDiscountRepository()
{
	var repository = HttpContext.Current
		.Items["DiscountRepository"]
		as DiscountRepository;
	if (repository == null)
	{
		var connStr = ConfigurationManager
			.ConnectionStrings["CommerceObjectContext"]
			.ConnectionString;
		repository = new SqlDiscountRepository(connStr);
		HttpContext.Current
			.Items["DiscountRepository"] = repository;
	}
	return repository;
}

Строка 3-5: Выполняет поиск репозитория в контексте запроса

Строка 12-13: Сохраняет репозиторий в контексте запроса

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

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

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

Листинг 8-11: Высвобождение устраняемых зависимостей, ограниченных контекстом веб-запроса
public class DiscountRepositoryLifestyleModule : IHttpModule
{
	public void Init(HttpApplication context)
	{
		context.EndRequest += this.OnEndRequest;
	}
	public void Dispose() { }
	private void OnEndRequest(object sender, EventArgs e)
	{
		var repository = HttpContext.Current
			.Items["DiscountRepository"];
		if (repository == null)
		{
			return;
		}
		var disposable = repository as IDisposable;
		if (disposable != null)
		{
			disposable.Dispose();
		}
		HttpContext.Current
			.Items.Remove("DiscountRepository");
	}
}

Строка 10-11: Выполняет поиск репозитория в контексте запроса

Строка 16-20: Уничтожает репозиторий

Строка 21-22: Удаляет репозиторий из контекста запроса

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

Стиль существования Web Request Context связывает зависимость с текущим запросом, сохраняя и извлекая этот запрос посредством HttpContext.Current. Данный пример продемонстрировал специализированное решение, но методика может быть обобщена таким образом, чтобы произвольное количество зависимостей множества различных типов можно было связывать с контекстом запроса. Это относится к сфере соответствующего DI-контейнера.

Вариация: Session Request Context

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

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

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

Используйте стиль существования Session Request Context только, если он вам действительно необходим. Вероятнее всего, использование данного стиля понизит производительность вашей системы.

Подсказка

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

Еще одна проблема, с которой мы сталкиваемся, – это то, что состояние сессии может быть сохранено во внешнем хранилище, например на отдельном сервере сессий или на SQL Server session state. При таких конфигурациях должны быть сериализованы все данные сессии, а также зависимости, на которые оказывается влияние. Сериализация типа может быть так же проста, как и наделение этого типа атрибутом [Serializable], но существует еще кое-что, что мы должны не забыть сделать.

В целом, я считаю Session Request Context непривлекательным и не припомню, чтобы когда-либо видел, как он используется.

Вариация: Thread Context

Еще одна, более привлекательная вариация стиля существования Web Request Context, – это ассоциирование зависимости с конкретным потоком. Сущность остается той же: в каждом потоке управление зависимостью ведется как с Singleton, но для каждого потока существует свой экземпляр зависимости.

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

Рисунок 8-12: Когда приложение незамедлительно проносится по некоторому количеству параллельных задач и разрешает зависимости в рамках каждого потока, мы можем использовать стиль существования Thread Context для того, чтобы убедиться, что любые зависимости, не являющиеся потоко-безопасными, могут быть разделены между любым количеством потребителей одного и того же потока. Каждый поток имеет свои собственные экземпляры.

Чтобы реализовать стиль существования Thread Context, мы можем поискать запрашиваемую зависимость в Thread Local Storage (TLS). Если мы ее там обнаружим, то можем повторно ее использовать; иначе, мы создаем эту зависимость и сохраняем ее в TLS.

Между тем, как Session Request Context может быть совершенно опасным, а Thread Context – слегка экзотичным, стиль существования Web Request Context полезен. Он позволяет нам разделять зависимости в пределах веб-запроса, не беспокоясь при этом о том, являются ли они потоко-безопасными. Web Request Context – это нечто среднее между Singleton и Transient.

Web Request Context является более эффективной альтернативой стиля существования Transient, но мы можем использовать его только в веб-приложениях. Если мы имеем зависимости, для управления которыми требуются большие затраты, в других типах приложений, то мы можем обратиться к другим возможностям оптимизации.

Pooled

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

Несмотря на то, что общая концепция объектов в пуле должна быть вам знакома, в таблице 8-3 перечислены некоторые вариации реализации.

Таблица 8-3: Варианты реализации пулов объектов
Вариант Описание
Подготовка пула Как мы подготавливаем пул? Создаем ли мы все объекты в пуле заранее или наполняем его постепенно по мере поступления запросов?

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

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

В противном случае мы можем начать с пустого пула и заполнять его постепенно по необходимости. Это приводит к тому, что в начале время доступа замедляется, но такой подход может помочь хранить пул в нужном размере.
Минимальный размер Мы можем решить проблему подготовки пула путем ввода настраиваемого минимального размера. Если мы задаем минимальный размер, отличающийся от нуля, то перед тем, как объекты смогут его использовать, пул должен заполниться до этого размера. С другой стороны, при минимальном размере, равном нулю, объекты могут начать использовать его, как только он начнет заполняться.
Максимальный размер Каков максимальный размер пула?
Поведение пула при достижении максимального размера Что происходит, когда мы достигаем максимального размера пула? Позволяем ли мы пулу вырасти в размере? Если мы позволим ему это сделать, то мы рискуем попасть в ситуацию, когда у нас закончится память. Если не позволим, то, как, в таком случае, мы должны обрабатывать дополнительные запросы объектов?

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

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

В таблице представлены все важные концепции, касающиеся пулов объектов. Но как в случае со стилем существования Web Request Context нам не следует разрабатывать свои собственные пользовательские пулы объектов, а следует использовать те, которые предоставляются DI-контейнерами.

Примечание

Не все DI-контейнеры предоставляют стиль существования Pooled, поэтому очевидно, что мы можем выбрать этот стиль существования только, если он поддерживается нашим DI-контейнером.

При использовании стиля существования Pooled, предоставляемого DI-контейнером, все варианты, описанные в таблице 8-3, могут быть недоступны. Нам придется работать только с тем, что доступно.

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

Стиль существования Pooled вступает в игру в тех ситуациях, когда у нас есть специфичные компоненты, часто используемые, но для создания которых необходимы большие затраты. Даже если создание компонента трудозатратно, нам все еще стоит предпочесть стиль существования Singleton, если это возможно, поскольку он позволяет нам работать с единственным экземпляром и расплачиваться за создание объекта всего лишь единожды.

Из этого следует, что организация пула применима только тогда, когда рассматриваемый компонент не должен использоваться коллективно, что часто происходит в тех случаях, когда данный компонент не является потоко-безопасным. Если мы заглянем в веб-приложение, то стиль существования Web Request Context мог бы стать разумной альтернативой; мы должны были понять, что стиль существования Pooled используется вне веб-приложений.

Обратите внимание на то, что тот факт, что рассматриваемый компонент можно использовать повторно, является необходимым условием. Если этот компонент обладает обычным жизненным циклом, который исключает возможность повторного использования, то мы не можем организовать его пул. Одним из примером этого является WCF интерфейс ICommunicationObject, который имеет вполне определенный жизненный цикл. Когда ICommunicationObject находится в состоянии Closed или Faulted, то он может, по определению, никогда не покидать это состояние. Такой тип объекта не пригоден для организации пула. Мы должны уметь возвращать объект обратно в пул в первоначальном состоянии.

Пример: Повторное использование затратных репозиториев

Однажды я работал над проектом, в котором нам нужно было из .NET кода взаимодействовать с мэйнфреймом. Ранее консультанты создали неуправляемую COM библиотеку, которая могла взаимодействовать с некоторой конечной точкой мейнфрейма, и мы решили обернуть эту библиотеку в управляемую сборку.

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

Давайте посмотрим, как создать пул экземпляров ProductRepository, которые могут взаимодействовать посредством такого протокола. В проекте, в который я был вовлечен, мы называли COM библиотеку Xfer (видовой), поэтому давайте создадим пул экземпляров XferProductRepository.

Примечание

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

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

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

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

Листинг 8-12: Схема базы для организации пула контейнера
public partial class PooledContainer : ICommerceServiceContainer
{
	private readonly IContractMapper mapper;
	private readonly List<XferProductRepository> free;
	private readonly List<XferProductRepository> used;
	public PooledContainer()
	{
		this.mapper = new ContractMapper();
		this.free = new List<XferProductRepository>();
		this.used = new List<XferProductRepository>();
	}
	public int MaxSize { get; set; }
	public bool HasExcessCapacity
	{
		get
		{
			return this.free.Count + this.used.Count < this.MaxSize;
		}
	}
}

Несмотря на то, что вы планируете организовать пул экземпляров XferProductRepository, вы все равно конфигурируете ContractMapper в виде Singleton, поскольку он является сервисом, несохраняющим свое состояние.

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

Свойство MaxSize позволяет вам определить максимальный размер пула, а свойство HasExcessCapacity по существу является инкапсулированным вычислением, которое вы можете использовать в условных выражениях для определения того, превышаете ли вы по-прежнему размер пула.

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

Листинг 8-13: Разрешение репозиториев из пула
public IProductManagementService ResolveProductManagementService()
{
	XferProductRepository repository = null;
	if (this.free.Count > 0)
	{
		repository = this.free[0];
		this.used.Add(repository);
		this.free.Remove(repository);
	}
	if (repository != null)
	{
		return this.ResolveWith(repository);
	}
	if (!this.HasExcessCapacity)
	{
		throw new InvalidOperationException(
			"The pool is full.");
	}
	repository = new XferProductRepository();
	this.used.Add(repository);
	return this.ResolveWith(repository);
}
private IProductManagementService ResolveWith(
	ProductRepository repository)
{
	return new ProductManagementService(repository,
		this.mapper);
}

Строка 4-9: При возможности выбирает из пула

Строка 10-13: Возвращает из пула

Строка 19-20: Добавляет новый репозиторий

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

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

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

Листинг 8-14: Возврат репозиториев в пул
public void Release(object instance)
{
	var service = instance as ProductManagementService;
	if (service == null)
	{
		return;
	}
	var repository = service.Repository
		as XferProductRepository;
	if (repository == null)
	{
		return;
	}
	this.used.Remove(repository);
	this.free.Add(repository);
}

Строка 4-7, 10-13: Граничные операторы

Строка 14-15: Возвращает репозиторий в пул

Вернуть репозиторий в пул легко: вы перемещаете его из коллекции используемых репозиториев в коллекцию доступных репозиториев.

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

  • Пример определенно не является потоко-безопасным. Рабочая реализация должна позволять нескольким потокам разрешать и освобождать экземпляры параллельно.
  • Поскольку класс XferProductRepository инкапсулирует неуправляемый код, он реализует IDisposable. Пока вы продолжаете повторно использовать экземпляры, вам не нужно их уничтожать, но вам точно следует это сделать в тех ситуациях, когда контейнер выходит за рамки области применения. Таким образом, сам контейнер должен реализовывать IDisposable и удалять все репозитории с помощью метода Dispose.

Организация пула объектов – хорошо известный паттерн проектирования, но он часто инкапсулируется в существующие API; например, ADO.NET использует пулы соединений, но мы не сталкиваемся с ним явно. Только когда нам точно нужно оптимизировать доступ к затратным ресурсам, стиль существования Pooled начинает иметь смысл.

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

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

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

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

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

Lazy

Стиль существования Lazy или Delayed – это Virtual Proxy более затратной зависимости. Смысл в том, что если у нас есть требующая больших затрат зависимость, которую мы не планируем часто использовать, то мы можем отложить создание затратной зависимости до тех пор, пока она нам не понадобится. Рисунок 8-13 иллюстрирует, как можно внедрить потребителя с легковесным дублером для существующей, более затратной реализации.

Рисунок 8-13: Потребителю необходима зависимость IService, но если он использует эту зависимость лишь в небольших фракциях своего жизненного цикла, то он может долгое время просуществовать до того момента, когда ему понадобятся сервисы IService. Когда он, в конце концов, вызывает IService.SelectItem(), LazyService использует его внедренный IServiceFactory для создания экземпляра другого IService. К данному моменту ExpensiveService еще не создан. При создании ExpensiveService все последующие вызовы могут быть делегированы ему.

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

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

Стиль существования Lazy более интересен с технической точки зрения, нежели практически полезная стратегия жизненного цикла; если вам интересно, то я даю ссылки на рекомендуемую литературу, связанную с данной книгой. (Mark Seemann, "Rebuttal: Constructor over-injection anti-pattern," 2010, http://blog.ploeh.dk/2010/01/20/RebuttalConstructorOverinjectionAntipattern.aspx)

Future

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

Наилучший способ реализации такого стиля существования похож на стиль существования Lazy: мы можем использовать Decorator, который делегирует полномочия первоначальной реализации до тех пор, пока нужная зависимость не станет доступной. Рисунок 8-14 иллюстрирует концептуальное взаимодействие между компонентами. Первоначальная реализация, используемая в качестве дублера до тех пор, пока Future Decorator не дождется нужной зависимости, часто является приложением паттерна проектирования Null Object.

Рисунок 8-14: Потребителю необходима зависимость IService, но DesiredService может быть еще недоступным. В этом случае мы можем инкапсулировать NullService в виде дублера, который будет использоваться до тех пор, пока мы находимся "в ожидании Годо". FutureService – это установленная машина, которая выполняет опрос с целью определения того, стал ли доступным DesiredService. Когда DesiredService недоступен, FutureService Decorator ничего не остается, как использовать резервную реализацию, обеспечиваемую NullService. Когда DesiredService становится окончательно доступным, все последующие запросы направляются к нему.

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

Лучше мы будем иметь дело с недоступными внешними ресурсами, использующими паттерн Circuit Breaker, который мы рассмотрим в следующей главе. До тех пор, пока кто-нибудь не предоставит мне разумный сценарий, я считаю стратегию жизненного цикла Future как технически интересную.

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