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

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

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

Марк Симан

6.2. Работа с недолговечными зависимостями

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

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

  • Вы можете моделировать такие взаимодействия при помощи абстрактной фабрики, которая создает одноразовые экземпляры.
  • Вы должны стремиться скрыть этот паттерн за stateless абстракцией.

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

Закрытие соединений через абстракции

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

Тем не менее, некоторые зависимости представляют доступ к внешним ресурсам, а эти, как правило, переходят на вопросы, связанные с использованием ресурсов. Я, конечно, говорю о соединениях в различных видах и формах.

Большинство .NET разработчиков знают, что они должны открыть соединение ADO.NET прямо перед его использованием и закрыть его снова, как только работа будет закончена. Современные API интерфейсы, как LINQ to SQL или LINQ to Entities, автоматически сделают это для нас так, поэтому мы не должны работать с этим напрямую.

Хотя любой программист должен знать о правильно используемом паттерне, касающемся ADO.NET соединений, гораздо менее известно, что это же самое верно для WCF клиентов. Они должны быть закрыты, как только мы закончим с определенным набором операций или сервисов, потому что в противном случае они могут оставить «мертвые» ресурсы на стороне сервера.

WCF сервисы и состояние

Фундаментальное правило сервисной ориентации заключается в том, что сервисы не должны сохранять состояние (должны быть stateless). Если мы будем следовать этому правилу, тогда, безусловно, WCF клиент не оставит «живые» ресурсы на стороне сервера, так?

Удивительно, но это не может быть не так. Даже если мы построим сервис полностью stateless, WCF может таким не быть. Это зависит от связывания.

Один из многих примеров относится к безопасности. Основанная на сообщениях безопасность, как правило, влияет на производительность. Это верно, потому что асимметричные ключи требуют большого объема вычислений, но это еще более верно для Federated security, потому что несколько обменов сообщениями участвуют в создании контекста безопасности. Поведением по умолчанию для WCF является создание защищенного диалога на основе обмена асимметричными ключами. Сервис и клиент используют «рукопожатие» асимметричной безопасности для обмена специальным симметричным ключом, который используется для обеспечения безопасности всех последующих сообщений, которые являются частью этой сессии.

Однако такое поведение требует, чтобы обе стороны сохранили общий секрет в памяти. Клиент должен распрощаться с сервисом, когда он закончит сессию, или это будет «мертвый» симметричный ключ на сервере. Это, в конечном счете, может быть очищено после тайм-аута, но до тех пор это все занимает память. Чтобы сохранить ресурсы на сервере, клиент должен явно закрыть "соединение", когда оно будет завершено.

Хотя это верно не для всех WCF связываний, но таких много, так что мы должны гарантировать, что наши клиенты WCF являются «хорошими ребятами».

Как мы можем совместить необходимость закрыть WCF соединение с желанием избежать протекающей абстракции? Этот вопрос может быть рассмотрен на двух уровнях:

  • Сокрытие всей логики управления соединениями за абстракцией
  • Подражание открытию и закрытию соединений на более детальном уровне

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

Сокрытие управления соединениями за абстракцией

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

Некоторые люди прибегают к тому, что разрешают зависимостям быть наследованными от IDisposable. Тем не менее, метод Dispose – это просто метод Close с другим именем, так что такой подход не решит основной проблемы.

К счастью, технологии доступа к базам данных, такие как LINQ to SQL и LINQ to Entities демонстрируют правильный подход. В обоих случаях мы имеем доступ к данным через контекст, который содержит соединение. Всякий раз, когда мы общаемся с базой данных, контекст автоматически открывает и закрывает соединение по мере необходимости, полностью освобождая нас от бремени борьбы с этим.

Наша первая реакция должна заключаться в том, чтобы сделать то же самое. Рисунок 6-3 показывает, как определить абстракцию на уровне, который достаточно «крупнозернист», чтобы реализация могла открывать и закрывать соединения по мере необходимости.

Рисунок 6-3: Мы можем разработать интерфейс, который достаточно «крупнозернист», чтобы каждый метод включал в себя все взаимодействия с внешним ресурсом в одном пакете. Consumer вызывает метод для интерфейса IResource. Реализация этого метода может открыть соединение и вызывать несколько методов по отношению ко внешним ресурсам до закрытия соединения и возвращения результата потребителю.

Потребитель никогда не знает, что некоторые реализации могут открывать и закрывать соединения от его имени.

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

Открытие и закрытие зависимостей

Проблема с «крупнозернистыми» API заключается в том, что они не могут быть достаточно гибкими. Иногда нужна просто абстракция, которая позволяет нам явно моделировать жизненный цикл зависимости, которая в противном случае приведет к утечке памяти.

Внимание

Остановка одной утечки приводит к другой. Мы меняем утечки памяти на "дырявые" абстракции (Leaky Abstractions).

Наиболее распространенный жизненный цикл, который нам нужно смоделировать, показан на фигуре 6-4.

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

В разделе 6-1 было показано, как использовать абстрактную фабрику для создания зависимости по желанию, так что мы должны найти идиому кодирования, которая подходит к закрытию соединения. Как показано на рисунке 6-4, мы можем использовать паттерн IDisposable, чтобы работать с зависимостями, использующими соединения.

Внимание

С одноразовыми зависимостями код «пахнет». Используйте их только тогда, когда нет другого выбора. Подробнее об этом в разделе 8.2.

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

Рисунок 6-5: Мы можем смоделировать управление соединением и аналогичные жизненные циклы, принимая зависимость от абстрактной фабрики, такой как IFooFactory, показанной здесь. Каждый раз, когда потребителю нужен экземпляр IFoo, он создается IFooFactory, но потребитель должен помнить, что должен избавиться от него соответствующим образом.

Паттерн, показанный на рисунке 6-5, часто лучше всего реализовать с помощью ключевого слова C# using (или аналогичной конструкции в других языках).

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

Пример: вызов сервиса управления продуктом

Представьте себе Windows Presentation Foundation (WPF), который обеспечивает богатый пользовательский интерфейс для управления каталогом продукции. Такое приложение может общаться с бэкэндом (сервером) через WCF сервис, который предоставляет необходимые операции по управлению каталогом продукции.

На рисунке 6-6 показано, как реализация сочетает в себе обе техники из предыдущего раздела.

Рисунок 6-6: Класс MainWindowViewModel потребляет интерфейс IProductManagementAgent. Это крупнозернистый интерфейс, который предоставляет соответствующие методы для потребителя. С точки зрения MainWindowViewModel, нет никакого управления соединением. Когда приложение запущено, класс WcfProductManagementAgent обеспечивает реализацию крупнозернистого интерфейса. Он делает это, потребляя абстрактную фабрику IProductChannelFactory, которая создает одноразовые экземпляры. Интерфейс IProductManagementServiceChannel наследуется от IDisposable, что позволяет WcfProductManagementAgent избавиться от WCF клиента, когда операции были успешно вызваны.

Примечание

Мы вернемся к этому WPF приложению в разделах 6.3.2 и 7.4.2.

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

Всякий раз, когда класс MainWindowViewModel хочет вызвать сервисную операцию, он вызывает зависимость IProductManagementAgent. Это совершенно нормальная зависимость, внедренная через конструктор. Это, например, показывает, как удалить продукт:

this.agent.DeleteProduct(productId);

В этом случае this.agent является внедренной зависимостью IProductManagementAgent. Как видите, здесь нет никакого явного управления соединением, но если вы посмотрите на реализацию в WcfProductManagementAgent, вы увидите, как абстрактная фабрика используется в комбинации с одноразовой зависимостью:

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

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

Зависимость factory является экземпляром интерфейса IProductChannelFactory. Это пользовательский интерфейс, созданный по данному случаю:

public interface IProductChannelFactory
{
	IProductManagementServiceChannel CreateChannel();
}

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

WCF понимает этот тип, что делает реализацию IProductChannelFactory тривиальной, поскольку мы можем использовать System.ServiceModel.ChannelFactory<TChannel> для создания экземпляров.

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

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