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

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

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

Марк Симан

9.1. Знакомство с механизмом перехвата

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

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

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

Пример: реализация аудита

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

Реализация AuditingProductRepository

Реализовать аудит для ProductRepository мы можем путем введения нового класса AuditingProductRepository, который обертывает другой ProductRepository и реализует аудит. Рисунок 9-3 иллюстрирует то, как типы связаны друг с другом.

Рисунок 9-3: AuditingProductRepository наследуется от абстрактного класса ProductRepository и обертывает экземпляр любой другой реализации ProductRepository. AuditingProductRepository делегирует всю работу расширенному ProductRepository, но добавляет аудит на соответствующие места. Сможете ли вы увидеть панировку?

Помимо расширенного ProductRepository для AuditingProductRepository также нужен сервис, который реализует аудит. В следующем листинге роль такого сервиса играет интерфейс IAuditor.

Листинг 9-1: Объявление AuditingProductRepository
public partial class AuditingProductRepository :
	ProductRepository
{
	private readonly ProductRepository
		innerRepository;
	private readonly IAuditor auditor;
	public AuditingProductRepository(
		ProductRepository repository,
		IAuditor auditor)
	{
		if (repository == null)
		{
			throw new ArgumentNullException("repository");
		}
		if (auditor == null)
		{
			throw new ArgumentNullException("auditor");
		}
		this.innerRepository = repository;
		this.auditor = auditor;
	}
}

Строка 2, 4-5, 8, 19: Наследуется и обертывает ProductRepository

Строка 6, 9, 20: Сервис аудита

AuditingProductRepository наследуется от той же самой абстракции, которую он расширяет. AuditingProductRepository использует стандартный Внедрение в конструктор (Constructor Injection) для запроса ProductRepository, который он может обернуть и которому он может делегировать свою основную реализацию. Помимо расширенного репозитория для AuditingProductRepository также требуется IAuditor, который он может использовать для отслеживания операций, реализованных расширенным репозиторием.

Следующий листинг демонстрирует шаблонные реализации двух методов AuditingProductRepository.

Листинг 9-2: Реализация AuditingProductRepository
public override Product SelectProduct(int id)
{
	return this.innerRepository.SelectProduct(id);
}
public override void UpdateProduct(Product product)
{
	this.innerRepository.UpdateProduct(product);
	this.auditor.Record(
		new AuditEvent("ProductUpdated", product));
}

Не для всех операций нужен аудит. Универсальным требованием является аудит всех операций Create, Update и Delete, и игнорирование операций Read. Поскольку метод SelectProduct является истинной операцией Read, вы делегируете вызов расширенного репозитория и незамедлительно возвращаете результат.

Метод UpdateProduct, с другой стороны, должен подвергаться аудиту. Вы все еще делегируете реализацию расширенному репозиторию, но после того, как делегированный метод возвращается, вы используете внедренный IAuditor для отслеживания операции.

Decorator, подобно AuditingProductRepository, является своего рода панировкой говяжьей отбивной: он приукрашивает основной ингредиент, не изменяя его. Сама по себе панировка не является просто пустой оболочкой, а содержит собственный список ингредиентов. Настоящая панировка делается из панировочных сухарей и специй; подобным образом AuditingProductRepository содержит IAuditor.

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

Вы можете создать любую реализацию IAuditor, какую только пожелаете, но реализация, основанная на SQL Server, – универсальный вариант. Давайте посмотрим, как вы можете подключить все соответствующие зависимости для выполнения этой работы.

Компоновка AuditingProductRepository

Несмотря на то, что многие приложения используют класс ProductRepository для извлечения информации о товаре, и в связи с тем, что WCF веб-сервис CommerceService из раздела 7.3.2 "Пример: подключение сервиса управления продуктами" раскрывает CRUD-операции для Products, это подходящее место для начала работы.

В главе 8 вы видели несколько примеров того, как компоновать экземпляр ProductManagementService. Листинги 8-4 и 8-5 предоставляли самую корректную реализацию, но в следующем листинге мы проигнорируем тот факт, что SqlProductRepository является устраняемым, для того, чтобы сконцентрироваться на компоновке Decorator'ов.

Листинг 9-3: Компоновка Decorator
public IProductManagementService ResolveProductManagementService()
{
	string connectionString =
		ConfigurationManager.ConnectionStrings
		["CommerceObjectContext"].ConnectionString;
	ProductRepository sqlRepository =
		new SqlProductRepository(connectionString);
	IAuditor sqlAuditor =
		new SqlAuditor(connectionString);
	ProductRepository auditingRepository =
		new AuditingProductRepository(
			sqlRepository, sqlAuditor);
	IContractMapper mapper = new ContractMapper();
	return new ProductManagementService(
		auditingRepository, mapper);
}

Строка 6-7: Внутренний ProductRepository

Строка 10-12: Decorator

Строка 14-15: Внедряет Decorator

Как и в листинге 7-9, поскольку вам хочется использовать ProductRepository, базирующийся на SQL Server, вы создаете новый экземпляр SqlProductRepository. Но вместо того, чтобы напрямую внедрять его в экземпляр ProductManagementService, вы будете обертывать его в AuditingProductRepository.

Вы внедряете и SqlProductRepository, и базирующуюся на SQL Server реализацию IAuditor в экземпляр AuditingProductRepository. Обратите внимание на то, как и sqlRepository, и auditingRepository объявлены в виде экземпляров ProductRepository.

Теперь вы можете внедрить auditingRepository в новый экземпляр ProductManagementService и вернуть его. ProductManagementService видит только auditingRepository и ничего не знает о sqlRepository.

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

Листинг 9-3 – это упрощенный пример, в котором игнорируются проблемы жизненного цикла. Поскольку и SqlProductRepository, и SqlAuditor являются устраняемыми типами, данный код станет причиной утечки ресурсов. Более корректной реализацией была бы интерполяция листинга 9-3 с листингами 8-4 и 8-5 – но я уверен, вы поймете, что в данном случае реализация начнет усложняться.

Подсказка

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

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

Обязательная пищевая аналогия

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

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

Паттерны и принципы механизма перехвата

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

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

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

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

Именно это вы и делали в листинге 9-3, когда заменяли первоначальный SqlProductRepository на AuditingProductRepository. Вы могли бы сделать это, не изменяя код ProductManagementService, так как ProductManagementService следует принципу подстановки Барбары Лисков: ему нужен экземпляр ProductRepository, и тогда любая реализация будет выполнена.

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

SOLID

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

Decorator

Паттерн Decorator впервые был описан в книге "Паттерны проектирования". Цель этого паттерна – "динамически присоединить к объекту дополнительные ответственности. Decorator'ы являются гибкой альтернативой деления на подклассы, которое выполняется для расширения функциональности."

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

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

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

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

public string Greet(string name)
{
	return this.innerComponent.Greet(name);
}

Кроме того, перед тем, как делегировать вызов, он может изменить входные данные:

public string Greet(string name)
{
	var reversed = this.Reverse(name);
	return this.innerComponent.Greet(reversed);
}

Похожим образом он может изменить возвращаемое значение перед тем, как его вернуть:

public string Greet(string name)
{
	var returnValue = this.innerComponent.Greet(name);
	return this.Reverse(returnValue);
}

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

Decorator также может решить не вызывать приведенную ниже реализацию:

public string Greet(string name)
{
	if (name == null)
	{
		return "Hello world!";
	}
	return this.innerComponent.Greet(name);
}

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

То, что отличает Decorator от любого класса, содержащего зависимости, – расширенный объект реализует ту же самую абстракцию, что и Decorator. Это позволяет Composer заменить первоначальный компонент Decorator'ом, не изменяя при этом потребителя. Расширенный объект часто внедряется в Decorator, объявленный в виде абстрактного типа, причем в этом случае Decorator должен соблюдать принцип подстановки Барбары Лисков и относиться ко всем расширенным объектам одинаково.

В некоторых местах данной книги вы уже видели Decorator'ы в действии. В примере раздела 9.1.1 "Пример: реализация аудита" использовался Decorator, как и в разделе 4.4.4.

Под акронимом SOLID мы понимаем пять принципов объектно-ориентированного программирования, которые оказываются полезными при написании поддерживаемого кода. В таблице 9-1 перечислены эти принципы.

Таблица 9-1: Пять принципов SOLID
Принцип Описание Как связан с механизмом внедрения зависимостей
Принцип единственной ответственности (SRP) Класс должен иметь только одну ответственность. Он должен выполнять только одну задачу, но выполнять ее хорошо. Противоположностью данного принципа является антипаттерн под названием God Class, в котором один класс может делать все, включая приготовление кофе. Придерживаться данного принципа может быть сложновато, но одним из многочисленных преимуществ Constructor Injection является тот факт, что данный принцип становится очевидным всякий раз, когда мы его не соблюдаем.

В примере раздела 9.1.1 "Пример: реализация аудита", вы могли соблюдать принцип единственной ответственности путем разделения ответственностей на отдельные типы: SqlProductRepository имеет дело только с хранением и извлечением данных о товарах, тогда как SqlAuditor сконцентрирован на сохранении пути аудита в базе данных. Единственной ответственностью класса AuditingProductRepository является координация действий ProductRepository и IAuditor.
Принцип открытости/закрытости (OCP) Класс должен быть открыт для расширяемости, но закрыт для модификации. Это означает, что должна присутствовать возможность добавлять к существующему классу поведение, не изменяя при этом код этого класса.

Этого не так просто достичь, но принцип единственной ответственности, по крайне мере, упрощает данный процесс, поскольку, чем проще код, тем проще обнаружить потенциальные "Швы".
Существует много способов сделать класс расширяемым, включая виртуальные методы, внедрение стратегий и применение Decorator'ов – но детали не важны, механизм внедрения зависимостей предоставляет данную возможность, позволяя нам компоновать объекты.
Принцип подстановки Барбары Лисков (LSP) Клиент должен одинаково относиться ко всем реализациям абстракций. Мы должны уметь заменять любую реализацию другой реализацией, не разрушая при этом потребителя. Принцип подстановки Барбары Лисков является основой механизма внедрения зависимостей. Если потребитель не соблюдает данный принцип, то вы не можете заменять зависимости, и мы теряем какие-то (если не все) преимущества механизма внедрения зависимостей.
Принцип разделения интерфейсов (ISP) Интерфейсы должны проектироваться дифференцированно. У нас нет желания смешивать слишком большое количество ответственностей в одном интерфейсе, поскольку становится слишком тяжело его реализовывать.

Я считаю, что принцип разделения интерфейсов является концептуальной основой принципа единственной ответственности. Согласно принципу ISP интерфейсы должны моделировать только одну сущность, тогда как принцип SRP утверждает, что реализации должны иметь только одну ответственность.[/p]
Поначалу, кажется, что принцип разделения интерфейсов лишь отдаленно связан с механизмом внедрения зависимостей. Но он важен, поскольку интерфейс, который моделирует все, включая кухонную раковину, подталкивает вас в направлении конкретной реализации. Часто это попахивает leaky-абстракцией и значительно усложняет процесс замены зависимостей, поскольку некоторые члены интерфейса могут не иметь смысла в контексте, отличающемся от того, который первоначально запускал проектирование.
Принцип инверсии зависимостей (DIP) Еще один термин крылатой фразы программирование на основании интерфейсов, а не на основании конкретной реализации. Принцип инверсии зависимостей (DIP) – это принцип, являющийся основанием механизма внедрения зависимостей.

Примечание

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

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

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

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