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

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

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

Марк Симан

11.3. Работа с составными компонентами

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

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

  • Для разных потребителей должны использоваться разные специфичные типы.
  • Зависимости являются последовательностями.
  • Используются Decorator'ы.

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

Давайте сначала рассмотрим то, как можно обеспечить более разветвленное управление, нежели то, которое предоставляет нам автоматическая интеграция (Auto-Wiring).

Выбор из составных кандидатов

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

Давайте для начала повторим, как StructureMap работает с составными регистрациями одной и той же абстракции.

Конфигурирование составных реализаций одного и того же плагина

Как вы уже видели в разделе 11.1.2 "Конфигурирование контейнера", вы можете конфигурировать составные плагины одного и того же сервиса:

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

Данный пример кода регистрирует как класс Steak, так и класс SauceBéarnaise вместе с плагином IIngredient. Выигрывает последняя реализация, поэтому, если вы разрешаете IIngredient посредством container.GetInstance<IIngredient>(), вы получите экземпляр Steak. Тем не менее, вызов container.GetAllInstances<IIngredient>() вернет IList<IIngredient>, который содержит как Steak, так и SauceBéarnaise. То есть, последующие конфигурации не позабыты, но их трудно получить.

Подсказка

Выигрывает последняя конфигурация данного типа. Она задает экземпляр по умолчанию для этого типа.

Если есть сконфигурированные экземпляры плагина, которые не могут быть разрешены при вызове GetAllInstances, StructureMap выдает исключение, объясняющее, что есть зависимости, которые не удовлетворяют заданным условиям. Это сообразно поведению метода GetInstance, но отличается от поведения Castle Windsor или MEF.

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

Листинг 11-8: Присваивание имен экземплярам
container.Configure(r =>
{
	r.For<IIngredient>()
		.Use<SauceBéarnaise>()
		.Named("sauce");
	r.For<IIngredient>()
		.Use<Steak>()
		.Named("meat");
});

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

При наличии именованных экземпляров из листинга 11-8, вы можете разрешать и Steak, и SauceBéarnaise следующим образом:

var meat = container.GetInstance<IIngredient>("meat");
var sauce = container.GetInstance<IIngredient>("sauce");

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

Предполагая, что вам всегда следует разрешать сервисы в единственном Composition Root, вы не должны ожидать, что на данном уровне столкнетесь с такой неопределенностью.

Подсказка

Если вы обнаружили, что вызываете метод GetInstance с конкретным идентификатором, подумайте, можете ли вы изменить свой подход так, чтобы он стал менее неопределенным.

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

Конфигурирование именованных зависимостей

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

public ThreeCourseMeal(ICourse entrée,
	ICourse mainCourse, ICourse dessert)

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

Листинг 11-9: Конфигурирование именованных course'ов
container.Configure(r => r
	.For<ICourse>()
	.Use<Rillettes>()
	.Named("entrée"));
container.Configure(r => r
	.For<ICourse>()
	.Use<CordonBleu>()
	.Named("mainCourse"));
container.Configure(r => r
	.For<ICourse>()
	.Use<MousseAuChocolat>()
	.Named("dessert"));

Как и в листинге 11-8 вы регистрируете три именованных компонента, преобразуя Rilettes в экземпляр под названием "entrée", CordonBleu – в экземпляр с именем "mainCourse", а MousseAuChocolat – в экземпляр под названием "dessert".

При такой конфигурации вы теперь можете регистрировать класс ThreeCourseMeal так, как показано в следующем листинге.

Листинг 11-10: Переопределение автоматической интеграции
container.Configure(r => r
	.For<IMeal>()
	.Use<ThreeCourseMeal>()
	.Ctor<ICourse>("entrée").Is(i =>
		i.TheInstanceNamed("entrée"))
	.Ctor<ICourse>("mainCourse").Is(i =>
		i.TheInstanceNamed("mainCourse"))
	.Ctor<ICourse>("dessert").Is(i =>
		i.TheInstanceNamed("dessert")));

Как обычно, выражение конфигурирования вы начинаете с преобразования интерфейса IMeal к конкретному ThreeCourseMeal. Но в дальнейшем вы расширяете выражение при помощи метода Ctor. Метод Ctor (сокращение от constructor) повзоляет вам выражать то, как должен преобразовываться параметр конструктора данного типа. В случае, когда для данного типа существует только один параметр, можно использовать перегрузку, в которой вам не приходится передавать имя параметра. Тем не менее, поскольку ThreeCourseMeal имеет три параметра ICourse, вам необходимо идентифицировать параметр по его имени, "entrée".

Метод Ctor возвращает объект, который позволяет вам задать то, как будет заполняться параметр конструктора. Метод Is позволяет вам использовать IInstanceExpression<ICourse> для отбора именованного экземпляра, что является еще одним примером паттерна Nested Closure. Эти же выражения вы в дальнейшем можете повторить и для следующих двух параметров.

Примечание

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

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

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

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

Интеграция ссылок на экземпляры

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

Листинг 11-11: Использование ссылок на экземпляры для переопределения автоматической интеграции
container.Configure(r =>
{
	var entrée =
		r.For<ICourse>().Use<Rillettes>();
	var mainCourse =
		r.For<ICourse>().Use<CordonBleu>();
	var dessert =
		r.For<ICourse>().Use<MousseAuChocolat>();
	r.For<IMeal>()
		.Use<ThreeCourseMeal>()
		.Ctor<ICourse>("entrée").Is(entrée)
		.Ctor<ICourse>("mainCourse").Is(mainCourse)
		.Ctor<ICourse>("dessert").Is(dessert);
});

Строка 3,5,7: Ссылки на экземпляры

Строка 11-13: Использование ссылок на экземпляры

До настоящего момента мы игнорировали тот факт, что типичная цепочка методов For/Use возвращает какой-то результат, поскольку для нас это было бесполезно. Но возвращаемые значения являются экземплярами SmartInstance<T>, которые можно использовать в качестве ссылок на конфигурации, которые вы делали до этого. Вместо имен экземпляров, которые вам пришлось использовать в листинге 11-10, вы можете использовать эти ссылки прямо с одной из множества перегрузок метода Is, сравнивая каждую локальную переменную с соответствующим именованным параметром конструктора.

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

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

Интеграция последовательностей

В разделе 10.3.2 "Разработка пользовательского стиля существования" мы обсуждали, как выполнить рефакторинг явного класса ThreeCourseMeal к более универсальному классу Meal, который обладает приведенным ниже конструктором:

public Meal(IEnumerable<ICourse> courses)

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

Автоматическое интегрирование последовательностей

StructureMap довольно хорошо разбирается в последовательностях. Если мы хотим использовать все сконфигурированные экземпляры данного плагина, то в этом случае нам нужна именно автоматическая интеграция. К примеру, при наличии сконфигурированных экземпляров ICourse из листинга 11-9 можно сконфигурировать плагин IMeal следующим образом:

container.Configure(r => r.For<IMeal>().Use<Meal>());

Обратите внимание на то, что в данном примере приведено совершенно стандартное преобразование абстракции к конкретному типу. StructureMap будет автоматически понимать конструктор Meal и определять, что подходящее направление действий – разрешение всех экземпляров ICourse. При разрешении IMeal вы получаете экземпляр Meal наряду с экземплярами ICourse, описанными в листинге 11-9: Rillettes, CordonBleu и MousseAuChocolat.

Примечание

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

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

Отбор нескольких экземпляров из большого набора

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

Рисунок 11-6: В ситуации, приведенной слева, мы хотим явно выбрать только конкретные зависимости из большого перечня всех сконфигурированных экземпляров. Данная ситуация отличается от той, которая приведена справа – здесь мы без разбора отбираем все экземпляры.

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

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

Листинг 11-12: Использование ссылок на экземпляры для внедрения последовательностей
container.Configure(r =>
{
	var entrée = r.For<ICourse>().Use<Rillettes>();
	var entrée1 = r.For<ICourse>().Use<LobsterBisque>();
	var mainCourse = r.For<ICourse>().Use<CordonBleu>();
	var dessert = r.For<ICourse>().Use<MousseAuChocolat>();
	r.For<IMeal>().Use<Meal>()
		.EnumerableOf<ICourse>()
		.Contains(entrée, mainCourse, dessert);
});

Аналогично коду из листинга 11-11 вы присваиваете переменную каждому экземпляру, который возвращается методом Use. Обратите внимание на то, что вы конфигурируете четыре экземпляра ICourse, даже если вы используете только три из них для экземпляра IMeal. Тем не менее, вам может понадобиться преобразовать ICourse к LobsterBisque для некоторых других целей, непродемонстрированных здесь. Поскольку вы не используете результирующую переменную entrée1, вы могли бы ее полностью опустить, но я решил ее включить для того, чтобы код был последовательным.

Поскольку конструктор Meal принимает в качестве входного параметра IEnumerable<ICourse>, вы можете использовать метод EnumerableOf для обозначения последовательности экземпляров ICourse, явно определенных в методе Contains, где вы передаете три ссылки на экземпляры, которые собираетесь использовать.

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

Листинг 11-13: Внедрение именованных экземпляров в последовательности
container.Configure(r => r
	.For<IMeal>()
	.Use<Meal>()
	.EnumerableOf<ICourse>().Contains(i =>
	{
		i.TheInstanceNamed("entrée");
		i.TheInstanceNamed("mainCourse");
		i.TheInstanceNamed("dessert");
	}));

При наличии набора именованных экземпляров, аналогичных тем, которые были созданы в листинге 11-9, вы можете ссылаться на каждый именованный экземпляр при конфигурировании экземпляра IMeal. Как и в листинге 11-12, вы используете цепочку методов EnumerableOf/Contains для обозначения последовательности зависимостей. На этот момент у вас нет переменных Instance, поэтому вы должны искать их по имени. Перегрузка метода Contains дает вам возможность использовать Nested Closure, который объявляет то, какие именованные экземпляры вы хотите внедрить в экземпляр Meal.

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

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

Интеграция Decorator'ов

В разделе 9.1.2 "Паттерны и принципы механизма перехвата" мы обсуждали то, насколько паттерн проектирования Decorator полезен при реализации сквозных сущностей. По определению Decorator'ы представляют собой составные типы одной и той же абстракции. У нас есть, по крайней мере, две реализации абстракции: сам Decorator и обернутый тип. Если бы мы помещали Decorator'ы в стек, то у нас было бы еще больше реализаций.

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

Создание обертки при помощи ссылок на экземпляры

Давайте посмотрим, как можно сконфигурировать класс Breading, который является Decorator'ом IIngredient. Этот класс использует Constructor Injection для получения экземпляра, который необходимо обернуть:

public Breading(IIngredient ingredient)

Для того чтобы сделать панированную телячью котлету, вам хотелось бы обернуть VealCutlet (еще один IIngredient) в класс Breading. Один из способов это сделать – использовать ссылки на экземпляры в рамках единичного метода Configure:

container.Configure(r =>
{
	var cutlet = r.For<IIngredient>().Use<VealCutlet>();
	r.For<IIngredient>().Use<Breading>()
		.Ctor<IIngredient>().Is(cutlet);
});

Как вы уже видели в листингах 11-11 и 11-12, можно использовать возвращаемое методом Use значение для того, чтобы перехватить ссылку на экземпляры. Переменная cutlet представляет собой сконфигурированное преобразование IIngredient в VealCutlet. И вы можете использовать этот факт для того, чтобы объявить, что эта переменная и есть Instance, который можно использовать в параметре IIngredient конструктора класса Breading. Поскольку выигрывает последняя конфигурация, Breading Instance на данный момент является используемым по умолчанию Instance.

Когда вы попросите контейнер разрешить IIngredient, он вернет объект, основанный на используемом по умолчанию Instance. Это и есть Breading Instance, в котором вы предоставили дополнительный намек на то, что он должен разрешить cutlet Instance для параметра IIngredient класса Breading. В результате мы получаем экземпляр Breading, содержащий экземпляр Cutlet.

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

Создание обертки при помощи именованных экземпляров

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

Давайте предположим, что вы уже сконфигурировали VealCutlet следующим образом:

container.Configure(r => r
	.For<IIngredient>()
	.Use<VealCutlet>()
	.Named("cutlet"));

Поскольку вы знаете, что имя экземпляра – cutlet, вы можете использовать его для конфигурирования класса Breading:

container.Configure(r => r
	.For<IIngredient>()
	.Use<Breading>()
	.Ctor<IIngredient>()
	.Is(i => i.TheInstanceNamed("cutlet")));

Как и в листингах 11-10 и 11-13 вы используете перегрузку метода Is, которая дает вам возможность предоставить блок кода, идентифицирующий именованный экземпляр. И снова вы видите паттерн Nested Closure в действии.

Если вы сравните два предыдущих примера, то не заметите, что они похожи. В обоих случаях вы использовали метод Ctor<T>, олицетворяющий параметр конструктора. Единственное отличие заключается в том, как вы идентифицируете параметр с методом Is.

Цепочка методов Ctor/Is имеет преимущество, заключающееся в том, что мы можем использовать ее для указания единственного параметра конструктора, даже если рассматриваемый конструктор имеет более одного параметра. Все параметры, которые мы не сможем сконфигурировать при помощи метода Ctor, будут автоматически интегрированы на основании алгоритмов, используемых StructureMap по умолчанию. Это полезно, если мы хотим сконфигурировать только один из нескольких параметров.

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

Другой вариант предлагает более строго типизированный подход.

Создание обертки при помощи делегатов

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

Это выглядит слегка абстрактным, поэтому давайте рассмотрим пример, демонстрирующий, как сконфигурировать Cotoletta следующим образом:

container.Configure(r => r
	.For<IIngredient>().Use<VealCutlet>()
	.EnrichWith(i => new Breading(i)));

Метод EnrichWith – член generic-класса SmartInstance<T>, который возвращается методом Use. В данном случае вы вызываете метод Use с аргументом типа VealCutlet. Этот метод возвращает экземпляр SmartInstance<VealCutlet>. Метод EnrichWith принимает в качестве параметра делегат, который, в свою очередь, принимает в качестве входного параметра VealCutlet и возвращает объект.

Вы можете сравнить этот делегат с блоком кода, который принимает в качестве входного параметра VealCutlet. Компилятор делает вывод, что переменная i – это экземпляр VealCutlet, поэтому вы теперь можете реализовать блок кода посредством вызова конструктора Breading с переменной VealCutlet.

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

Достоинство данного подхода заключается в том, что в блоке кода вы записываете код, в котором используется конструктор Breading. Этот код является такой же строкой кода, как и любая другая строка кода, поэтому она проверяется компилятором. Это обеспечивает вас уверенностью в том, что если метод Configure компилируется, то VealCutlet будет корректно обернута.

Несмотря на то, что строгая типизированность безопаснее, ее сложнее поддерживать. Если вы впоследствии решите добавить еще один параметр в конструктор Breading, блок кода больше не будет компилироваться, и вы должны будете вручную справиться с данной проблемой. Это было бы не нужно, если бы вы использовали метод Ctor<T>, поскольку StructureMap смог бы отсортировать новый параметр благодаря автоматической интеграции.

Как вы уже видели, существует несколько способов конфигурирования Decorator'ов. Строго типизированный подход более безопасен, но для него может потребоваться более сложное сопровождение. Более слабо типизированное API – более гибкое, и дает StructureMap возможность справиться с изменениями нашего API, но ценой менее слабой типовой безопасности.

Примечание

В данном разделе мы не обсуждали механизм перехвата во время выполнения. Несмотря на то, что StructureMap имеет Seam'ы, которые разрешают механизм перехвата, он не обладает встроенной поддержкой динамически создаваемых прокси. Можно использовать эти Seam'ы для того, чтобы использовать другую библиотеку (например, Castle Dynamic Proxy) для создания таких классов. Но поскольку они не являются частью StructureMap, их обсуждение выходит за рамки данной главы.

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

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