Главная страница   /   8.2. Работа с устраняемыми зависимостями (Внедрение зависимостей в .NET

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

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

Марк Симан

8.2. Работа с устраняемыми зависимостями

Несмотря на то, что .NET – это управляемая платформа, имеющая сборщик мусора, она все еще может взаимодействовать с неуправляемым кодом (unmanaged code). Когда это происходит, .NET код взаимодействует с неуправляемой памятью, которая не уничтожается сборщиком мусора. Для предотвращения утечки памяти нам нужен механизм, с помощью которого мы бы детерминированно выпускали неуправляемую память. Это и является ключевой целью интерфейса IDisposable.

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

Как нам следует моделировать устраняемые зависимости? Должны ли мы также позволять абстракциям быть устраняемыми? Все это могло бы выглядеть так:

public interface IMyDependency : IDisposable { }

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

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

Николас Блумхардт, участник форума Common Context Adapters

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

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

В целях рассуждений представьте себе, что у нас есть устраняемая абстракция, подобная абстрактному классу OrderRepository:

public abstract class OrderRepository : IDisposable

Как класс OrderRepository должен взаимодействовать с зависимостью? Большинство руководств по проектированию (включая FxCop и встроенный в Visual Studio Code Analysis) настаивало бы на том, что, если бы класс содержал устраняемый ресурс в качестве члена класса, то он сам должен был бы реализовывать IDisposable и избавляться от ресурса, подобного следующему:

protected virtual void Dispose(bool disposing)
{
	if (disposing)
	{
		this.repository.Dispose();
	}
}

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

Рисунок 8-6: Единичный экземпляр SqlOrderRepository внедряется и в OrderService, и в SupplierReorderPolicy. Эти два экземпляра используют одну и ту же зависимость. Если OrderService уничтожит свой внедренный OrderRepository, то он разрушит зависимость SupplierReorderPolicy, и когда SupplierReorderPolicy попытается использовать эту зависимость, возникнет исключение.

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

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

Создание недолговечных устраняемых объектов

Многие API стандартной библиотеки классов .NET используют IDisposable, чтобы сообщить о том, что конкретная область применения перестала существовать. Одним из самых выдающихся примеров является WCF-прокси.

WCF-прокси и IDisposable

Все авто-генерируемые WCF-прокси реализуют IDisposable, поэтому важно не забывать вызывать метод Dispose (или Close) для прокси, как только это становится возможным. Многие связывания при отправке первого запроса автоматически создают сессию для сервиса, и эта сессия задерживается в сервисе до тех пор, пока не заканчивается ее время, или до тех пор, пока она явным образом не уничтожается.

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

Чтобы быть полностью технически корректными, нам не приходится вызывать метод Dispose для WCF-прокси. Использование метода Close приведет к тому же самому результату.

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

К счастью, после уничтожения объекта мы не можем его заново использовать. Это означает, что если мы хотим вызвать то же самое API снова, мы должны создать новый экземпляр. К примеру, это хорошо подходит, когда мы используем WCF-прокси или ADO.NET команды: мы создаем прокси, вызываем его операции и избавляемся от него, как только работа с ним завершается. Как мы можем совместить это с механизмом внедрения зависимостей, если мы считаем, что устраняемые абстракции являются leak-абстракциями?

Как и всегда может быть полезным скрытие ненужных деталей в интерфейсе. Если мы обратимся к WPF приложению из раздела 7.4, то в данном случае мы спрятали WCF-прокси в интерфейсе IProductManagementAgent.

Примечание

Интерфейс IProductManagementAgent наиболее значителен в листинге 7-10, но если не брать во внимание этот листинг, мы не рассматривали этот интерфейс подробно. В сущности, такой агент занимает такое же самое место, как и репозиторий, но много лет назад я приобрел привычку называть компоненты доступа к данным Smart Clients агентами, а не репозиториями.

С точки зрения MainViewModel ниже приведено то, как вы удаляете товар:

this.agent.DeleteProduct(productId);

Вы просите внедренный agent удалить товар. MainViewModel может безопасно хранить ссылку на агента, поскольку интерфейс IProductManagementAgent не унаследован от IDisposable.

При рассмотрении WCF реализации этого интерфейса формируется другая картина. Ниже приведена реализация метода DeleteProduct:

public void DeleteProduct(int productId)
{
	using (var channel = this.factory.CreateChannel())
	{
		channel.DeleteProduct(productId);
	}
}

Класс WcfProductManagementAgent не является изменчивым, но имеет внедренную абстрактную фабрику, которую вы можете использовать для создания канала. Канал – это еще одно слово для обозначения WCF-прокси, и это еще и авто-генерируемый клиентский интерфейс, который вы получаете бесплатно, когда создаете ссылку на сервис с помощью Visual Studio или svcutil.exe. Поскольку этот интерфейс унаследован от IDisposable, вы можете завернуть его в оператор using.

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

Но постойте! Не утверждал ли я, что устраняемые абстракции являются leaky-абстракциями? Да, утверждал, но мне приходится сопоставлять прагматические сущности с принципами. В данном случае WcfProductManagementAgent, абстрактная фабрика IProductChannelFactory и IProductManagementServiceChannel определены в одной и той же WCF-специфичной библиотеке, выделенной на рисунке 8-7.

Рисунок 8-7: Помимо других типов библиотека ProductWcfAgent содержит реализацию IProductManagementAgent и поддерживаемые им типы. WcfProductManagementAgent использует IProductChannelFactory для создания экземпляров IProductManagementServiceChannel, которые являются устраняемыми. Несмотря на то, что они могут рассматриваться, как leak-абстракции, они не "протекают" слишком далеко, поскольку все потребители и реализаторы находятся в той же самой сборки.

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

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

Обобщая все выше сказанное, устраняемые абстракции являются leaky-абстракциями. Иногда мы должны принимать такую "протечку" для того, чтобы избежать багов (например, отвергнутые WCF соединения); но если мы так поступаем, то можем сделать все возможное, чтобы эта "протечка" не распространилась по всему приложению.

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

Управление устраняемыми зависимостями

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

Подсказка

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

Как всегда данная ответственность ложится на Composer (например, DI-контейнер). Composer лучше всех остальных знает, в какой момент он создает устраняемый экземпляр, поэтому он также знает, что этот экземпляр необходимо уничтожить. Для Composer проще хранить ссылку на устраняемый экземпляр и вызывать его метод Dispose в подходящий момент.

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

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

Таблица 8-1: Точки входа и выхода для различных .NET Framework'ов
Технология Точка входа Точка выхода
Консольные приложения Main Main
ASP.NET MVC IControllerFactory.CreateController IControllerFactory.ReleaseController
WCF IInstanceProvider.GetInstance IInstanceProvider.ReleaseInstance
WPF Application.OnStartup Application.OnExit
ASP.NET Constructors**, Page_Load IDisposable.Dispose**, Page_Unload
PowerShell Constructors** IDisposable.Dispose**

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

Высвобождение зависимостей

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

Рисунок 8-8: Когда Composer получает запрос о преобразовании объекта, он собирает все зависимости запрашиваемого объекта. В данном примере запрашиваемый объект имеет три зависимости, и две из них являются устраняемыми. Одна из этих устраняемых зависимостей также используется и другими потребителями, поэтому она является повторно используемой, тогда как остальные зависимости проиллюстрированы только в одном месте. При получении запроса о высвобождении объекта Composer уничтожает приватную устраняемую зависимость и разрешает неустранимой зависимости и самому объекту выйти за рамки области применения. Единственной взаимосвязью с повторно используемой зависимостью остается тот факт, что она внедрена в запрашиваемый объект; но так как она является повторно используемой, она пока не уничтожается.

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

Подсказка

Если вы когда-либо работали со счетчиками референсов (или имели опыт работы с багами, являющимися следствием плохой реализации), то вы оцените, каким сложным может быть процесс сохранения обозначений для всех зависимостей и их потребителей. Это именно тот момент, когда вам может пригодиться DI-контейнер, поскольку он берет заботу обо всем этом на себя. Используйте DI-контейнер, а не разрабатывайте свой собственный код отслеживания жизненного цикла. Реализация процесса управления жизненным циклом DI-контейнером гарантированно является более протестированной, нежели все то, что вы можете создать в пределах разумных временных рамок.

Давайте вернемся к примеру WCF сервиса из раздела "Управление жизненным циклом с помощью контейнеров". Оказывается, в листинге 8-2 есть баг, потому что, как демонстрирует рисунок 8-9, SqlProductRepository реализует IDisposable.

Рисунок 8-9: SqlProductRepository реализует IDisposable, потому что содержит устраняемый ресурс. Он также наследуется от абстрактного класса ProductRepository, который не реализует IDisposable.

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

Для начала учтите, что контейнер должен уметь обслуживать множество одновременных запросов, поэтому ему приходится связывать каждый экземпляр SqlProductRepository с создаваемым им IProductManagementService. Контейнер использует Dictionary<IProductManagementService, SqlProductRepository>, называемые репозиториями, для того, чтобы отслеживать эти связи. Следующий листинг демонстрирует, как контейнер преобразовывает запросы экземпляров IProductManagementService.

Листинг 8-4: Связывание устраняемых зависимостей с разрешенной центральной частью
public IProductManagementService ResolveProductManagementService()
{
	var repository = new SqlProductRepository(this.connectionString);
	var srvc = new ProductManagementService(repository, this.mapper);
	lock (this.syncRoot)
	{
		this.repositories.Add(srvc, repository);
	}
	return srvc;
}

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

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

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

Листинг 8-5: Высвобождение устраняемых зависимостей
public void Release(object instance)
{
	var srvc = instance as IProductManagementService;
	if (srvc == null)
	{
		return;
	}
	lock (this.syncRoot)
	{
		SqlProductRepository repository;
		if (this.repositories.TryGetValue(srvc, out repository))
		{
			repository.Dispose();
			this.repositories.Remove(srvc);
		}
	}
}

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

Строка 14: Удаляет репозиторий из словаря

Поскольку метод Release принимает любые типы объектов, вам для начала потребуется граничный оператор (Guard Clause), чтобы убедиться, что instance является IProductManagementService.

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

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

Примеры, продемонстрированные в листингах 8-4 и 8-5, используются специально для работы с одной конкретной устраняемой зависимостью: SqlProductRepository. Довольно банально было бы, если бы мы расширили код для того, чтобы иметь возможность работать с зависимостями любого вида, но после этого ситуация усложнилась бы. Представьте, что вам приходится работать с многочисленными устраняемыми зависимостями одного и того же объекта, или вложенными устраняемыми зависимостями, причем часть из них должны быть Singleton'ами, а некоторые должны быть Transient – и это мы еще даже не начинали обсуждать более сложные стили существования!

Подсказка

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

DI-контейнеры могут работать со сложными сочетаниями стилей существования, и предлагают возможности (например, метод Release) для явного высвобождения компонентов после завершения работы с ними. Мы должны не забывать использовать эти методы для того, чтобы избежать утечки памяти, особенно в тех случаях, когда одна или более одной зависимости из сконфигурированных являются устраняемыми.

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