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

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

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

Марк Симан

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

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

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

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

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

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

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

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

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

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

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

container.RegisterType<IIngredient, Steak>();
container.RegisterType<IIngredient, SauceBéarnaise>("sauce");

В этом коде может присутствовать только одна неименованная регистрация. Она называется регистрацией по умолчанию. Если вы впоследствии вызовете для IIngredient метод RegisterType без имени, то регистрация Steak будет заменена новым компонентом.

Примечание

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

При вызове метода Resolve без имени мы получаем объект на основании регистрации по умолчанию. Благодаря предыдущей конфигурации метод Resolve возвращает экземпляр Steak:

var ingredient = container.Resolve<IIngredient>();

Об именованной регистрации sauce мы не забыли. Разрешать составные IIngredient можно следующим образом:

IEnumerable<IIngredient> ingredients =
	container.ResolveAll<IIngredient>();

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

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

Метод ResolveAll возвращает все именованные регистрации, но не регистрацию по умолчанию.

Если существуют сконфигурированные экземпляры плагина, которые не могут быть разрешены при вызове метода ResolveAll, Unity выдает исключение, поясняющее, что существуют зависимости, неудовлетворяющие условиям. Такое поведение совместимо с поведением метода Resolve, но отлично от того, как поступают Castle Windsor или MEF.

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

Листинг 14-8: Присваивание имен регистрациям
container.RegisterType<IIngredient, Steak>("meat");
container.RegisterType<IIngredient, SauceBéarnaise>("sauce");

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

Примечание

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

Благодаря именованным регистрациям, приведенным в листинге 14-8, вы можете разрешить и 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.

Листинг 14-9: Регистрация именованных course
container.RegisterType<ICourse, Rillettes>("entrée");
container.RegisterType<ICourse, CordonBleu>("mainCourse");
container.RegisterType<ICourse, MousseAuChocolat>("dessert");

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

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

Листинг 14-10: Переопределение механизма автоматической интеграции
container.RegisterType<IMeal, ThreeCourseMeal>(
	new InjectionConstructor(
			new ResolvedParameter<ICourse>("entrée"),
			new ResolvedParameter<ICourse>("mainCourse"),
			new ResolvedParameter<ICourse>("dessert")));

До этого момента мы еще не обсуждали подробно тот факт, что все перегрузки метода RegisterType принимают в качестве параметра массив объектов класса InjectionMember. InjectionMember – это стратегия, которую Unity использует в качестве инструкции при компоновке типов друг с другом. Например, InjectionConstructor позволяет определять параметры, используемые для паттерна Constructor Injection.

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

Примечание

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

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

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

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

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

public Meal(IEnumerable<ICourse> courses)

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

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

Unity довольно хорошо разбирается в массивах, но не в других видах последовательностей, например, IEnumerable<T> или IList<T>. Для эффективной работы с последовательностями мы должны определить или преобразовать их в массивы, чтобы Unity мог работать с ними соответствующим образом.

Если вы попытаетесь зарегистрировать Meal, не сообщая при этом контейнеру о том, как ему следует работать с зависимостью IEnumerable<ICourse>, то при попытке разрешить IMeal будет выдаваться исключение:

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

При разрешении IMeal выдается исключение, потому что Unity не знает, как нужно разрешать IEnumerable<ICourse>. Так произойдет, даже если вы перед этим зарегистрируете несколько компонентов ICourse, как вы и поступали в листинге 14-9.

Чтобы преобразовать все именованные регистрации ICourse в IEnumerable<ICourse>, можно воспользоваться преимуществами врожденного понимания контейнером Unity массивов. Самый простой способ сделать это – преобразовать два приведенных ниже типа:

container.RegisterType<IEnumerable<ICourse>, ICourse[]>();

Это может показаться слегка странным, но зато такой подход достаточно хорошо работает. Всякий раз, когда Unity сталкивается с зависимостью IEnumerable<ICourse>, он преобразует ее в запрос массива экземпляров ICourse, что даст нам такой же самый результат, как если бы мы вызвали container.ResolveAll<ICourse>().

Примечание

Unity разрешает массивы подобно тому, как он возвращает результат метода ResolveAll. Возвращаются все запрашиваемые именованные компоненты, но не компонент по умолчанию.

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

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

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

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

Рисунок 14-7: В ситуации, продемонстрированной слева, мы хотим явным образом отобрать определенные зависимости из большого списка всех зарегистрированных компонентов. Это отличается от ситуации, приведенной справа, когда мы отбираем все без разбора.

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

В листинге 14-10 вы использовали класс InjectionConstructor для того, чтобы определить вместо стратегии автоматической интеграции, используемой по умолчанию, другую стратегию. Когда дело касается класса Meal, вы можете сделать то же самое с единственной лишь разницей, что теперь вместо трех отдельных аргументов ICourse внедряется зависимость IEnumerable<ICourse>. Следующий листинг демонстрирует, как сконфигурировать явный массив с помощью InjectionConstructor.

Листинг 14-11: Внедрение именованных компонентов в последовательность
container.RegisterType<IMeal, Meal>(
	new InjectionConstructor(
			new ResolvedArrayParameter<ICourse>(
					new ResolvedParameter<ICourse>("entrée"),
					new ResolvedParameter<ICourse>("mainCourse"),
					new ResolvedParameter<ICourse>("dessert"))));

Чтобы переопределить механизм автоматической интеграции и явным образом задать стратегию внедрения зависимостей в конструктор Meal, вы еще раз обращаетесь к классу InjectionConstructor. Поскольку для конструктора Meal нужен IEnumerable<ICourse>, вы можете воспользоваться экземпляром ResolvedArrayParameter<ICourse> для определения массива, который будет вычисляться при разрешении контейнером класса Meal. Класс ResolvedArrayParameter<ICourse> определяет стратегию, при которой анализ массива экземпляров ICourse откладывается до тех пор, пока не разрешится сам Meal.

Чтобы определить значения, которые будут использоваться при разрешении Meal, можно использовать три именованных экземпляра ResolvedParameter<ICourse>, как вы уже делали в листинге 14-10. Единственное отличие – теперь они используются в качестве аргументов для конструктора ResolvedArrayParameter<ICourse> вместо того, чтобы использоваться напрямую в InjectionConstructor. При разрешении IMeal ResolvedArrayParameter<ICourse> разрешает три именованных регистрации entrée, mainCourse и dessert, а также создает массив ICourse из этих трех компонентов. Поскольку ICourse[] реализует IEnumerable<ICourse>, может подойти конструктор Meal.

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

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

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

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

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

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

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

public Breading(IIngredient ingredient)

Чтобы получить Cotoletta, вам необходимо будет вложить VealCutlet (еще один IIngredient) в класс Breading. Поскольку вы уже знаете, как соединить именованные компоненты с аргументами конструктора, было бы вполне естественным сделать что-то аналогичное в следующем листинге.

Листинг 14-12: Создание обертки с помощью именованного компонента
container.RegisterType<IIngredient, VealCutlet>("cutlet");
container.RegisterType<IIngredient, Breading>(
	new InjectionConstructor(
			new ResolvedParameter<IIngredient>("cutlet")));

Компонент Breading по умолчанию должен иметь тип IIngredient, поэтому вам нужно присвоить VealCutlet имя IIngredient, поскольку по умолчанию IIngredient должен быть только один. При регистрации компонента Breading вы еще раз используете InjectionConstructor, чтобы указать, как контейнер Unity должен интегрировать аргумент конструктора ingredient класса Breading. ResolvedParameter<IIngredient> позволяет вам указать, что должен разрешаться именно первый параметр конструктора (и только он) и интегрироваться с именованным компонентом cutlet.

При разрешении IIngredient вы получаете экземпляр Breading, в который вложен VealCutlet.

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

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

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

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

container.RegisterType<IIngredient, Breading>(
	new InjectionConstructor(
			new ResolvedParameter<VealCutlet>()));

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

Несмотря на то, что вы не регистрировали компонент VealCutlet, вы все еще можете это сделать, если вам необходимо сконфигурировать другие аспекты, например, его стиль существования:

container.RegisterType<VealCutlet>(
	new ContainerControlledLifetimeManager());
container.RegisterType<IIngredient, Breading>(
	new InjectionConstructor(
			new ResolvedParameter<VealCutlet>()));

В этом примере вы конфигурируете конкретный VealCutlet в виде Singleton, но, поскольку вы не планируете разрешать его в виде IIngredient, вы не преобразуете его в интерфейс. Все это превращает его в VealCutlet по умолчанию, а затем ResolvedParameter<VealCutlet> сможет должным образом его разрешить.

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

Создание перехватчиков

В разделе 9.3.3 "Пример: перехват с помощью Windsor" вы видели пример того, как добавить в WCF-приложение обработчик ошибок и Circuit Breaker с помощью возможности динамического перехвата, предлагаемой Castle Windsor. В этом разделе мы сделаем то же самое с помощью Unity.

Как показано на рисунке 14-8, добавление аспекта в Unity – процесс, включающий в себя несколько шагов.

Рисунок 14-8: Шаги, которые включает в себя процесс добавления аспекта в Unity

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

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

Реализация перехватчика обработчика исключений

Реализация перехватчика для контейнера Unity требует от нас реализации интерфейса IInterceptionBehavior. Листинг 14-13 демонстрирует, как реализовать стратегию обработки исключений, приведенную в главе 9. Эта конкретная реализация стратегии для контейнера Unity соответствует листингу 9-8, который приводился при описании Castle Windsor, и листингу 12-4, который приводился при описании Spring.NET.

Листинг 14-13: Реализация интерфейса IInterceptionBehavior, предназначенного для обработки исключений.
public class ErrorHandlingInterceptionBehavior :
	IInterceptionBehavior
{
	public IEnumerable<Type> GetRequiredInterfaces()
	{
		return Type.EmptyTypes;
	}
	public bool WillExecute
	{
		get { return true; }
	}
	public IMethodReturn Invoke(
		IMethodInvocation input,
		GetNextInterceptionBehaviorDelegate getNext)
	{
		var result = getNext()(input, getNext);
		if (result.Exception is CommunicationException
			|| result.Exception is
				InvalidOperationException)
		{
			this.AlertUser(result.Exception.Message);
			return input.CreateMethodReturn(null);
		}
		return result;
	}
	private void AlertUser(string message)
	{
		var sb = new StringBuilder();
		sb.AppendLine("An error occurred.");
		sb.AppendLine("Your work is likely lost.");
		sb.AppendLine("Please try again later.");
		sb.AppendLine();
		sb.AppendLine(message);
		MessageBox.Show(sb.ToString(), "Error",
			MessageBoxButton.OK,
			MessageBoxImage.Error);
	}
}

Строка 4-7: Передача интерфейсов

Строка 8-11: Подключение перехватчика

Строка 12-14: Реализация логики перехвата

Строка 16: Получение результата из вложенного объекта

Строка 17-22: Обработка исключений

Класс ErrorHandlingInterceptionBehavior реализует IInterceptionBehavior, который является интерфейсом с тремя членами. Двое из этих членов, в основном, относятся к инфраструктуре Unity, и реализовать их довольно легко. Метод GetRequiredInterfaces позволяет нам указать, к каким интерфейсам обращается этот перехватчик, но возвращая пустой массив, вы можете отложить принятие решения до того момента, когда вы будете конфигурировать, какие компоненты собираетесь перехватывать. Свойство WillExecute должно возвращать true, если вы хотите, чтобы перехватчик работал. Это дает нам возможность сконфигурировать, должен ли выполняться конкретный перехватчик. Но в этом примере вы хотите всегда выполнять ErrorHandlingInterceptionBehavior, если он сконфигурирован для компонента.

Основная реализация IInterceptionBehavior выполняется в методе Invoke, который вызывается (так!) контейнером Unity при вызове перехватываемого компонента. Параметр input содержит некоторую информацию о текущем вызове метода, тогда как параметр getNext содержит делегат, который можно использовать для вызова вложенного компонента. Это приблизительно соответствует методу Procced, который используется в Castle Windsor и продемонстрирован в листинге 9-8.

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

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

ErrorHandlingInterceptionBehavior обрабатывает определенные исключения вложенного компонента. В роли этого компонента может выступать перехватчик в форме Circuit Breaker.

Реализация Circuit Breaker перехватчика

Circuit Breaker перехватчик – слегка более сложный перехватчик, поскольку для него необходима зависимость ICircuitBreaker. Но как демонстрирует следующий листинг, мы решаем эту проблему путем применения стандартного паттерна Constructor Injection. Когда дело доходит до компоновки класса, контейнер Unity выполняет ее так же, как и любой другой компонент: пока он может разрешать зависимость, все идет нормально.

Листинг 14-14: Реализация IInterceptionBehavior в виде Circuit Breaker
public class CircuitBreakerInteceptionBehavior :
	IInterceptionBehavior
{
	private readonly ICircuitBreaker breaker;
	public CircuitBreakerInteceptionBehavior(
		ICircuitBreaker breaker)
	{
		if (breaker == null)
		{
			throw new ArgumentNullException("breaker");
		}
		this.breaker = breaker;
	}
	public IMethodReturn Invoke(IMethodInvocation input,
		GetNextInterceptionBehaviorDelegate getNext)
	{
		try
		{
			this.breaker.Guard();
		}
		catch (InvalidOperationException e)
		{
			return
				input.CreateExceptionMethodReturn(e);
		}
		var result = getNext()(input, getNext);
		if (result.Exception != null)
		{
			this.breaker.Trip(result.Exception);
		}
		else
		{
			this.breaker.Succeed();
		}
		return result;
	}
	public IEnumerable<Type> GetRequiredInterfaces()
	{
		return Type.EmptyTypes;
	}
	public bool WillExecute
	{
		get { return true; }
	}
}

Строка 17-25: Реализация граничного оператора

Строка 26: Возвращение result из вложенного метода

Строка 27-30: Обработка исключения

Строка 31-34: Обозначение успешного выполнения

Строка 37-44: Необходимые данные для IInterceptionBehavior

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

В методе Invoke вам необходимо реализовать идиоматическое выражение Guard-Succeed/Trip, которое вы уже видели в листингах 9-4 и 9-9. Для начала вам необходимо вызвать метод Guard, и вернуть исключение, если этот метод возвращает исключение. Unity предполагает, что вы будете передавать исключения не посредством вывода их, а путем инкапсуляции их в экземпляры IMethodReturn. Поэтому вы должны явным образом перехватывать InvalidOperationException и из перехваченного исключения создавать возвращаемое значение.

Если метод Guard уже выполнился, то вы можете приступить к вызову вложенного метода. Делается это точно так же, как вы делали это в листинге 14-13. После получения result из вложенного метода можно проанализировать это значение, чтобы определить, не было ли возвращено исключение. Если это так, то с помощью метода Trip вы устанавливаете прерыватель, но, обратите внимание на то, что при этом вы не изменяете result. Не забудьте о том, что после вызова метода Trip все равно необходимо выдавать исключение, поэтому result вы можете оставить неизмененным – он уже инкапсулирует исключение.

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

После реализации ErrorHandlingInterceptionBehavior и CircuitBreakerInteceptionBehavior настает время для того, чтобы сконфигурировать контейнер таким образом, чтобы ErrorHandlingInterceptionBehavior и CircuitBreakerInteceptionBehavior стали обертками для IProductManagementAgent.

Конфигурирование механизма перехвата

Все, что нам необходимо сделать, – перехватить компонент IProductManagementAgent с помощью Circuit Breaker и механизма обработки ошибок таким образом, чтобы при возникновении исключений во время взаимодействия с веб-сервисом открывался Circuit Breaker и обрабатывалось исключение. Это дает приложению возможность восстанавливаться при каждом последующем подключении веб-сервиса или сети.

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

Примечание

Чтобы иметь возможность использовать механизм перехвата в рамках контейнера Unity, необходимо добавить ссылку на сборку Microsoft.Practices.Unity.InterceptionExtension.

После добавления ссылки на сборку Microsoft.Practices.Unity.InterceptionExtension необходимо добавить в контейнер расширение Interception. Это всего лишь еще одно расширение контейнера, поэтому добавить его можно с помощью метода AddNewExtension:

container.AddNewExtension<Interception>();

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

Рисунок 14-9: IProductManagementAgent должен быть вложен в Circuit Breaker перехватчик таким образом, чтобы при выдаче агентом исключения на некоторое время открывался цикл. Circuit Breaker не управляет исключениями, а всего лишь регистрирует их. Поэтому за управление исключениями отвечает перехватчик обработки ошибок, который должен располагаться как можно дальше для того, чтобы иметь возможность управлять исключениями, получаемыми как от агента, так и от Circuit Breaker.

При конфигурировании механизма перехвата для компонента используется тот факт, что все перегрузки метода RegisterType принимают в качестве параметра массив объектов InjectionMember. До настоящего момента вы использовали только класс InjectionConstructor, но для конфигурирования механизма перехвата можно использовать совокупность и других классов:

container.RegisterType<IProductManagementAgent,
	➥WcfProductManagementAgent>(
	new Interceptor<InterfaceInterceptor>(),
	new InterceptionBehavior<ErrorHandlingInterceptionBehavior>(),
	new InterceptionBehavior<CircuitBreakerInteceptionBehavior>());

Разобъем этот код на составляющие части. Сначала вы добавляете Interceptor<InterfaceInterceptor>. Это InjectionMember, который сообщает Unity о том, что далее следуют один или несколько InterceptionBehavior, которые выполняют перехват на уровне инетрфейса (в противоположность, например, перехвату виртуальных членов).

Следующие два InjectionMember добавляют реализованные вами перехватчики ErrorHandlingInterceptionBehavior и CircuitBreakerInteceptionBehavior. Обратите внимание, что, поскольку вы сначала указываете ErrorHandlingInterceptionBehavior, он становится самым дальним перехватчиком и, в свою очередь, перехватывает CircuitBreakerInteceptionBehavior.

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

container.RegisterType<ICircuitBreaker, CircuitBreaker>(
	new ContainerControlledLifetimeManager(),
	new InjectionConstructor(TimeSpan.FromMinutes(1)));

Для обеспечения эффективности важно, чтобы в коде присутствовал только один экземпляр Circuit Breaker (хотя бы по одному на каждый внешний ресурс), поэтому мы регистрируем его как Singleton. Кроме того, для конструктора CircuitBreaker мы задаем одноминутную задержку, чтобы гарантировать, что приложению разрешается восстанавливать прерванное подключение один раз в минуту.

Этот раздел продемонстрировал, как можно применять механизм динамического перехвата в рамках контейнера Unity. Лично я считаю, что сложность использования механизма перехвата в рамках Unity сравнима с поддержкой этого механизма контейнерами Castle Windsor и Spring.NET. Несмотря на то, что это применение не является полностью тривиальным, пользу оно приносит колоссальную.

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

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