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

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

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

Марк Симан

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

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

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

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

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

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

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

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

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

Регистрация составных реализаций одного и того же сервиса

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

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

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

Подсказка

Побеждает первая регистрация данного типа. Она определяет регистрацию, используемую для него по умолчанию.

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

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

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

Листинг 10-5: Присваивание имен компонентам
container.Register(Component
	.For<IIngredient>()
	.ImplementedBy<Steak>()
	.Named("meat"));
container.Register(Component
	.For<IIngredient>()
	.ImplementedBy<SauceBéarnaise>()
	.Named("sauce"));

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

Имея представленные в листинге 10-5 именованные компоненты, вы можете разрешить и Steak, и SauceBéarnaise следующим образом:

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

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

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

Подсказка

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

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

Регистрация именованных зависимостей

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

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

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

Листинг 10-6: Регистрация именованных course'ов
container.Register(Component
	.For<ICourse>()
	.ImplementedBy<Rillettes>()
	.Named("entrée"));
container.Register(Component
	.For<ICourse>()
	.ImplementedBy<CordonBleu>()
	.Named("mainCourse"));
container.Register(Component
	.For<ICourse>()
	.ImplementedBy<MousseAuChocolat>()
	.Named("dessert"));

Согласно листингу 10-6 вы регистрируете три именованных компонента, преобразуя Rilettes в регистрацию с именем "entrée" (напоминаю американским читателям, что это то же самое, что и starter или appetizer – то, что возбуждает аппетит, придает вкус), CordonBleu – в регистрацию с именем "mainCourse", а MousseAuChocolat – в регистрацию с именем "dessert".

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

Листинг 10-7: Переопределение автоматической интеграции
container.Register(Component
	.For<IMeal>()
	.ImplementedBy<ThreeCourseMeal>()
	.ServiceOverrides(new
		{
			entrée = "entrée",
			mainCourse = "mainCourse",
			dessert = "dessert"
		}));

Вы можете явным образом обеспечить переопределения для тех параметров (или свойств), к которым хотите явно обращаться. В случае класса ThreeCourseMeal вам нужно обращаться ко всем трем параметрам конструктора. Тем не менее, в других случаях вы можете захотеть переопределить только один из нескольких параметров; это тоже возможно. Метод ServiceOverrides позволяет вам применять анонимный типизированный объект, который указывает, какие параметры необходимо переопределить. Если вы не хотите использовать безымянные типы, то другие перегрузки методов ServiceOverrides дают нам возможность использовать массив специализированных экземпляров ServiceOverride или IDictionary.

Используя анонимный тип, вы сравниваете параметры, которые хотите переопределить, с именованной регистрацией. В первом случае вы сравниваете имя параметра entrée с регистрацией, имеющей имя "entrée". В данном случае имена параметров идентичны именам регистраций, но это не обязательно. Остальные параметры преобразуются похожим способом.

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

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

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

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

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

В разделе 6.4.1 мы обсуждали, как Constructor Injection выступает в роли системы оповещения при нарушениях принципа единственной ответственности. Урок, извлеченный из того раздела, – вместо того, чтобы рассматривать constructor over-injection как слабую сторону паттерна Constructor Injection, мы должны скорее порадоваться тому, что constructor over-injection делает сомнительную композицию столь очевидной.

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

Подсказка

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

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

Рефакторинг с целью получения лучшего course

В разделе 10.3.1 "Выбор из составных кандидатов" вы увидели, как ThreeCourseMeal и присущая ему неопределенность вынудили нас отказаться от автоматической интеграции и вместо нее использовать явный ServiceOverride. Это должно было дать вам намек на пересмотр конструкции API.

Простое обобщение приводит нас к реализации IMeal, которая принимает в качестве параметров произвольное количество экземпляров ICourse вместо явных трех, как было в случае с классом ThreeCourseMeal:

public Meal(IEnumerable<ICourse> courses)

Обратите внимание на то, что вместо требования о наличии в конструкторе трех отдельных экземпляров ICourse, единичная зависимость экземпляра IEnumerable<ICourse> позволяет вам обеспечить любое количество блюд в классе Meal – от 0 до … бесконечности! Это решает проблему неопределенности, поскольку теперь существует только одна зависимость. Кроме того, это улучшает API и реализацию, обеспечивая единственный универсальный класс, который может моделировать множество различных видов обедов, от простого обеда с единственным блюдом до сложного, состоящего из 12 блюд.

Имея регистрацию course'ов, продемонстрированную в листинге 10-6, вы сможете автоматически разрешать IMeal, если зарегистрируете его следующим образом:

container.Register(Component
	.For<IMeal>()
	.ImplementedBy<Meal>());

Тем не менее, когда вы пытаетесь разрешить IMeal, контейнер выдает исключение. Несмотря на то, что исключение совсем ни о чем нам не говорит, причина в том, что вы не сообщили контейнеру, как он должен разрешать IEnumerable<ICourse>. Давайте сделаем обзор некоторых различных доступных вариантов.

Конфигурирование массивов

Castle Windsor отлично понимает массивы. Поскольку массивы реализуют IEnumerable<T>, вы можете явным образом сконфигурировать массив для параметра конструктора courses. Это можно сделать способом, похожим на синтаксис, который вы видели в листинге 10-7. В следующем листинге вы увидите те же самые courses, определенные в виде сервисов.

Листинг 10-8: Явное определение массива сервисов
container.Register(Component
	.For<IMeal>()
	.ImplementedBy<Meal>()
	.ServiceOverrides(new
		{
			courses = new[]
				{
					"entrée",
					"mainCourse",
					"dessert"
				}
		}));

Строка 6-10: Переопределение параметра courses

Аналогично листингу 10-7 вы используете метод ServiceOverrides, когда хотите переопределить автоматическую интеграцию для конкретных параметров. В данном примере вы хотите явным образом сконфигурировать параметр конструктора courses для класса Meal. Поскольку этот параметр является IEnumerable<ICourse>, вы должны теперь задать последовательность сервисов ICourse.

Поскольку массивы реализуют IEnumerable<T>, вы можете определить массив именованных сервисов. Осуществляете вы это путем создания массива имен сервисов. Эти имена идентичны именам, присвоенным каждой регистрации в листинге 10-6, а Castle Windsor настолько добр, что преобразует этот массив имен сервисов в массив экземпляров ICourse в рабочей среде. Все это аналогично листингу 10-7 за одним лишь исключением – fluent registration API по своей природе понимает и преобразует массивы имен сервисов в массивы сервисов.

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

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

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

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

Разрешение последовательностей

Castle Windsor по умолчанию не разрешает массивы или IEnumerable<T>. Это может показаться довольно неожиданным, поскольку в результате вызова ResolveAll возвращается массив:

IIngredient[] ingredients = container.ResolveAll<IIngredient>();

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

container.Kernel.Resolver.AddSubResolver(
	new CollectionResolver(container.Kernel));

CollectionResolver даст контейнеру возможность разрешать такие последовательности зависимостей, как IEnumerable<T>. Благодаря этому теперь вы можете разрешать класс Meal, не используя явный ServiceOverrides. Имея данную регистрацию

container.Register(Component
	.For<IMeal>()
	.ImplementedBy<Meal>());

вы можете разрешать IMeal с помощью CollectionResolver:

var meal = container.Resolve<IMeal>();

Будет создан экземпляр Meal со всеми сервисами ICourse контейнера.

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

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

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

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

Явное подключение Decorator'ов

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

Листинг 10-9: Явная конфигурация Decorator'а
container.Register(Component
	.For<IIngredient>()
	.ImplementedBy<Breading>()
	.ServiceOverrides(new
		{
			ingredient = "cutlet"
		}));
container.Register(Component
	.For<IIngredient>()
	.ImplementedBy<VealCutlet>()
	.Named("cutlet"));

Как мы уже обсуждали во введении к главе 9, панировку можно рассматривать в качестве обертки для говяжьей отбивной – регистрация Cotoletta. При разрешении Cotoletta вам нужна ссылка на breading, которая должна содержать в себе veal cutlet. Сначала вы регистрируете Breading. Вспомните, что в Castle Windsor всегда выигрывает первая регистрация. Вы явным образом используете метод ServiceOverrides для того, чтобы сконфигурировать, какой именованный сервис должен использоваться для параметра конструктора ingredient. Обратите внимание на то, что вы ссылаетесь на компонент под названием cutlet, несмотря на то, что данный компонент на данном этапе еще не был зарегистрирован. Такое возможно, поскольку порядок регистрации мало что значит. Можно регистрировать компоненты до того, как вы зарегистрируете их зависимости, и при этом все будет работать, поскольку при попытке разрешения сервисов все регистрируется должным образом.

Это означает, что перед разрешением IIngredient вы все равно должны зарегистрировать veal cutlet. Впоследствии вы регистрируете ее под именем cutlet. Это имя совпадает с именем сервиса, переданного в параметр конструктора ingredient в предыдущей конфигурации.

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

Неявное подключение Decorator'ов

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

По определению Decorator имеет зависимость от другого экземпляра одного и того же типа. Если мы явным образом не определим, какую регистрацию использовать, мы можем ожидать появления циклических ссылок. Тем не менее, Castle Windsor умнее всего этого. Он выбирает следующую регистрацию соответствующего типа. Это означает, что вместо листинга 10-9 вы можете записать

container.Register(Component
	.For<IIngredient>()
	.ImplementedBy<Breading>());
container.Register(Component
	.For<IIngredient>()
	.ImplementedBy<VealCutlet>());

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

Примечание

Следующий логический шаг вперед от Decorator'а – механизм перехвата. Castle Windsor обладает великолепными возможностями для осуществления перехвата. В разделе 9.3.3 "Пример: перехват с помощью Windsor" уже приводился исчерпывающий пример. Поэтому вместо того, чтобы заново повторять его в данном разделе я сошлюсь на раздел 9.3.3.

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

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