Главная страница   /   3.2. Конфигурирование DI-контейнеров (Внедрение зависимостей в .NET

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

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

Марк Симан

3.2. Конфигурирование DI-контейнеров

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

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

Рисунок 3-5: Наиболее универсальные способы конфигурирования DI-контейнера, показанные относительно таких параметров, как ясность и степень связывания.

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

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

В таблице 3-2 перечисляются преимущества и недостатки каждой опции.

Таблица 3-2: Опции конфигурации
Стиль Описание Преимущества Недостатки
XML Настройки конфигурации (часто в .config файлах) определяют преобразования. Обеспечивает возможность замены без повторной компиляции. Высокая степень контроля. Отсутствуют проверки во время компиляции. Очень подробный.
Использование кода в качестве конфигурации Код явно определяет преоразования. Проверки во время компиляции. Высокая степень контроля. Не поддерживает возможность замены без повторной компиляции.
Автоматическая регистрация Для определения местоположения подходящих компонентов и построения преобразований используются правила. Обеспечивает возможность замены без повторной компиляции. Необходимы минимальные усилия. Помогает принудительным соглашениям сделать базу кода более логичной. Частичные проверки во время компиляции. Наименьший контроль.

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

Конфигурирование контейнеров при помощи XML

Когда DI-контейнеры впервые появились в ранние 2000-е годы, все они использовали XML в качестве механизма конфигурации – с тех пор многое изменилось. Более частое использование XML в качестве механизма конфигурации в дальнейшем раскрыло тот факт, что такой подход изредка является самым лучшим.

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

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

Подсказка

Используйте XML конфигурацию только в тех случаях, когда вам очень хочется обеспечить позднее связывание. Во всех остальных случаях отдайте предпочтение стилю "код в качестве конфигурации" или автоматической регистрации.

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

Пример: Конфигурирование шаблонного коммерческого приложения с помощью XML

Поскольку контейнер Unity является одним из самых XML-центрированных DI-контейнеров, рассматриваемых в данной книге, имеет смысл использовать его для примера XML конфигурации.

В этом примере вы будете конфигурировать шаблонное коммерческое приложение из раздела "Расширение шаблонного приложения". Самая большая составляющая задачи – применить конфигурацию, которая законспектирована в таблице 3-1, но кроме того вы должны применить похожую конфигурацию для того, чтобы обеспечить композицию класса HomeController. Следующий листинг демонстрирует конфигурацию, необходимую для поднятия и запуска приложения.

Листинг 3-1: Конфигурирование Unity при помощи XML
<register type="IBasketService" mapTo="BasketService" />
<register type="BasketDiscountPolicy" mapTo="RepositoryBasketDiscountPolicy" />
<register type="BasketRepository" mapTo="SqlBasketRepository">
	<constructor>
		<param name="connString">
			<value value="CommerceObjectContext"
				typeConverter="ConnectionStringConverter" />
		</param>
	</constructor>
</register>
<register type="DiscountRepository" mapTo="SqlDiscountRepository">
	<constructor>
		<param name="connString">
			<value value="CommerceObjectContext"
				typeConverter="ConnectionStringConverter" />
		</param>
	</constructor>
</register>
<register type="ProductRepository" mapTo="SqlProductRepository">
	<constructor>
		<param name="connString">
			<value value="CommerceObjectContext"
				typeConverter="ConnectionStringConverter" />
		</param>
	</constructor>
</register>
<register type="CurrencyProvider" mapTo="SqlCurrencyProvider">
	<constructor>
		<param name="connString">
			<value value="CommerceObjectContext"
				typeConverter="ConnectionStringConverter" />
		</param>
	</constructor>
</register>

Строка 4-9: Определяет строку соединения

Строка 1: Простое преобразование

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

Тем не менее, как вы, возможно, помните, некоторые конкретные классы принимают строку соединения в качестве входных данных, поэтому вам необходимо определить, каким образом находится значение этой строки. Что касается Unity, вы можете сделать это, указав, что вы используете пользовательский тип конвертера под названием ConnectionStringConverter. Этот конвертер будет искать значение CommerceObjectContext среди стандартных строк соединения web.config и возвращать строку соединения с этим именем.

Оставшиеся элементы повторяют эти два паттерна.

Поскольку Unity может автоматически преобразовывать запросы в конкретные типы, даже если отсутствуют явные регистрации, вам не нужно применять XML элементы для HomeController и BasketController.

Загрузка конфигурации в контейнер выполняется при помощи вызова единственного метода:

container.LoadConfiguration();

Метод LoadConfiguration загружает XML конфигурацию из листинга 3-1 в контейнер. После размещения конфигурации контейнер теперь может преобразовывать запросы в HomeController и в другие.

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

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

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

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

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

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

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

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

Мартин Фаулер

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

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

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

Подсказка

Используйте код в качестве конфигурации вместо XML конфигурации, за исключением тех моментов, когда вам нужно позднее связывание. Компилятор может быть полезным, а Visual Studio, которая формирует систему, будет автоматически копировать все необходимые зависимости в выходную папку.

Многие конфигурационные API используют дженерики и Fluent Builders для регистрации компонентов; StructureMap – не исключение.

Пример: Конфигурирование шаблонного коммерческого приложения с помощью кода

В разделе "Конфигурирование контейнеров при помощи XML" вы увидели, как сконфигурировать шаблонное коммерческое приложение с помощью XML, используя Unity. Я также мог бы продемонстрировать использование кода в качестве конфигурации на примере Unity, но в этом примере я буду использовать StructureMap; так как он обладает более выразительным API, он лучше подходит для страниц этой книги.

Используя конфигурационное API StructureMap, вы можете выразить конфигурацию из листинга 3-1 более компактно, как это продемонстрировано в следующем листинге.

Листинг 3-2: Конфигурирование StructureMap с помощью кода
c.For<IBasketService>()
	.Use<BasketService>();
c.For<BasketDiscountPolicy>()
	.Use<RepositoryBasketDiscountPolicy>();
string connectionString = ConfigurationManager
	.ConnectionStrings["CommerceObjectContext"].ConnectionString;
c.For<BasketRepository>().Use<SqlBasketRepository>()
	.Ctor<string>().Is(connectionString);
c.For<DiscountRepository>().Use<SqlDiscountRepository>()
	.Ctor<string>().Is(connectionString);
c.For<ProductRepository>().Use<SqlProductRepository>()
	.Ctor<string>().Is(connectionString);
c.For<CurrencyProvider>().Use<SqlCurrencyProvider>()
	.Ctor<string>().Is(connectionString);

Сравните этот код с кодом из листинга 3-1 и заметьте, насколько он более компактен – несмотря на то, что выполняет он тоже самое. Такое простое преобразование, как преобразование IBasketService в BasketService, выражается с помощью видовых методов For и Use. Переменная c фактически является так называемым ConfigurationExpression, но воспринимайте ее как контейнер.

Для того чтобы поддержать те классы, для которых нужна строка соединения, вы продолжаете последовательность For/Use путем вызова метода Ctor и передачи строки соединения. Метод Ctor выполняет поиск строкового параметра в конструкторе конкретного класса и использует переданное значение для этого параметра.

Остальная часть кода повторяет эти два паттерна.

Использование кода в качестве конфигурации не только компактнее XML конфигурации, но также поддерживается компилятором. Типы аргументов, используемые в листинге 3-2, представляют собой реальные типы, которые проверяет компилятор. Переменное API StructureMap поставляется даже с некоторыми видовыми ограничителями, которые сообщают компилятору о проверке того, совпадает ли тип, определяемый методом Use с абстракциями, обозначенными с помощью метода For. Если преобразование невозможно, то код не компилируется.

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

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

Обратили ли вы в листинге 3-2 внимание на то, насколько схожи большинство регистраций? В частности все компоненты доступа к данным, базирующиеся на SQL Server, руководствуются универсальным паттерном в тех случаях, когда вы конфигурируете компонент подходящей строкой соединения.

Неоднократное написание такого кода регистрации нарушает принцип DRY (Don't Repeat Yourself – Не повторяйся). К тому же он выглядит как непродуктивный фрагмент кода инфраструктуры, который не добавляет особую значимость в приложение. Вы можете сэкономить время и допустить меньшее количество ошибок, если сможете автоматизировать процесс регистрации компонентов.

Все более популярной становится архитектурная модель – "соглашения по конфигурации" (Convention over Configuration). Вместо того чтобы писать и поддерживать большие объемы конфигурационного кода, вы можете принять соглашения, которые влияют на базу кода.

Способ, при помощи которого ASP.NET MVC находит контроллеры по их именам, – это отличный пример простого соглашения.

  1. Поступает запрос контроллера с именем Home.
  2. Используемая по умолчанию фабрика контроллеров ищет в списке известных пространств имен класс с названием HomeController. Если она находит такой класс и этот класс реализует IController, то это как раз то, что нужно.
  3. Используемая по умолчанию фабрика контроллеров использует конструктор по умолчанию найденного класса для того, чтобы создать экземпляр контроллера.

Здесь используются, по крайней мере, два соглашения: контроллер должен иметь название [Имя контроллера]Controller и должен обладать конструктором по умолчанию.

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

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

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

Определение

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

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

Подсказка

Принцип "Соглашения по конфигурации" имеет больше преимуществ, нежели просто поддержка DI конфигурации. Он делает ваш код более последовательным, поскольку код будет автоматически работать, пока вы будете следовать вашим соглашениям.

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

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

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

Если вы рассмотрите листинги 3-1 и 3-2, то надеюсь, что вы согласитесь с тем, что регистрирование различных компонентов доступа к данным – это наиболее повторяющиеся части кода. Можем ли мы выразить для них соглашение некоторого рода?

Все четыре конкретных типа обладают следующими общими характеристиками:

  • Все они определены в одной и той же сборке.
  • Каждый из них является конкретным классом, унаследованным от абстрактного базового класса.
  • Имя каждого из них начинается с Sql.
  • Каждый из них обладает единственным открытым конструктором, который принимает строковый параметр с названием connString.

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

string connectionString = ConfigurationManager
	.ConnectionStrings["CommerceObjectContext"]
	.ConnectionString;
var a = typeof(SqlProductRepository).Assembly;
builder.RegisterAssemblyTypes(a)
	.Where(t => t.Name.StartsWith("Sql"))
	.As(t => t.BaseType)
	.WithParameter("connString", connectionString);

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

Теперь, когда у вас есть сборка, вы можете сообщить контейнеру о том, что вы хотите просмотреть ее. Метод RegisterAssemblyTypes указывает на намерение регистрировать все типы сборки, которые соответствуют критерию, согласно которому имя класса должно начинаться Sql. Переменная builder – это экземпляр класса ContainerBuilder, но вы можете считать, что он является контейнером.

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

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

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

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

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

builder.RegisterAssemblyTypes(typeof(BasketService).Assembly)
	.Where(t => t.Name.EndsWith("Service"))
	.AsImplementedInterfaces();

Это соглашение просматривает определенную сборку на факт наличия типов, названия которых заканчиваются на Service, и регистрирует каждый тип относительно интерфейса, который он реализует. Фактически это соглашение регистрирует BasketService относительно интерфейса IBasketService, но поскольку в настоящее время у вас нет никаких других совпадений с этим соглашением, вы ничего из этого не получаете. Однако возможно имеет смысл сформулировать соглашение заранее для того, чтобы вдохновить разработчиков на следование этому соглашению.

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

До настоящего момента вы видели три разных подхода к конфигурированию DI-контейнера:

  • XML
  • Использование кода в качестве конфигурации
  • Автоматическая регистрация

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

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

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