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

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

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

Марк Симан

10.1. Знакомство с Castle Windsor

Из этого раздела вы узнаете, где можно взять Castle Windsor, что вы при этом получите и как начать его использовать. Кроме того, мы рассмотрим универсальные варианты конфигурации, а также то, как упаковать настройки конфигурации в повторно используемые компоненты. В таблице 10-1 содержится фундаментальная информация, которая, скорее всего, понадобится вам для того, чтобы приступить к работе.

Как показывает рисунок 10-2, в Castle Windsor присутствует простая цикличность: конфигурирование контейнера путем добавления компонентов, а затем разрешение необходимых компонентов.

Рисунок 10-2: Общий паттерн применения Castle Windsor прост: сначала мы конфигурируем контейнер, затем разрешаем компоненты из этого контейнера. В большинстве случаев мы создаем экземпляр WindsorContainer и полностью конфигурируем его перед тем, как начать разрешать компоненты из этого контейнера. Мы разрешаем компоненты из того экземпляра, который конфигурируем.
Таблица 10-1: Краткая информация о Castle Windsor
Вопрос Ответ
Откуда мне его получить? Перейти на сайт http://www.castleproject.org/download/ и нажать на ссылку соответствующего релиза.

Из Visual Studio 2010 можно получить его посредством NuGet. Имя пакета – Castle.Windsor.
Что находится в загруженном файле? Можно загрузить zip-файл, содержащий предварительно скомпилированные бинарные файлы. Кроме того, можно получить текущий исходный код и скомпилировать его самостоятельно.

Бинарные файлы – это dll-файлы, которые можно размещать там, где захочется, и ссылаться на них из собственного кода.
Какие платформы поддерживаются? .NET 3.5 SP1, .NET 4 Client Profile, .NET 4, Silverlight 3, Silverlight 4.
Сколько он стоит? Нисколько. Это программное обеспечение с открытым исходным кодом, обладающее мягкой лицензией.
Где мне получить помощь? Коммерческое сопровождение можно получить от Castle Stronghold. Больше информации по этому вопросу можно получить на сайте www.castlestronghold.com/services/support.

Помимо коммерческого сопровождения, Castle Windsor остается программным обеспечением с открытым исходным кодом, имеющим бурно развивающуюся экосистему, поэтому, скорее всего (но не гарантированно), вы получите помощь на официальном форуме http://groups.google.com/group/castle-project-users. Stack Overflow (http://stackoverflow.com/) – еще одно место, где можно задать вопросы.
На какой версии Castle Windsor основана данная глава? 2.5.2

После окончания изучения данного раздела вы должны будете уже хорошо разбираться в Castle Windsor, а также уметь использовать его в сценариях, в которых все компоненты руководствуются соответствующими DI-паттернами, например, Constructor Injection. Мы начнем с самого простого сценария и увидим, как можно разрешать объекты с помощью контейнера Windsor.

Разрешение объектов

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

var container = new WindsorContainer();
container.Register(Component.For<SauceBéarnaise>());
SauceBéarnaise sauce = container.Resolve<SauceBéarnaise>();

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

После соответствующей конфигурации контейнера можно разрешить тип SauceBéarnaise для того, чтобы получить его экземпляр. Вам не придется выполнять проверку на null-значение, поскольку WindsorContainer выдаст исключение, если не сможет выполнить автоматическую интеграцию и вернуть экземпляр запрашиваемого типа.

Примечание

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

Этот первый пример функционально эквивалентен прямому созданию экземпляра класса SauceBéarnaise с помощью ключевого слова new: ничего нового еще не приобретено. Вспомните, что механизм внедрения зависимостей – средство достижения результата, а результатом является слабое связывание. Для того чтобы достичь слабого связывания, вы должны преобразовать абстракции к конкретным типам.

Преобразование абстракций к конкретным типам

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

Ниже вы преобразуете интерфейс IIngredient к конкретному классу SauceBéarnaise, который позволяет вам успешно разрешать IIngredient:

var container = new WindsorContainer();
container.Register(Component
	.For<IIngredient>()
	.ImplementedBy<SauceBéarnaise>());
IIngredient ingredient = container.Resolve<IIngredient>();

Вместо регистрации конкретного типа вы преобразуете абстракцию к конкретному типу. Когда вы позднее запросите экземпляр IIngredient, контейнер вернет экземпляр SauceBéarnaise.

Строго типизированное свободное API, доступное через класс Component (Castle.MicroKernel.Registration.Component, а не System.ComponentModel.Component), помогает предотвратить ошибки конфигурации, поскольку метод ImplementedBy имеет generic-ограничитель, который гарантирует, что тип, указанный в аргументе типа, реализует аргумент типа абстракция, заданный в методе For. То есть, предыдущий код примера компилируется, потому что SauceBéarnaise реализует IIngredient.

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

Разрешение слабо типизированных сервисов

В некоторых случаях мы не можем писать generic-код для разрешения типа, так как можем даже не знать точный тип абстракции на этапе проектирования. Хороший пример такой ситуации – DefaultControllerFactory в ASP.NET MVC, который мы обсуждали в разделе 7.2 "Построение ASP.NET MVC приложений". Соответствующая часть этого класса – виртуальный метод GetControllerInstance:

protected internal virtual IController GetControllerInstance(
	RequestContext requestContext, Type controllerType);

В этом API нет строго типизированных generic-ов. Вместо них нам предоставляют Type и просят вернуть экземпляр IController. Класс WindsorContainer также обладает слабо типизированной версией метода Resolve. Вы можете использовать этот метод для реализации GetControllerInstance:

return (IController)this.container.Resolve(controllerType);

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

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

В предыдущем примере this.container – это экземпляр IWindsorContainer. Для того чтобы иметь возможность разрешать требуемые типы, необходимо перед этим зарегистрировать все типы и их зависимости. Существует множество способов конфигурации контейнера Windsor, и в следующем разделе рассматриваются наиболее универсальные из этих способов.

Конфигурирование контейнера

Как обсуждалось в разделе 3.2 "Конфигурирование DI-контейнеров", существует несколько, концептуально разных способов конфигурирования DI-контейнера. На рисунке 10-3 представлен обзор возможных вариантов.

Рисунок 10-3: Концептуально разные варианты конфигурирования. Использование кода в качестве конфигурации подразумевает строгую типизированность и явное объявление. XML, с другой стороны, – позднее связывание, но все равно явное объявление. Автоматическая регистрация полагается на соглашения, которые могут быть строго типизированными и более слабо определенными.

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

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

Code as Configuration

В главе 3 вы видели примеры API Castle Windsor, использующего код в качестве конфигурации. Каждая регистрация инициализируется с помощью метода Register и обычно указывается при помощи Fluent API.

Мы конфигурируем WindsorContainer методом Register, который в качестве входных данных принимает массив IRegistration. На первый взгляд все это выглядит довольно абстрактным. Но вместо того, чтобы возлагать на нас обязанность определения того, какую реализацию IRegistration использовать, Castle Windsor предоставляет Fluent Registration API, которое позволяет создавать экземпляры IRegistration с более понятным синтаксисом.

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

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

Не путайте Castle.MicroKernel.Registration.Component с System.ComponentModel.Component из стандартной библиотеки классов.

Как вы уже видели ранее, самая простая возможная регистрация – регистрация конкретного типа:

container.Register(Component.For<SauceBéarnaise>());

Данный код регистрирует класс SauceBéarnaise с помощью контейнера, но не обеспечивает никакого преобразования. Даже если SauceBéarnaise будет реализовывать IIngredient, контейнер выдаст исключение, если вы попросите его разрешить IIngredient:

container.Resolve<IIngredient>()

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

container.Register(Component
	.For<IIngredient>()
	.ImplementedBy<SauceBéarnaise>());

Обратите внимание на то, что теперь вместо класса SauceBéarnaise вы регистрируете интерфейс IIngredient. Это позволит вам разрешить IIngredient, но, что может показаться слегка удивительным, вы при этом потеряли способность разрешать конкретный класс SauceBéarnaise. Изредка тот факт, что код слабо связан, становится проблемой, но в исключительных ситуациях, когда вам нужно уметь разрешать оба типа, вы можете обеспечить это с помощью перегрузки метода For:

container.Register(Component
	.For<SauceBéarnaise, IIngredient>());

Данный код регистрирует компонент SauceBéarnaise, в то же самое время перенаправляя регистрацию к интерфейсу IIngredient. Это означает, что и SauceBéarnaise, и IIngredient регистрируются как разрешаемые типы. В обоих случаях реализация обеспечивается с помощью SauceBéarnaise. Заметьте, что при использовании этой перегрузки вам не нужно неявно использовать метод ImplementedBy.

Очевидно, вы можете регистрировать составные типы при помощи последовательных вызовов метода Register:

container.Register(Component
	.For<IIngredient>()
	.ImplementedBy<SauceBéarnaise>());
container.Register(Component
	.For<ICourse>()
	.ImplementedBy<Course>());

Данный код регистрирует и интерфейс IIngredient, и интерфейс ICourse, а также преобразует их к конкретным типам. Тем не менее, регистрация одной и той же абстракции несколько раз имеет приводит к некоторым интересным последовательностям:

container.Register(Component
	.For<IIngredient>()
	.ImplementedBy<Steak>());
container.Register(Component
	.For<IIngredient>()
	.ImplementedBy<SauceBéarnaise>());

В этом примере вы регистрируете IIngredient дважды. Если вы разрешаете IIngredient, то получаете экземпляр Steak. Выигрывает первая регистрация, но последующие регистрации не забыты. Castle Windsor имеет изощренную модель работы с составными регистрациями. Мы вернемся к этому в разделе 10.3 "Работа с составными компонентами".

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

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

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

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

Сделать это можно путем применения статического класса AllTypes, который играет роль, аналогичную той роли, которую играет класс Component. Мы можем использовать один из методов класса AllTypes для того, чтобы просмотреть сборку на факт наличия типов, удовлетворяющих определенному критерию. Приведенный ниже код регистрирует все реализации IIngredient за один шаг:

container.Register(AllTypes
	.FromAssemblyContaining<Steak>()
	.BasedOn<IIngredient>());

Класс AllTypes предоставляет множество методов, которые дают нам возможность обратиться к конкретной сборке, но я считаю generic-метод FromAssemblyContaining наиболее кратким: предоставь ему тип представителя в качестве параметра типа, и он будет использовать сборку, содержащую данный тип. Существуют также и другие методы, предоставляющие возможность обеспечения Assembly с помощью других средств.

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

container.Register(AllTypes
	.FromAssemblyContaining<SauceBéarnaise>()
	.Where(t => t.Name.StartsWith("Sauce"))
	.WithService.AllInterfaces());

Обратите внимание на то, что вы применяете предикат к методу Where, который выполняет фильтрацию по типу имени. Любой тип, имя которого начинается с Sauce, будет отобран из сборки, содержащей класс SauceBéarnaise. Свойство WithService позволяет задать правило регистрации типа. В данном примере вы регистрируете все типы относительно всех интерфейсов, которые они реализуют.

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

В классе AllTypes существует даже метод, который принимает в качестве входного параметра имя сборки. Он будет использовать Fusion (средство загрузки сборки .NET Framework) для обнаружения соответствующей сборки. Сочетая сборку с поздним связыванием и нетипизированный предикат, можно продвинуться вглубь территории позднего связывания. Такая возможность могла бы стать полезным приемом реализации дополнений, поскольку Castle Windsor также может просматривать все сборки в директории.

Еще один способ регистрации дополнений и других сервисов с поздним связыванием – применение возможности XML конфигурации Castle Windsor.

XML конфигурация

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

Подсказка

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

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

Сделать это можно несколькими способами, но рекомендуемый способ – использовать метод Install (подробнее об Installer'ах мы поговорим в разделе 10.1.3 "Пакетирование конфигурации"):

container.Install(Configuration.FromAppConfig());

Метод FromAppConfig возвращает экземпляр ConfigurationInstaller, который читает XML конфигурацию Castle Windsor из конфигурационного файла приложения и преобразует ее в объекты, понятные контейнеру.

Для того чтобы разрешить размещать конфигурацию Castle Windsor в конфигурационном файле, вы должны сначала добавить секцию конфигурации:

<configSections>
	<section name="castle"
				type="Castle.Windsor.Configuration.AppDomain
				➥.CastleSectionHandler, Castle.Windsor" />
</configSections>

Данный код позволяет вам добавить секцию конфигурации castle в конфигурационный файл. Ниже приведен простой пример, который преобразует интерфейс IIngredient в класс Steak:

<castle>
	<components>
		<component id="ingredient.sauceBéarnaise"
					service="IIngredient"
					type="Steak"/>
	</components>
</castle>

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

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

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

Подсказка

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

В данном разделе мы, главным образом, рассматривали различные API конфигурации Castle Windsor. Несмотря на то, что наверняка можно написать один большой блок неструктурированного кода конфигурации, лучше всего разделить конфигурацию на модули. Для этих целей у нас есть Windsor Installer'ы.

Пакетирование конфигурации

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

Благодаря Castle Windsor мы можем упаковать конфигурацию в Installer'ы. Installer – это класс, который реализует интерфейс IWindsorInstaller:

public interface IWindsorInstaller
{
	void Install(IWindsorContainer container, IConfigurationStore store);
}

Все, что вы делали до настоящего момента, вы также можете сделать и внутри Installer. Следующий листинг демонстрирует Installer, который регистрирует все реализации IIngredient.

Листинг 10-1: Реализация Windsor Installer
public class IngredientInstaller : IWindsorInstaller
{
	public void Install(IWindsorContainer container,
		IConfigurationStore store)
	{
		container.Register(AllTypes
			.FromAssemblyContaining<Steak>()
			.BasedOn<IIngredient>());
	}
}

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

Для того чтобы зарегистрировать Installer, вызовите метод Install:

container.Install(new IngredientInstaller());

Несмотря на то, что метод Install можно вызывать бессчисленное множество раз, в документации к Castle Windsor рекомендуется выполнять всю конфигурацию в единственном вызове метода Install. Метод Install принимает в качестве параметра массив экземпляров IWindsorInstaller:

public IWindsorContainer Install(params IWindsorInstaller[] installers);

Подсказка

Windsor Installer'ы позволяют вам упаковывать и структурировать код конфигурации вашего контейнера. Используйте их вместо встроенной конфигурации: это сделает вашу Composition Root более читабельной.

Подсказка

Кроме преимуществ, которые Installer'ы предлагают для вашего собственного кода, Castle Windsor также движется по направлению оптимизации большинства своих API вокруг Installer'ов. Это своеобразный и рекомендуемый способ конфигурирования в большей степени Castle Windsor, нежели других контейнеров.

Кроме того, вы можете задать один или более одного Installer'а в XML, и загрузить конфигурационный файл, как это было описано ранее:

<installers>
	<install type="IngredientInstaller" />
</installers>

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

Данный раздел познакомил вас с DI-контейнером Castle Windsor и продемонстрировал основные принципы: как сконфигурировать контейнер и впоследствии использовать его для того, чтобы разрешать сервисы. Выполнять разрешение сервисов легко с помощью единичного вызова метода Resolve, поэтому вся сложность заключается в конфигурировании контейнера. Это можно сделать несколькими различными способами, включая императивный код и XML. До настоящего момента мы рассматривали только самое основное API. Есть и более перспективные области, которые нам необходимо рассмотреть. Одна из наиболее важных тем – как управлять жизненным циклом компонентов.