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

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

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

Марк Симан

11.1. Знакомство с StructureMap

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

Таблица 11-1: Краткая информация о StructureMap
Вопрос Ответ
Откуда мне его получить? Перейти на сайт http://structuremap.github.com/structuremap/index.html и нажать на ссылку Download the Latest Release.

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

Бинарные файлы – это dll-файлы, которые можно размещать там, где захочется, и ссылаться на них из собственного кода.
Какие платформы поддерживаются? .NET 3.5 SP1, .NET 4
Сколько он стоит? Нисколько. Это программное обеспечение с открытым исходным кодом.
Откуда мне получить помощь? Гарантированная поддержка отсутствует, но получить помощь можно на официальном форуме http://groups.google.com/group/structuremapusers.
На какой версии StructureMap основана данная глава? 2.6.1

Как и в случае с Castle Windsor, при использовании StructureMap соблюдается простой ритм, проиллюстрированный на рисунке 11-2.

Рисунок 11-2: Полноценный паттерн применения StructureMap довольно прост: сначала мы конфигурируем контейнер, а затем разрешаем компоненты из этого контейнера. В большинстве случаев мы создаем экземпляр класса Container и полностью конфигурируем его перед тем, как начать разрешать компоненты из него. Мы разрешаем компоненты того же экземпляра, который и конфигурировали.

Контейнер или ObjectFactory?

В более ранних версиях StructureMap статический класс ObjectFactory использовался в качестве одиночного контейнера приложений. Он использовался следующим образом:

SauceBéarnaise sauce = ObjectFactory.GetInstance<SauceBéarnaise>();

Среди нескольких проблем, возникающих при использовании статической фабрики, можно выделить тот факт, что статическая фабрика способствует неправильному ее использованию в качестве Service Locator. На сегодняшний момент использование класса ObjectFactory не приветствуется, при этом предпочтение отдается экземплярам контейнеров. На сайте StructureMap (и не только на нем) представлено множество примеров, в которых образцы кода ObjectFactory используются для демонстрации различных возможностей StructureMap. Но мы должны рассматривать их как рудименты более ранних лет.

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

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

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

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

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

Ключевая обязанность любого DI-контейнера – разрешение компонентов. В данном разделе мы рассмотрим API, позволяющее нам разрешать компоненты с помощью StructureMap.

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

var container = new Container();
SauceBéarnaise sauce = container.GetInstance<SauceBéarnaise>();

Если у вас есть экземпляр StructureMap.Container, то для получения экземпляра конкретного класса SauceBéarnaise, вы можете использовать generic-метод GetInstance. Поскольку у этого класса есть конструктор по умолчанию, StructureMap автоматически поймет, как создать его экземпляр. При этом никакой явной конфигурации контейнера не требуется.

Примечание

Метод GetInstance<T> аналогичен методу Resolve<T> контейнера Windsor.

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

В качестве примера рассмотрите приведенный ниже конструктор Mayonnaise:

public Mayonnaise(EggYolk eggYolk, OliveOil oil)

Между тем, как рецепт майонеза достаточно прост, и EggYolk, и OliveOil являются конкретными классами, обладающими конструкторами по умолчанию. Несмотря на то, что сам Mayonnaise не имеет конструктора по умолчанию, StructureMap все равно может создать его, не выполняя при этом никакой конфигурации:

var container = new Container();
var mayo = container.GetInstance<Mayonnaise>();

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

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

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

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

var container = new Container();
container.Configure(r => r
	.For<IIngredient>()
	.Use<SauceBéarnaise>());
IIngredient ingredient = container.GetInstance<IIngredient>();

Метод Configure предоставляет возможность сконфигурировать ConfigurationExpression с помощью блока кода (объяснение смотри в приведенном ниже блоке "Nested Closures" (вложенные замыкания)). Оператор конфигурации читается почти как предложение (или как инструкция из кулинарной книги): для IIngredient используйте SauceBéarnaise. Метод For позволяет определить абстракцию, а метод Use позволяет определить конкретный тип, реализующий абстракцию.

Строго типизированное API, предоставляемое классом ConfigurationExpression, помогает предотвратить ошибки конфигурации, поскольку метод Use имеет generic-ограничение, настаивающее на том, чтобы тип, указанный в аргументе типа, наследовался от аргумента типа абстракции, указанного в методе For. Предыдущий пример кода компилируется, поскольку SauceBéarnaise реализует IIngredient.

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

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

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

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

Поскольку у вас есть только экземпляр Type, вы не можете воспользоваться generic'ами и вместо них должны обратиться к слабо типизированным API. К счастью, StructureMap предлагает слабо типизированную перегрузку метода GetInstance, которая позволяет реализовать метод GetControllerInstance приведенным ниже способом:

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

Слабо типизированная перегрузка GetInstance позволяет передавать аргумент controllerType прямо в StructureMap, но также требует, чтобы вы явным образом привели возвращаемое значение к IController.

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

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

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

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

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

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

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

Конфигурация в коде

В разделе 11.1.1 "Разрешение объектов" вы уже видели небольшой намек на строго типизированное API для конфигурации StructureMap. В данном разделе мы рассмотрим его подробнее.

Существует несколько точек входа в конфигурационное API. Вы уже видели, что оно вызывается посредством явного использования метода Configure:

var container = new Container();
container.Configure(r => r
	.For<IIngredient>()
	.Use<SauceBéarnaise>());

Еще один вариант – определение точно такого же блока кода прямо при создании экземпляра Container:

var container = new Container(r => r
	.For<IIngredient>()
	.Use<SauceBéarnaise>());

Результат тот же, однако, в данной главе я руководствуюсь соответствующим соглашением и предпочитаю использовать метод Configure, а не конструктор.

Вложенные замыкания

StructureMap широко использует паттерн Вложенное замыкание (Nested Closure), в котором конфигурация определяется блоками кода (известными как лямбда-выражения). В качестве примера ниже приведена сигнатура метода Configure:

public void Configure(Action<ConfigurationExpression> configure);

Параметр configure – делегат, который принимает ConfigurationExpression в качестве входного параметра. В примерах кода, приведенных в этой главе, этот параметр обычно обозначается как r, и обычно я передаю делегат в виде блока кода, выраженного посредством параметра r.

При просмотре примеров кода, встречающихся на сайте StructureMap или в блоге Джереми Миллера, можно обнаружить, что иногда имя параметра, используемого в блоке кода, задается как x, а иногда – как registry. Поскольку подходящие преценденты отсутствуют, я решил использовать r (соответствующее registry) в качестве условного обозначения, которое будет применяться в данной главе. Несмотря на то, что r не является достаточно понятным именем для переменной, рассматриваемые здесь небольшие по объему блоки кода делают r более подходящим для этих целей именем, нежели более длинное и менее краткое имя.

Класс ConfigurationExpression содержит множество методов, которые можно использовать для конфигурирования StructureMap. Один из этих методов, For, мы уже видели ранее. Как вы увидите в данном разделе позднее, еще одним таким методом является метод Scan с приведенной ниже сигнатурой:

public void Scan(Action<IAssemblyScanner> action);

Обратите внимание на то, что сам метод Scan принимает делегат в качестве входного параметра. Когда вы передаете блок кода метода Scan, то получается, что у вас имеется один блок кода внутри другого блока кода – отсюда и название Nested Closure (вложенное замыкание).

В отличие от Castle Windsor преобразование IIngredient в SauceBéarnaise продемонстрированным ранее способом не исключает разрешения самого SauceBéarnaise. То есть, и sauce, и ingredient будут разрешены должным образом:

container.Configure(r =>
	r.For<IIngredient>().Use<SauceBéarnaise>());
var sauce = container.GetInstance<SauceBéarnaise>();
var ingredient = container.GetInstance<IIngredient>();

Если вы вспомните обсуждение, проводимое в разделе 10.1.2 "Конфигурирование контейнера", то вспомните и то, что преобразование IIngredient в SauceBéarnaise с помощью Castle Windsor приводит к тому, что "исчезает" конкретный класс (SauceBéarnaise), и вам приходится использовать перенаправление типов (Type Forwarding), чтобы суметь разрешить и IIngredient, и SauceBéarnaise. При использовании StructureMap такие дополнительные шаги выполнять не нужно, поскольку StructureMap умеет преобразовывать и IIngredient, и SauceBéarnaise. В обоих случаях возвращаемые объекты являются экземплярами SauceBéarnaise.

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

container.Configure(r => 
{
	r.For<IIngredient>() 
		.Use<SauceBéarnaise>(); 
	r.For<ICourse>() 
		.Use<Course>(); 
});

и

container.Configure(r => r
	.For<IIngredient>()
	.Use<SauceBéarnaise>());
container.Configure(r => r
	.For<ICourse>()
	.Use<Course>());

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

container.Configure(r =>
	r.For<IIngredient>().Use<SauceBéarnaise>());
container.Configure(r =>
	r.For<IIngredient>().Use<Steak>());

В этом примере вы регистрируете IIngredient дважды. Если вы разрешаете IIngredient, то получаете экземпляр Steak. Выигрывает последняя конфигурация, но предыдущие конфигурации не забыты. StructureMap отлично управляет составными конфигурациями одной и той же абстракции, но к этой теме мы вернемся в разделе 11.3.1 "Выбор из составных кандидатов".

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

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

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

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

Это возможно благодаря методу Scan, который является еще одним примером обширного применения StructureMap делегатов. Метод Scan доступен в классе ConfigurationExpression, который уже доступен через блок кода. Именно здесь мы и видим паттерн Nested Closure в действии. Приведенный ниже пример кода конфигурирует все реализации IIngredient одним махом:

container.Configure(r =>
	r.Scan(s =>
	{
		s.AssemblyContainingType<Steak>();
		s.AddAllTypesOf<IIngredient>();
	}));

Метод Scan расположен в пределах блока кода Configure. Переменная s представляет собой экземпляр IAssemblyScanner, который можно использовать для определения того, каким образом необходимо просматривать сборку и как должны конфигурироваться типы.

Экземпляр IAssemblyScanner предоставляет несколько методов, которые можно использовать для того, чтобы определить, какие сборки необходимо просматривать и как конфигурировать типы из этой сборки. Мы можем использовать generic-метод AssemblyContainingType для идентификации сборки по типу представителя, но существует несколько других методов, позволяющих нам предоставить экземпляр Assembly или даже добавить все сборки, находящиеся по данному пути.

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

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

container.Configure(r =>
	r.Scan(s =>
	{
		s.AssemblyContainingType<Steak>();
		s.AddAllTypesOf<IIngredient>();
		s.Include(t => t.Name.StartsWith("Sauce"));
	}));

Единственное отличие от предыдущего примера заключается в добавлении вызова метода Include, который вводит третий уровень Nested Closure. Метод Include принимает в качестве параметра предикат, который используется для определения того, нужно ли включать данный Type или нет. В этом примере ответ – true, поскольку Name для Type начинается с Sauce.

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

Листинг 11-1: Реализация пользовательского соглашения
public class SauceConvention : IRegistrationConvention
{
	public void Process(Type type, Registry registry)
	{
		var interfaceType = typeof(IIngredient);
		if (!interfaceType.IsAssignableFrom(type))
		{
			return;
		}
		if (!type.Name.StartsWith("Sauce"))
		{
			return;
		}
		registry.For(interfaceType).Use(type);
	}
}

Класс SauceConvention реализует IRegistrationConvention, который определяет единичный член. Метод Process будет вызываться StructureMap для каждого типа сборки, определенного в методе Scan, поэтому вы должны явным образом предоставить набор граничных операторов, которые отфильтровывают все не нужные вам типы.

Граничные операторы гарантируют, что любой тип, проходящий через них, – это IIngredient, чье имя начинается с Sauce, поэтому теперь вы можете зарегистрировать этот тип с помощью registry. Обратите внимание на то, что Registry, между прочим, предоставляется посредством Method Injection, который имеет огромный смысл, поскольку IRegistrationConvention определяет встраиваемый элемент для StructureMap.

Можно использовать класс SauceConvention в методе Scan следующим образом:

container.Configure(r =>
	r.Scan(s =>
	{
		s.AssemblyContainingType<Steak>();
		s.Convention<SauceConvention>();
	}));

Обратите внимание на то, что вы все равно определяете сборку за рамками соглашения. Это позволяет вам менять источники типов, которые обрабатываются независимо от самого соглашения. SauceConvention определяется с помощью метода Convention. Данный метод требует, чтобы IRegistrationConvention, указанный в качестве аргумента типа, имел конструктор по умолчанию. Но существует также метод With, принимающий в качестве входного параметра экземпляр IRegistrationConvention, который можно создать вручную любым необходимым способом.

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

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

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

Подсказка

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

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

Конфигурацию можно определить в XML и прочитать при помощи метода AddConfigurationFromXmlFile:

container.Configure(r =>
	r.AddConfigurationFromXmlFile(configName));

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

var configName =
	AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;

Примечание

Несмотря на то, что статический класс ObjectFactory напрямую поддерживает чтение конфигурации из App.config, данная возможность не поддерживается для экземпляров контейнера. Использование AppDomain API для получения имени файла – рекомендуемая технология работы.

Помимо направления StructureMap к соответствующему файлу XML конфигурацию можно передать в виде XmlNode:

container.Configure(r =>
	r.AddConfigurationFromNode(xmlNode));

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

Не важно, каков источник XML, схема остается той же. Ниже приведена простая конфигурация, которая преобразует IIngredient в Steak:

<StructureMap MementoStyle="Attribute">
	<DefaultInstance PluginType="Ploeh.Samples.MenuModel.IIngredient,
									➥Ploeh.Samples.MenuModel"
									PluggedType="Ploeh.Samples.MenuModel.Steak,
									➥Ploeh.Samples.MenuModel" />
</StructureMap>

Обратите внимание на то, что вы должны передавать квалифицированное имя типа сборки как для абстракции, так и для реализации – StructureMap называет их Плагинами (Plugins) или Подключаемыми типами (Plugged types).

Если вы хотите вставить данный код XML в конфигурационный файл приложения, то вы должны зарегистрировать элемент StructureMap в виде раздела конфигурации:

<configSections>
	<section name="StructureMap"
					type="StructureMap.Configuration.
					➥StructureMapConfigurationSection, StructureMap"/>
</configSections>

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

Подсказка

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

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

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

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

В рамках StructureMap мы можем упаковывать конфигурацию в регистры (Registries), которые представляют собой классы, унаследованные от конкретного класса Registry. Рисунок 11-4 демонстрирует взаимосвязь между классом Registry и методом Configure, который использовался в разделе 11.1.2 "Конфигурирование контейнера".

Рисунок 11-4: Метод Configure класса Container принимает в качестве входного параметра делегат, который действует для ConfigurationExpression – в данной главе мы обозначаем экземпляр этого ConfigurationExpression с помощью имени переменной r. Класс ConfigurationExpression – дочерний класс конкретного класса Registry.

Каждый раз при использовании метода Configure в этой главе вы представляете экземпляр ConfigurationExpression с помощью имени переменной r. Большинство методов, вызываемых для r (например, методы For и Scan), определены в классе Registry.

Для того чтобы реализовать Registry, мы реализуем класс, который наследуется от Registry. Приведенный ниже листинг демонстрирует пример, который конфигурирует используемый по умолчанию ICourse, а также добавляет типы IIngredient из сборки. В нем используется то же самое API, которое мы ранее использовали в разделе 11.1.2 "Конфигурирование контейнера", но в настоящее время это API упаковано в отдельный класс.

Листинг 11-2: Реализация регистра
public class MenuRegistry : Registry
{
	public MenuRegistry()
	{
		this.For<ICourse>().Use<Course>();
		this.Scan(s =>
		{
			s.AssemblyContainingType<Steak>();
			s.AddAllTypesOf<IIngredient>();
		});
	}
}

Registry или ConfigurationExpression?

Несмотря на то, что большинство конфигурационных API (например, методы For и Scan) все равно доступны при прямом наследовании от Registry, мы не можем использовать методы, определенные непосредственно для класса ConfigurationExpression. Какую функциональность мы теряем?

Существует только пять методов, которые определены непосредственно для ConfigurationExpression, и разделяются они на две категории:

  • Методы, которые считывают конфигурацию из XML
  • Методы, добавляющие регистры

Скорее всего, нам не понадобится добавлять Registry внутри Registry, поэтому данная возможность не столь важна.

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

И все-таки, можно ли унаследовать регистр от ConfigurationExpression вместо того, чтобы наследовать его напрямую от Registry? К несчастью, мы не можем это сделать, поскольку конструктор ConfigurationExpression – внутренний.

Основной момент – это то, что регистр не может быть унаследован от ConfigurationExpression, а должен наследоваться от самого Registry.

Класс MenuRegistry наследуется от Registry и определяет всю конфигурацию в конструкторе. Внутри класса вы можете получить доступ ко всему открытому API класса Registry, поэтому использовать методы For и Scan вы можете тем же самым способом, что и в разделе 11.1.2 "Конфигурирование контейнера". Единственное отличие заключается в том, что в данном случае вы реализуете не безымянный делегат, а конструктор. Вместо блока кода и вездесущей переменной r, к которым вы, возможно, на данный момент уже привыкли, вы обращаетесь к API посредством переменной this.

После получения MenuRegistry вы теперь можете добавить его в контейнер с помощью метода Configure:

container.Configure(r =>
	r.AddRegistry<MenuRegistry>());

Эта generic-версия метода AddRegistry требует, чтобы реализация Registry имела конструктор по умолчанию, но помимо этого доступна и не generic-перегрузка, которая принимает экземпляр Registry в качестве входного параметра, предоставляя нам полный контроль над тем, как он создается.

Примечание

Методы AddRegistry – два из пяти методов, определенных непосредственно для ConfigurationExpression и недоступных внутри Registry.

Вы также можете передать Registry непосредственно через конструктор контейнера:

var container = new Container(new MenuRegistry());

Я предпочитаю использовать метод Configure, поскольку он позволяет мне добавлять в последовательность более одного регистра.

Подсказка

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

Благодаря регистрам мы можем конфигурировать StructureMap, используя в качестве конфигурации код или автоматическую регистрацию, тогда как XML конфигурация должна импортироваться непосредственно через метод Configure. Кроме того, мы можем сочетать оба подхода, получая некоторую часть конфигурации из XML, а остальную – из одного или более чем одного регистра:

container.Configure(r =>
{
	r.AddConfigurationFromXmlFile(configName);
	r.AddRegistry<MenuRegistry>();
});

После того, как контейнер сконфигурирован, вы можете начать разрешать с помощью него сервисы, как это описано в разделе 11.1.1 "Разрешение объектов".

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