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

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

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

Марк Симан

9.3. Объявление аспектов

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

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

Примечание

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

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

Использование атрибутов для объявления аспектов

Атрибуты имеют общую с Decorator'ами черту: несмотря на то, что они могут добавлять или подразумевать изменение поведения члена, сигнатуру они оставляют неизменной. Как вы видели в разделе 9.2.3 "Добавление функциональности обеспечения безопасности", вы можете заменить явный, императивный код авторизации на атрибут. Вместо того чтобы написать одну или более одной строки явного кода, вы могли бы достичь того же результата, применяя атрибут [PrincipalPermission].

Довольно заманчиво было бы экстраполировать эту концепцию в другие сквозные сушности. Было бы это здорово, если бы вы могли отметить метод или класс атрибутом [HandleError] или даже пользовательским атрибутом [CircuitBreaker], и, таким образом, применить аспект при помощи единственной строки декларативного кода?

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

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

Но подождите! Разве атрибут [PrincipalPermission] не изменил поведение метода? Да, это так, но этот атрибут (и некоторые другие атрибуты, доступные в стандартной библиотеке классов) – особенный. .NET Framework понимает этот атрибут и влияет на него, но .NET Framework не будет так делать для всякого пользовательского атрибута, который вам захотелось бы ввести.

Если вы хотите разрешить пользовательским атрибутам изменять поведение приложения, то у вас есть два варианта:

  • Изменить шаг компиляции
  • Ввести пользовательский хост рабочей среды

Давайте вкратце исследуем каждый вариант.

Модификация компиляции

Работа одного из самых популярных фреймворков аспектно-ориентированного программирования, PostSharp, заключается именно в разрешении вам добавлять пользовательские атрибуты в код. Эти атрибуты должны наследоваться от специального атрибута, который определен в PostSharp SDK, предоставляющем виртуальные методы, которые вы можете переопределить для того, чтобы задать желаемое вами поведение аспекта. Затем вы можете применить эти атрибуты к своим классам или членам класса. Рисунок 9-8 демонстрирует, что случится потом.

Рисунок 9-8: Работа PostSharp заключается в добавлении шага посткомпиляции после того, как будет завершена обычная компиляция. Поскольку пользовательские PostSharp атрибуты в вашем коде трактуются обычным компилятором (например, csc.exe) так же, как и любые другие атрибуты, выходной результат – это обычная сборка с пассивными атрибутами. В PostSharp входит шаг посткомпиляции, на котором PostSharp собирает скомпилированную сборку и чередует код ваших пользовательских атрибутов напрямую с атрибутивным кодом. Результатом является новая .NET сборка с вложенными аспектами.

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

Эта сборка – абсолютно обычная сборка, которая запускается всюду, где запускается весь остальной .NET код. Для этого не требуется никакой специальной рабочей среды.

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

Один из недостатков данного подхода – тот факт, что запускаемый код отличается от написанного вами кода. Если вы захотите отладить код, то вам нужно будет выполнить особые шаги, и, несмотря на то, что поставщик с радостью предоставляет средства, позволяющие вам выполнить только отладку, он к тому же направляет вас к антипаттерну Vendor Lock-In.

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

Использование пользовательского хоста

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

Мы знаем данный прием из многочисленных .NET технологий, которые полагаются на атрибуты. Примерами этого приема являются:

  • В WCF входит множество атрибутов, например, [ServiceContract], [OperationContract] и т.д. Эти атрибуты приобретают поведение только, когда вы размещаете сервис в экземпляре ServiceHost (то же самое для вас делает и IIS).
  • ASP.NET MVC дает вам возможность указать, какой HTTP verbs вы допустите с атрибутом [AcceptVerbs], а также дает вам возможность обрабатывать исключения с помощью атрибута [HandleError], и еще несколько других возможностей. Это возможно, поскольку ASP.NET MVC – это один большой пользовательский хост, и он управляет жизненными циклами своих контроллеров.
  • Все .NET фреймворки модульного тестирования, о которых я осведомлен, используют атрибуты для идентификации тестовых сценариев. Фреймворк модульного тестирования инициализирует тестовые сценарии и интерпретирует атрибуты, чтобы указать, какой из тестов необходимо выполнить.

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

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

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

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

Недостатки атрибутов аспектов

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

Во-первых, атрибуты компилируются совместно с кодом, который они расширяют. Это означает, что вы не сможете так просто изменить ход своих мыслей. Рассмотрим в качестве примера процесс обработки ошибок. В разделе 9.2.2 "Обработка исключений" вы видели, как можно использовать паттерн проектирования Decorator для того, чтобы реализовать обработку ошибок для любого IProductManagementAgent. Интересно то, что упомянутый выше WcfProductManagementAgent ничего не знает об ErrorHandlingProductManagementAgent. Как проиллюстрировано на рисунке 9-9, они реализованы даже в разных библиотеках.

Рисунок 9-9: Как ErrorHandlingProductManagementAgent, так и WcfProductManagementAgent реализуют IProductManagementAgent, но определены они в двух различных библиотеках. Поскольку сборка ProductManagementClient содержит Composition Root, она зависит от ProductWcfAgent и PresentationLogic.

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

Для того чтобы сохранять свою открытость и гибкость, библиотека ProductWcfAgent не должна включать в себя обработку ошибок. Но, если вы применяете атрибут аспекта к WcfProductManagementAgent (или, что еще хуже, к IProductManagementAgent), то вы сильно привязываете этот аспект к реализации (или даже к абстракции). Если вы так поступаете, то вы навязываете WcfProductManagementAgent определенную стратегию обработки ошибок, и теряете способность варьировать аспекты независимо от реализации.

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

  • Параметры, включая возвращаемые значения
  • Члены, например, методы, свойства и поля
  • Типы, например, классы и интерфейсы
  • Библиотеки

Несмотря на то, что атрибуты аспектов предоставляют вам широкий круг возможностей, вы не можете с легкостью выразить большее количество конфигураций, основанных на соглашениях, например, "Я хочу применить аспект Circuit Breaker ко всем типам, имена которых начинаются с Wcf". Вместо этого вам пришлось бы применить гипотетический атрибут [CircuitBreaker] ко всем подходящим классам, нарушающим принцип DRY (Don't Repeat Yourself – Не повторяйся).

Последний недостаток атрибутов аспектов – атрибуты должны обладать простым конструктором. Если вам нужно использовать зависимости от аспекта, вы можете сделать это только с помощью Ambient Context. Вы уже видели пример этого в разделе 9.2.3 "Добавление функциональности обеспечения безопасности", где Thread.CurrentPrincipal является Ambient Context. Но данный паттерн в редких случаях является наиболее подходящим, и делает процесс управления жизненным циклом более трудным. К примеру, совместное использование ICircuitBreaker в рамках многочисленных WCF клиентов неожиданно становится более сложным.

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

Применение динамического перехвата

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

Повторяемость Decorator'ов

Примеры из разделов 9.1.1 "Пример: реализация аудита" и 9.2.1 "Осуществление перехвата с помощью Circuit Breaker" демонстрируют только репрезентативные методы, поскольку каждый метод реализуется одинаково, а мне не хотелось загружать несколько страниц практически идентичным кодом, так как это отвлекало бы нас от того, что мы рассматриваем. Следующий листинг демонстрирует, как похожи методы CircuitBreakerProductmanagementAgent. Этот листинг демонстрирует только два метода интерфейса IProductManagementAgent, но я уверен, что вы сможете экстраполировать их и представить, как выглядит остальная реализация.

Листинг 9-7: Нарушение DRY принципа
public void DeleteProduct(int productId)
{
	this.breaker.Guard();
	try
	{
		this.innerAgent.DeleteProduct(productId);
		this.breaker.Succeed();
	}
	catch (Exception e)
	{
		this.breaker.Trip(e);
		throw;
	}
}
public void InsertProduct(ProductEditorViewModel product)
{
	this.breaker.Guard();
	try
	{
		this.innerAgent.InsertProduct(product);
		this.breaker.Succeed();
	}
	catch (Exception e)
	{
		this.breaker.Trip(e);
		throw;
	}
}

Строка 6, 20: Единственное отличие

Так как вы уже видели метод InsertProduct в листинге 9-4, цель данного примера кода – проиллюстрировать повторяющуюся сущность Decorator'ов, используемых в виде аспектов. Единственное отличие между методами DeleteProduct и InsertProduct – это то, что каждый из них вызывает свой собственный соответствующий метод расширенного агента.

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

В качестве одного из способов решения данной проблемы вы могли бы рассматривать применение таких генераторов кода, как Text Template Transformation Toolkit (T4) от Visual Studio, но многие DI-контейнеры предлагают лучший вариант решения этой проблемы – посредством динамического перехвата.

Автоматизация Decorator'ов

Код в каждом методе листинга 9-7 очень похож на шаблон. Самая сложная составляющая процесса реализации Decorator'а в виде аспекта – проектирование этого шаблона, но после того, как шаблон спроектирован, остальное – это уже просто механический процесс:

  • Создать новый класс
  • Унаследовать от нужного интерфейса
  • Реализовать каждый член интерфейса путем применения шаблона

Данный процесс является настолько механическим, что вы можете использовать какой-либо инструмент для его автоматизации. Такой инструмент использовал бы рефлексию и схожие API для того, чтобы найти все члены, которые нужно реализовать, а затем применял бы шаблон ко всем этим членам. Рисунок 9-10 демонстрирует, как эту процедуру можно применить с помощью T4 шаблона.

Рисунок 9-10: T4 делает возможной автоматическую генерацию кода Decorator'а из шаблонов. Стартовая точка – прототип шаблона, который понимает основную концепцию Decorator'а. Прототип шаблона содержит код генерации кода, который будет генерировать границы расширяемого класса, но не определяет никакого кода аспекта. Из прототипа шаблона разрабатывается шаблон аспекта, который описывает, как должен применяться конкретный аспект (например, Circuit Breaker) при расширении любого интерфейса. Результатом является специализированный шаблон (SomeAspect.tt) этого конкретного аспекта, который можно использовать для генерации Decorator'ов конкретных интерфейсов. Результат – обычный файл кода (SomeAspectDecorator.cs), который обычно компилируется вместе с другими файлами кода.

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

Даже если вы не купитесь на этот аргумент, у вас все еще есть статический набор авто-генерируемых Decorator'ов. Если вам понадобится новый Decorator для данной комбинации аспекта и абстракции, то вы должны явным образом добавить данный класс. Он может генерироваться автоматически, но вам все равно нужно не забыть создать его и присоединить. Более условный подход в данном случае не возможен.

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

Динамический перехват

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

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

Примечание

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

Рисунок 9-11: Некоторые DI-контейнеры позволяют нам определять аспекты в виде перехватчиков. Перехватчик – фрагмент кода, который реализует аспект и взаимодействует с контейнером. Регистрация перехватчика с помощью контейнера позволяет контейнеру динамически создавать и порождать Decorator'ы, которые содержат поведение аспекта. Эти классы существуют только во время выполнения.

Для применения динамического перехвата вам все равно нужно написать код, который реализует аспект. Это мог бы быть код инфраструктуры, необходимый для аспекта Circuit Breaker, что продемонстрировано в листинге 9-7. Как только вы написали этот код, вы должны сообщить DI-контейнеру об этом аспекте и о том, когда ему следует его применять.

Во время выполнения DI-контейнер будет динамически порождать новые классы в выполняемом AppDomain, основанном на зарегистрированных аспектах. Самое лучшее в данном подходе – это то, что вы можете использовать конфигурацию на основе соглашений для того, чтобы определить, как применяются аспекты, и можете решить использовать разные соглашения в разных приложениях (например, несмотря на то, что вы можете совместно использовать множество библиотек, в WPF приложении и PowerShell приложении у вас могут быть разные стратегии обработки ошибок).

Примечание

В аспектно-ориентированном программировании соглашение, которое сопоставляет аспекты с классами и членами, называется Pointcut.

Хватит теории – давайте рассмотрим пример.

Пример: перехват с помощью Windsor

Благодаря их повторяющемуся коду аспекты Circuit Breaker и обработчик ошибок из разделов 9.2.1 "Осуществление перехвата с помощью Circuit Breaker" и 9.2.2 "Обработка исключений" являются отличными кандидатами для динамического перехвата. В качестве примера давайте рассмотрим, как можно получить DRY, SOLID код с помощью возможностей перехвата Castle Windsor.

Примечание

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

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

Рисунок 9-12: Три шага, составляющие процесс добавления аспекта в Windsor

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

Реализация перехватчика обработки ошибок

Реализация перехватчика для Windsor требует от нас реализации интерфейса IInterceptor, который имеет всего один метод. Следующий листинг демонстрирует, как реализовать стратегию обработки ошибок из листинга 9-5, но в отличие от листинга 9-5 следующий листинг демонстрирует весь класс.

Листинг 9-8: Реализация перехватчика обработки ошибок
public class ErrorHandlingInterceptor : IInterceptor
{
	public void Intercept(IInvocation invocation)
	{
		try
		{
			invocation.Proceed();
		}
		catch (CommunicationException e)
		{
			this.AlertUser(e.Message);
		}
		catch (InvalidOperationException e)
		{
			this.AlertUser(e.Message);
		}
	}
	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);
	}
}

Строка 1: Реализация IInterceptor

Строка 5-6, 8-16: Реализация аспекта

Строка 7: Вызов расширенного метода

Строка 18: Демонстрация диалогового окна

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

Единственное отличие от листинга 9-5 – вместо того, чтобы делегировать вызов метода конкретному методу, вы должны обобщить все, поскольку применяете этот код к потенциально любому методу. Вы даете Windsor указание, позволить вызову пройти дальше к расширенной строке, вызывая для этого метод Proceed для входного параметра invocation.

Интерфейс IInvocation, переданный в метод Intercept в качестве параметра, представляет собой вызов метода. К примеру, он мог бы представлять вызов метода InsertProduct. Метод Proceed – один из ключевых членов данного интерфейса, поскольку он дает нам возможность позволить вызову пройти к следующей реализации стека.

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

Реализация перехватчика – сложный шаг. Следующий шаг – полегче.

Регистрация перехватчика обработки ошибок

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

Примечание

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

Зарегистрировать класс ErrorHandlingInterceptor легко (container – это экземпляр IWindsorContainer):

container.Register(Component.For<ErrorHandlingInterceptor>());

Регистрация класса ErrorHandlingInterceptor ничем не отличается от регистрации любого другого компонента с помощью Windsor, и вы даже могли бы решить использовать подход, основанный на соглашениях для того, чтобы зарегистрировать все реализации IInterceptor, обнаруженные в конкретной сборке. Возможно, это похоже на шаблонный код из раздела 3.2 "Конфигурирование DI-контейнеров".

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

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

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

Листинг 9-9: Реализация перехватчика Circuit Breaker
public class CircuitBreakerInterceptor : IInterceptor
{
	private readonly ICircuitBreaker breaker;
	public CircuitBreakerInterceptor(
		ICircuitBreaker breaker)
	{
		if (breaker == null)
		{
			throw new ArgumentNullException(
				"breaker");
		}
		this.breaker = breaker;
	}
	public void Intercept(IInvocation invocation)
	{
		this.breaker.Guard();
		try
		{
			invocation.Proceed();
			this.breaker.Succeed();
		}
		catch (Exception e)
		{
			this.breaker.Trip(e);
			throw;
		}
	}
}

Строка 3-13: Constructor Injection

Строка 16-18, 20-26: Реализация аспекта

Строка 19: Вызов расширенного метода

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

Как вы видели в листинге 9-8, вы реализуете интерфейс IInterceptor путем применения шаблона, предложенного предыдущей, повторяющейся реализацией из листинга 9-4. Еще раз вместо вызова конкретного метода вы вызываете метод Proceed для того, чтобы дать перехватчику указание, позволить обработке продолжить свое выполнение для следующего компонента стека Decorator.

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

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

Регистрация перехватчика Circuit Breaker

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

container.Register(Component
	.For<ICircuitBreaker>()
	.ImplementedBy<CircuitBreaker>()
	.DependsOn(new
	{
		timeout = TimeSpan.FromMinutes(1)
	}));
container.Register(Component.For<CircuitBreakerInterceptor>());

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

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

Активация перехватчиков

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

Вы можете рассматривать этот шаг как аналог применения атрибутов аспектов. Если мы применяем к методу гипотетический атрибут [CircuitBreaker], то мы соединяем аспект Circuit Breaker с этим методом. Определение и применение пользовательских атрибутов – один из способов, с помощью которых мы можем активизировать перехватчиков Windsor, но у нас еще есть несколько других, более подходящих доступных вариантов.

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

В следующем листинге вы используете простую реализацию такого Pointcut.

Листинг 9-10: Реализация Pointcut
public class ProductManagementClientInterceptorSelector :
	IModelInterceptorsSelector
{
	public bool HasInterceptors(ComponentModel model)
	{
		return typeof(IProductManagementAgent)
			.IsAssignableFrom(model.Service);
	}
	public InterceptorReference[]
		SelectInterceptors(ComponentModel model,
			InterceptorReference[] interceptors)
	{
		return new[]
		{
			InterceptorReference
				.ForType<ErrorHandlingInterceptor>(),
			InterceptorReference
				.ForType<CircuitBreakerInterceptor>()
		};
	}
}

Строка 6-7: Применение перехватчиков к IProductManagementAgent

Строка 15-18: Возврат перехватчиков

Интерфейс IModelInterceptorsSelector руководствуется паттерном Tester-Doer. Windsor сначала вызовет метод HasInterceptors, чтобы узнать, имеет ли данный компонент, который он собирается инициализировать, какие-либо перехватчики. В данном примере вы отвечаете на этот вопрос положительно, при этом компонент реализует интерфейс IProductManagementAgent, но вы могли бы написать условно сложный код, если бы захотели реализовать более эвристический подход.

Когда метод HasInterceptors вернет значение true, будет вызван метод SelectInterceptors. Благодаря этому методу вы возвращаете ссылки на перехватчиков, которые вы уже зарегистрировали. Обратите внимание, что вы возвращаете не экземпляры перехватчиков, а ссылки на перехватчики, которые вы уже реализовали и зарегистрировали.

Это позволяет Windsor автоматически интегрировать любые перехватчики, которые могут иметь свои собственные зависимости (например, CircuitBreakerInterceptor из листинга 9-9).

Знаете, что! Вам еще нужно зарегистрировать класс ProductManagementClientInterceptorSelector с помощью контейнера. Это делается немного по-другому, но все равно вкладывается в одну строку кода:

container.Kernel.ProxyFactory.AddInterceptorSelector(
	new ProductManagementClientInterceptorSelector());

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

Вы можете подумать, что этот многостраничный анализ перехватчиков Windsor довольно сложен, но вам следует кое-что иметь ввиду:

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

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

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

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

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