Главная страница   /   3.1. Знакомство с DI-контейнерами (Внедрение зависимостей в .NET

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

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

Марк Симан

3.1. Знакомство с DI-контейнерами

DI-контейнер – это библиотека программных средств, которая может автоматизировать множество задач, включая компоновку объектов и управление их жизненным циклом. Несмотря на то, что можно написать весь необходимый код инфраструктуры с помощью Poor man's DI, такой способ не добавляет большой значимости приложению. С другой стороны, задача компоновки объектов является всеобщей по своей натуре и может быть решена в один момент и сразу для всех; это так называемый видовой субдомен.

Определение

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

Примечание

DI-контейнеры также называют IoC-контейнерами или (намного реже) Легковесными контейнерами.

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

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

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

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

Контейнер "Hello"

DI-контейнер – это библиотека программных средств, похожая на любую другую библиотеку программных средств. Он предоставляет API, которое вы можете использовать для компоновки объектов. Формирование диаграммы объектов – это вызов единичного метода. Все DI-контейнеры необходимо конфигурировать перед использованием их для компоновки объектов, но я заново пересмотрю этот вопрос в разделе "Конфигурирование DI-контейнеров".

В этом разделе я продемонстрирую вам несколько примеров того, как DI-контейнеры могут преобразовывать диаграммы объектов для расширенного шаблонного приложения из раздела "Расширение шаблонного приложения". Для каждого запроса ASP.NET MVC фреймворк будет запрашивать экземпляр соответствующего типа IController, поэтому вы должны реализовать метод, который использует DI-контейнер для формирования соответствующей диаграммы объектов.

Подсказка

Раздел "Построение ASP.NET MVC приложений" содержит подробную информацию о том, как формировать ASP.NET MVC приложения.

MVC фреймворк будет вызывать метод для экземпляра Type, который определяет нужный ему тип IController (например, HomeController или BasketController), а вы должны возвращать экземпляр этого типа.

Эта функциональность может быть реализована для всех DI-контейнеров, рассматриваемых в части 4, но в данном разделе я продемонстрирую только несколько примеров.

Преобразование контроллеров для различных DI-контейнеров

Unity – это DI-контейнер с явно соответствующим паттерну API. Предполагая, что у вас уже есть экземпляр класса UnityContainer контейнера Unity, вы можете преобразовать экземпляр IController из аргумента controllerType типа Type:

var controller = (IController)this.container.Resolve(controllerType);

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

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

Многие DI-контейнеры обладают API, которое похоже на API контейнера Unity. Соответствующий код для Castle Windsor выглядит идентично коду Unity, несмотря на то, что экземпляр container будет уже экземпляром класса WindsorContainer. Остальные контейнеры имеют несколько другие названия – для StructureMap, например, предыдущий код будет выглядеть следующим образом:

var controller = (IController)this.container.GetInstance(controllerType);

Единственное реальное отличие – метод Resolve называется GetInstance. Вы можете извлечь из этих примеров общий вид DI-контейнера.

Диаграммы преобразования объектов для DI-контейнеров

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

object Resolve(Type service);

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

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

T Resolve<T>();

Вместо применения аргумента Type метода такая перегрузка принимает типизированный параметр (T), который указывает на необходимый тип. Метод возвращает экземпляр T.

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

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

Сигнатура метода Resolve чрезвычайно влиятельная и универсальная. Вы можете запросить экземпляр любого типа, и ваш код все еще будет компилируемым. В действительности, метод Resolve устанавливает сигнатуру анти-паттерна Service Locator, и вам нужно действовать осторожно, чтобы не использовать свой DI-контейнер в качестве Service Locator.

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

Он не знает этого, и вам придется сначала ему это разъяснить. Вы делаете это посредством регистрации или конфигурирования, и именно здесь вы преобразуете абстракции в конкретные типы – я вернусь к этому вопросу в разделе "Конфигурирование DI-контейнеров". Если у контейнера не будет подходящей конфигурации для того, чтобы полноценно сформировать необходимый тип, он будет выдавать описательное исключение. Например, Castle Windsor имеет следующие примерные сообщения-исключения:

  • Невозможно создать компонент "Ploeh.Samples.MenuModel.Mayonnaise", поскольку у него есть зависимости, которые необходимо выделить.
  • Для Ploeh.Samples.MenuModel.Mayonnaise нужны следующие зависимости:
  • Сервисы:
  • – Ploeh.Samples.MenuModel.EggYolk, который не был зарегистрирован.

В данном примере вы можете увидеть, что Castle Windsor не может преобразовать Mayonnaise, поскольку он не был настроен для работы с классом EggYolk.

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

Автоматическая интеграция

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

Некоторые DI-контейнеры понимают и паттерн Property Injection (внедрение зависимостей через свойства), но все они, по своему существу, понимают паттерн Constructor Injection и формируют диаграммы объектов путем сочетания их собственной конфигурации с информацией, извлеченной из конструкторов классов. Данный процесс называется автоматической интеграцией.

Определение

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

Рисунок 3-4 описывает общий алгоритм, которому следует большинство DI-контейнеров, чтобы автоматически интегрировать диаграмму объектов. DI-контейнер будет использовать эту конфигурацию для того, чтобы найти соответствующий конкретный класс, совпадающий с запрашиваемым типом. Затем DI-контейнер использует рефлексию для изучения конструктора класса. Если существует конструктор по умолчанию, то DI-контейнер будет вызывать конструктор и возвращать созданный экземпляр.

Рисунок 3-4: Упрощенная последовательность действий для автоматической интеграции. DI-контейнер будет рекурсивно находить конкретные типы и изучать их конструкторы до тех пор, пока он не сможет создать целостное дерево объектов.

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

В разделе "Конфигурирование DI-контейнеров" мы подробнее рассмотрим то, как можно конфигурировать контейнеры, а сейчас самое главное – это понять, что центральной частью конфигурации является то, как различные абстракции преобразуются в конкретные классы. Все это звучит слегка теоретически (думаю, что слово "абстракция" не поможет), поэтому я думаю, что пример будет очень полезным.

Пример: Автоматическая интеграция BasketController

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

Представьте себе, что вам нужно преобразовать экземпляр класса BasketController. Вы делаете это путем вызова метода Resolve для typeof(BasketController). В итоге вам хотелось бы получить экземпляр BasketController, сформированный так, как это показано на рисунке 2-26. Чтобы достичь этого, вы должны сначала убедиться, что контейнер имеет корректную конфигурацию. Таблица 3-1 демонстрирует, как эта конфигурация преобразует абстракции в конкретные типы. Кроме того, я добавил столбец, который показывает, является ли абстракция интерфейсом или абстрактным базовым классом – с точки зрения DI-контейнера это очень важно, но я думал, что это поможет прояснить то, что происходит.

Таблица 3-1: Преобразование типов для обеспечения автоматической интеграции BasketController
Тип абстракции Абстракция Конкретный тип
Явный BasketController BasketController
Интерфейс IBasketService BasketService
Абстрактный класс BasketRepository SqlBasketRepository
Абстрактный класс BasketDiscountPolicy RepositoryBasketDiscountPolicy
Абстрактный класс DiscountRepository SqlDiscountRepository
Строка connString "metadata=res://*/CommerceModel.csdl| […]"

Когда DI-контейнер получит запрос BasketController, первое, что он сделает – будет искать тип в его конфигурации. BasketController – это конкретный класс, поэтому он преобразуется в самого себя. Затем контейнер использует рефлексию для осмотра конструктора BasketController. Из раздела "Возможность добавления в корзину" вы можете помнить, что BasketController обладает единственным конструктором со следующей сигнатурой:

public BasketController(IBasketService basketService)

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

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

public BasketService(BasketRepository repository,
	BasketDiscountPolicy discountPolicy)

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

SqlBasketRepository обладает открытым конструктором со следующей сигнатурой:

public SqlBasketRepository(string connString)

Единственный аргумент конструктора – это строковый параметр под названием connString, который сконфигурирован таким образом, что обладает конкретным значением. Теперь, когда у контейнера есть подходящее значение, он может вызвать конструктор SqlBasketRepository. На данный момент контейнер успешно обрабатывал параметр repository конструктора BasketService, но ему понадобится придержать это значение ненадолго, поскольку ему также нужно позаботиться и о параметре discountPolicy.

Согласно конфигурации BasketDiscountPolicy преобразуется в конкретный класс RepositoryBasketDiscountPolicy, который обладает следующим открытым конструктором:

public RepositoryBasketDiscountPolicy(DiscountRepository repository)

Выполняя поиск DiscountRepository в своей конфигурации, контейнер обнаруживает, что он DiscountRepository преобразуется в SqlDiscountRepository, который обладает следующим конструктором:

public SqlDiscountRepository(string connString)

Ситуация совпадает с той, когда вы сталкивались с SqlBasketRepository. Аргумент connString преобразуется в конкретную строку соединения, которую контейнер может передать в конструктор.

На данный момент контейнер передает новый экземпляр SqlDiscountRepository в конструктор RepositoryBasketDiscountPolicy. Наряду с SqlBasketRepository, он теперь выполняет конструктор BasketService и вызывает его посредством рефлексии. В конце концов, он передает вновь созданный экземпляр BasketService в конструктор BasketController и возвращает экземпляр BasketController.

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

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