Главная страница

Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей


Скачать 5.66 Mb.
НазваниеРуководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
АнкорВнедрение зависимостей в .net
Дата14.12.2019
Размер5.66 Mb.
Формат файлаpdf
Имя файлаВнедрение зависимостей в .NET.pdf
ТипРуководство
#100226
страница24 из 43
1   ...   20   21   22   23   24   25   26   27   ...   43
, но добавляет аудит на соответствующие места. Сможете ли вы увидеть панировку?
Помимо расширенного
ProductRepository для
AuditingProductRepository также нужен сервис, который реализует аудит. В следующем листинге роль такого сервиса играет интерфейс
IAuditor
Листинг 9-1: Объявление
AuditingProductRepository
1.
public partial class AuditingProductRepository :
2.
ProductRepository
3.
{
4.
private readonly ProductRepository
5.
innerRepository;
6.
private readonly IAuditor auditor;
7.
public AuditingProductRepository(
8.
ProductRepository repository,
9.
IAuditor auditor)
10.
{
11.
if (repository == null)
12.
{
13.
throw new ArgumentNullException("repository");
14.
}
15.
if (auditor == null)
16.
{
17.
throw new ArgumentNullException("auditor");
18.
}
19.
this.innerRepository = repository;
20.
this.auditor = auditor;
21.
}
22.
}
Строка 2, 4-5, 8, 19: Наследуется и обертывает
ProductRepository
Строка 6, 9, 20: Сервис аудита
AuditingProductRepository наследуется от той же самой абстракции, которую он расширяет.
AuditingProductRepository использует стандартный Внедрение в конструктор (Constructor Injection) для запроса
ProductRepository
, который он может обернуть и которому он может делегировать свою основную реализацию. Помимо расширенного репозитория для
AuditingProductRepository также требуется
IAuditor
, который он может использоват ь для отслеживания операций, реализованных расширенным репозиторием.

329
Следующий листинг демонстрирует шаблонные реализации двух методов
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, – универсальный вариант. Давайте посмотрим, как вы можете подключить все соответствующие зависимости для выполнения этой работы.
Компоновка Auditi ngProductRepository
Несмотря на то, что многие приложения используют класс
ProductRepository для извлечения информации о товаре, и в связи с тем, что WCF веб-сервис
CommerceService из раздела 7.3.2 "Пример: подключение сервиса управления продуктами" раскрывает
CRUD-операции для
Products
, это подходящее место для начала работы.
В главе 8 вы видели несколько примеров того, как компоновать экземпляр
ProductManagementService
. Листинги 8-4 и 8-5 предоставляли самую корректную реализацию, но в следующем листинге мы проигнорируем тот факт, что
SqlProductRepository является устраняемым, для того, чтобы сконцентрироваться на компоновке Decorat or'ов.

330
Листинг 9-3: Компоновка Decorat or
1.
public IProductManagementService ResolveProductManagementService()
2.
{
3.
string connectionString =
4.
ConfigurationManager.ConnectionStrings
5.
["CommerceObjectContext"].ConnectionString;
6.
ProductRepository sqlRepository =
7.
new SqlProductRepository(connectionString);
8.
IAuditor sqlAuditor =
9.
new SqlAuditor(connectionString);
10.
ProductRepository auditingRepository =
11.
new AuditingProductRepository(
12.
sqlRepository, sqlAuditor);
13.
IContractMapper mapper = new ContractMapper();
14.
return new ProductManagementService(
15.
auditingRepository, mapper);
16.
}
Строка 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
, не изменяя при этом исходного кода существующих классов. Для того чтобы добавить возможность аудита,

331 нам не пришлось изменять
SqlProductRepository
. Это желанное свойство, известное как
принцип от крыт ост и/закрытости.
О бязате льная пищевая аналогия
Думаю, это относится к покрытию говяжьей отбивной панировкой. Несмотря на то, что мы изменили отбивную, мы оставили ее в том же размере вместо того, чтобы разрезать ее и потушить.
Т еперь, когда вы увидели пример перехватывания конкретного
SqlProductRepository с помощью расширенного
AuditingProductRepository
, давайте немного вернемся назад и изучим паттерны и принципы, лежащие в основе механизма перехвата.
Паттерны и принципы механизма перехвата
Как и в случае со многими другими DI-паттернами, паттерн Decorator является давним и хорошо описанным паттерном проектирования, который имел место еще за несколько лет до появления механизма внедрения зависимости. Он является настолько фундаментальной частью механизма перехвата, что независимо от того, знаком ты с ним близко или нет, он гарантированно о себе напоминает.
Вы могли заметить, что такие термины, как принцип единственной от ветственност и и
принцип от крыт ост и/закрытости, используются чаще обычного. Это составляющие части пятиэлементного меню SOLID.
Все эти паттерны и принципы считаются ценным руководством по чистому коду.
Основная цель данного раздела – связать это заданное руководство с механизмом внедрения зависимостей для того, чтобы продемонстрироват ь, что механизм внедрения зависимостей является всего лишь средство достижения цели. Мы используем DI как средство разрешения поддерживаемого кода.
Все потребители зависимостей должны при вызове своих зависимостей соблюдать
принцип подстановки Барбары Лисков. Это позволяет нам заменить первоначально запланированну ю реализацию другой реализацией той же самой абстракции. Поскольку
Decorator реализует такую же абстракцию, как и класс, который он обертывает, мы можем заменить первоначальный класс на Decorator.
Именно это вы и делали в листинге 9-3, когда заменяли первоначальный
SqlProductRepository на
AuditingProductRepository
. Вы могли бы сделать это, не изменяя код
ProductManagementService
, так как
ProductManagementService следует принципу подстановки Барбары Лисков: ему нужен экземпляр
ProductRepository
, и тогда любая реализация будет выполнена.
Возможность расширения поведения класса без изменения его кода называется принципом
от крыт ост и/закрытости, и является одним из пяти принципов, зашифрованных сущностью под названием SOLID.
SO LID
Кому не хотелось бы создавать надежное программное обеспечение? Программное обеспечение, которое могло бы выдержать тест временем и остаться полезным для своих

332 пользователей, – стоящая цель; введение 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);
}
Похожим образом он может изменить возвращаемое значение перед тем, как его вернуть:

333 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'ом, не изменяя при этом потребителя.
Расширенный объект часто внедряется в Decorat or, объявленный в виде абстрактного типа, причем в этом случае Decorator должен соблюдать принцип подстановки Барбары
Лисков и относиться ко всем расширенным объектам одинаково.
В некоторых местах данной книги вы уже видели Decorator'ы в действии. В примере раздела 9.1.1 "Пример: реализация аудита" использовался Decorator, как и в разделе 4.4.4.
Под акронимом SOLID мы понимаем пять принципов объектно-ориентированного программирования, которые оказываются полезными при написании поддерживаемого кода. В таблице 9-1 перечислены эти принципы.
Таблица 9-1: Пять принципов SOLID
Принцип
Описание
Как связан с механизмом внедрения зависимостей
Принцип единственной ответственности (SRP)
Класс должен иметь только одну ответственность. Он должен выполнять только одну задачу, но выполнять ее хорошо.
Противоположность ю данного принципа является антипаттерн под названием
God Class, в котором один класс может делать все, включая приготовление кофе.
Придерживат ься данного принципа может быть сложновато, но одним из многочисленных преимуществ
Constructor Injection является тот факт, что данный принцип становится очевидным всякий раз, когда мы его не соблюдаем.
В примере раздела 9.1.1 "Пример: реализация аудита", вы могли соблюдать принцип единственной ответственности путем разделения

334
Принцип
Описание
Как связан с механизмом внедрения зависимостей ответственностей на отдельные типы:
SqlProductRepository имеет дело только с хранением и извлечением данных о товарах, тогда как
SqlAuditor сконцентрирован на сохранении пути аудита в базе данных.
Единственной ответственностью класса
AuditingProductRepository является координация действий
ProductRepository и
IAuditor
Принцип открытости/закр ытости
(OCP)
Класс должен быть открыт для расширяемости, но закрыт для модификации.
Это означает, что должна присутствовать возможность добавлять к существующему классу поведение, не изменяя при этом код этого класса.
Этого не так просто достичь, но принцип единственной ответственности, по крайне мере, упрощает данный процесс, поскольку, чем проще код, тем проще обнаружить потенциальные "Швы".
Существует много способов сделать класс расширяемым, включая виртуальные методы, внедрение стратегий и применение
Decorat or'ов – но детали не важны, механизм внедрения зависимостей предоставляет данную возможность, позволяя нам компоновать объекты.
Принцип подстановки
Барбары Лисков (LSP)
Клиент должен одинаково относиться ко всем реализациям абстракций.
Мы должны уметь заменять любую реализацию другой реализацией, не разрушая при этом потребителя.
Принцип подстановки Барбары
Лисков является основой механизма внедрения зависимостей. Если потребитель не соблюдает данный принцип, то вы не можете заменять зависимости, и мы теряем какие-то (если не все) преимущества механизма внедрения зависимостей.
Принцип разделения интерфейсов (ISP)
Интерфейсы должны проектироваться дифференцированно. У нас нет желания смешивать слишком большое количество ответственностей в одном интерфейсе, поскольку становится слишком тяжело
Поначалу, кажется, что принцип разделения интерфейсов лишь отдаленно связан с механизмом внедрения зависимостей. Но он важен, поскольку интерфейс, который моделирует все, включая кухонную раковину, подталкивает вас в направлении конкретной реализации. Часто это попахивает

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

336 9.2. Реализация сквозных сущностей
Большинство приложений должно обращаться к аспектам, которые не связаны напрямую ни с какой конкретной возможностью, а вместо этого обращаются к более широкой сущности. Эти сущности часто касаются множества других несвязанных областей кода, находящихся даже в различных модулях и на различных уровнях. Поскольку они охватывают большую область базы кода, мы называем их сквозными сущност ями. В следующей таблице приведены некоторые примеры. Данная таблица не является всеобъемлющим списком всех доступных аспектов; это всего лишь наглядный пример.
Таблица 9-2: Универсальные примеры сквозных сущностей
Аспект
Описание
Аудит
Любая операция, подразумевающая изменение данных, должна оставлять путь аудита, включая временную отметку, идентификатор пользователя, внесшего изменения, и информацию о том, что изменилось. Пример этого вы видели в разделе 9.1.1
"Пример: реализация аудита".
Вход в систему
Слегка отличающийся от аудита вход в систему фокусируется на записи событий, которые отражают состояние приложения. Это могут быть события, интересные IT -отделу, или, возможно, бизнес- события.
Контроль производительно сти
Слегка отличается от аспекта входа в систему, поскольку имеет дело больше с записью производительности, а не с конкретными событиями. Если у вас есть соглаш ения о качест ве
предоставляемых услуг (Service Level Agreem ent s), которые нельзя контролировать посредством стандартной инфраструктуры, вы должны реализовать пользовательский контроль производительно сти. Пользовательские счетчики производительности Windows хорошо подходят для этого, но вам еще нужно добавить некоторый код, который захватывает данные.
Безопасность
Некоторые операции должно быть разрешено выполнять только конкретным пользователям, и вы должны претворить это в жизнь.
Кэширование
Довольно часто вы можете увеличить производительность за счет реализации кэшов, но другой причины, согласно которой конкретный компонент доступа к данным должен иметь дело с этим аспектом, не существует. Возможно, вы захотите иметь возможность разрешать и блокировать кэширование различных реализаций доступа к данным. Мы уже наблюдали намек на реализацию кэширования с помощью Decorator'ов в разделе 4.4.4.
Обработка ошибок
Возможно, мы захотим обрабатывать определенные исключения и либо заносить их в лог, либо демонстрировать пользователю сообщение. Для того чтобы обрабатывать ошибки должным образом, мы можем использовать обработчик ошибок Decorator.
Отказоустойчивост ь
Внешние ресурсы гарантированно время от времени будут недоступны. Вы можете реализовать паттерны отказоустойчивости, например, Circuit Breaker, с помощью Decorator.

337
При рисовании диаграмм архитектуры многоуровневого приложения сквозные сущности чаще всего представляют ся в виде вертикальных блоков, размещенных около уровней, как это показано на рисунке 9-4.
Рисунок 9-4: Чаще всего мы представляем сквозные сущности на диаграммах архитектуры приложения с помощью вертикальных блоков, которые охватывают все уровни. В данном примере безопасность является сквозной сущностью.
В этом разделе мы рассмотрим некоторые примеры, которые иллюстрируют, как мы можем использовать механизм перехвата в форме Decorator'ов для реализации сквозных сущностей. Мы выберем несколько аспектов из таблицы 9-2, чтобы попробовать реализовать их с помощью SOLID принципов, но рассмотрим только небольшое их количество. Как и в случае со многими другими сущностями, механизм перехвата проще понять с помощью абстракции, но вся сложность заключается именно в деталях. Он принимает на себя воздействие, чтобы надлежащим образом впитать приемы, и лучше я приведу вам слишком много примеров, нежели совсем мало. Когда мы закончим с этими примерами, вы должны будете приобрести ясную картину того, что такое механизм перехвата, и как вы можете его применить.
Поскольку вы уже видели ознакомительный пример раздела 9.1.1 "Пример: реализация аудита", мы рассмотрим более сложный пример, чтобы проиллюстриро вать, как можно использовать механизм перехвата в рамках сколь угодно сложной логики. Как только мы это сделаем, мы изучим пример, который приведет нас прямиком к более декларативному подходу.
Осуществление перехвата с помощью Circuit Breaker
Любое приложение, взаимодейству ющее с внешними ресурсами, может сталкиваться с ситуациями, когда ресурс недоступен. Разрываются сетевые соединения, переходят в автономный режим базы данных, веб-сервисы засоряются DDos-ат аками (Distributed
Denial of Service). В таких случаях приложение, передающее сигнал, должно уметь восстанавливаться и незамедлительно решать проблему.
Большинство .NET API имеют время ожидания по умолчанию, гарантирующее, что внешний вызов не заблокирует навсегда потребляющий поток. Однако в ситуации, когда вы только что получили исключение, касающееся времени ожидания, как вам относиться к следующему вызову ресурса, который является виновником возникшего исключения?
Попытаться ли вам вызвать ресурс снова? Поскольку время ожидания чаще всего указывает на то, что другая сторона либо находится в автономном режиме, либо засорена запросами, осуществление нового блокирующего вызова может стать не очень хорошей

338 идеей. Будет лучше допустить самое худшее и незамедлит ельно выдать исключение. Это и есть логическое обоснование паттерна Circuit Breaker.
Circuit Breaker – ст абильный пат терн, так как он добавляет устойчивость приложению, быстро прерываясь, вместо того, чтобы зависать и потреблять ресурсы во время зависания. Это отличный пример нефункционального требования и истинной сквозной сущности, поскольку этому паттерну приходится совсем немного работать с той возможностью, которая реализуется с помощью внешнего вызова.
Сам по себе паттерн Circuit Breaker слегка сложный и его может быть сложно реализовывать, но нам нужно сделать эти вложения всего лишь раз. Мы даже могли бы при желании реализовать его в повторно используемой библиотеке. Имея повторно используемый Circuit Breaker, мы можем с легкостью применить его к разнообразным компонентам, используя паттерн Decorat or.
C ircuit Breaker
Паттерн проектирования Circuit Breaker получил свое название от электрического выключателя с таким же именем. Он создан для размыкания соединения при появлении неисправности с целью предотвращения распространения неисправности.
В прикладном программном обеспечении при возникновении таймаутов или похожих ошибок взаимодействия, ситуация может ухудшиться, если вы будете продолжать долбить упавшую систему. Если удаленная система засорена, множественные повторные вызовы могут привести к падению системы – при этом пауза может дать системе шанс восстановиться. На уровне осуществления вызова потоки, заблокировавшие ожидание таймаутов, могут сделать приложение-по требитель не реагирующим на вызов. Лучше обнаружить разрыв взаимодействия и быстро прерваться на время.
Паттерн Circuit Breaker решает этот вопрос путем выключения выключателя при возникновении ошибки. Обычно в этот паттерн входит время ожидания, что заставляет его повторить соединение через некоторое время; таким образом, он может автоматически восстанавливаться, когла удаленная система становится резервной.
Следующий рисунок иллюстрирует упрощенное представление переключения состояний в Circuit Breaker.
Упрощенная диаграмма переключения состояний паттерна Circuit Breaker. Паттерн Circuit
Breaker начинается с состояния Closed, указывающего на то, что цепь закрыта, и сообщения могут поступать. При возникновении ошибки отключается прерыватель, и состояние переключается на Open. В данном состоянии прерыватель не позволяет ни одного вызова удаленной системы; вместо этого он незамедлительно выдает исключение.
После таймаута состояние переключается на Half-Open (Полуоткрытое), при котором разрешается прохождение только одного удаленного вызова. При успешном выполнении состояние возвращается к Closed, но если вызов не проходит, прерыватель возвращается к состоянию Open, начиная новый таймаут.

339
Вы можете захотеть сделать Circuit Breaker более сложным, нежели это описано здесь.
Для начала вы, возможно, не захотите выключать прерыватель всякий раз при возникновении случайной ошибки, а захотите использовать барьер. Во-вторых, вы должны выключать прерыватель только при возникновении ошибок определенных типов.
Таймауты и исключения, касающиеся процесса взаимодействия, хорошо, но
NullReferenceException
, скорее всего, указывает на баг, а не на скачкообразную ошибку.
Давайте рассмотрим пример, который демонстрирует, как паттерн Decorator можно использовать для добавления поведения Circuit Breaker к существующему внешнему компоненту. В этом примере мы будем концентрироваться на применении повторно используемого Circuit Breaker, а не на том, как он реализован.
Пример: Реализация C ircuit Breaker
В разделе 7.4.2 "Пример: присоединение ценного клиента управления товарами" мы создали W PF приложение, которое взаимодействует с W CF сервисом при помощи интерфейса
IProductManagementAgent
. Несмотря на то, что мы возвращались к этому приложению в разделе 8.2.1 "Использование устраняемых зависимостей", мы никогда не изучали его подробно.
В предыдущих примерах вы использовали
WcfProductManagementAgent
, который реализует интерфейс посредством вызова операций WCF сервиса. Поскольку данная реализация не обладает возможностью явной обработки ошибок, любая ошибка взаимодействия будет передаваться вызывающему объекту.
Это отличная ситуация для Circuit Breaker. Вам хотелось бы незамедлительно прерываться, как только начинают возникать исключения; таким образом, вы не будете блокировать вызывающий поток и засорять сервис. Как демонстрирует рисунок 9-5, вы начинаете с объявления Decorator'а для
IProductManagementAgent и запроса необходимых зависимостей посредством Constructor Injection.

340
Рисунок 9-5:
CircuitBreakerProductManagementAgent
– это Decorat or для
IProductManagementAgent
: обратите внимание на то, как он реализует интерфейс, а также на то, что он содержит экземпляр, внедренный через конструктор. Еще одна зависимость

ICircuitBreaker
, которую мы можем использовать для реализации паттерна Circuit
Breaker.
Т еперь вы можете обернуть любой вызов расширенного
IProductManagementAgent подобно примеру, продемонстрированному в следующем листинге.
Листинг 9-4: Расширение с помощью Circuit Breaker public void InsertProduct(ProductEditorViewModel product)
{ this.breaker.Guard(); try
{ this.innerAgent.InsertProduct(product); this.breaker.Succeed();
} catch (Exception e)
{ this.breaker.Trip(e); throw;
}
}
Первое, что вам нужно сделать перед тем, как вы попытаетесь вызвать расширенного агента – проверить состояние Circuit Breaker. Метод
Guard позволит вам пройти, если
Circuit Breaker находится в состоянии
Closed или
Half-Open
, тогда как этот же метод выдаст исключение, если Circuit Breaker находится в состоянии
Open
. Это гарантирует, что вы незамедлительно прерветесь, если у вас есть причины полагать, что вызов не приведет к успешному завершению.
Если вы сможете пройти мимо метода
Guard
, то можете попытаться вызвать расширенного агента. Обратите внимание на то, что вызов обернут в блок try
: если вызов не выполнится, вы отключаете прерыватель. В данном примере все просто, но в настоящей реализации вам следует схватить и отключить прерыватель от выбора типа исключений. Поскольку
NullReferenceExceptions или похожие типы исключений редко указывают на скачкообразные ошибки, нет причины отключать прерыватель в таких случаях.

341
Как при состоянии
Closed
, так и при состоянии
Half-Open
, отключение прерывателя вернет нас в состояние
Open
. При состоянии
Open время ожидания определяет, когда мы вернемся к состоянию
Half-Open
Наоборот, вы вызываете Circuit Breaker в случае успешного вызова. Если вы уже находитесь в состоянии
Closed
, вы в нем и остаетесь. Если вы находитесь в состоянии
Half-Open
, вы переходите обратно к состоянию
Closed
. При нахождении Circuit Breaker в состоянии Open невозможно сигнализировать об успешном выполнении, поскольку метод
Guard будет гарантировать, что вы никогда не зайдете так далеко.
Все остальные методы
IProductManagementAgent выглядят также с одной лишь разницей, заключающейся в методе, который они вызывают для innerAgent
, и дополнительной строке кода для метода, который возвращает значение. Вы можете увидеть данную вариацию внутри блока try метода
SelectAllProducts
: var products = this.innerAgent.SelectAllProducts(); this.breaker.Succeed(); return products;
Поскольку вы должны сигнализировать Circuit Breaker об успехе, вам приходится сохранять возвращаемо е значение расширенного агента перед тем, как вернуть самого агента; но это единственное отличие между методами, которые возвращают значение, и методами, не возвращающими значение.
На данном этапе вы оставили реализацию
ICircuitBreaker открытой, но реальная реализация является полностью повторно используемым комплексом классов, применяющих паттерн проектирования "Состояние" (State). Рисунок 9-6 демонстрирует включенные в этот комплекс классы.
Рисунок 9-6: Класс
CircuitBreaker реализует интерфейс
ICircuitBreaker путем применения паттерна State. Все три метода реализованы путем делегирования полномочий члену State, подразумевающему возможность реконфигурации, который изменяется подобно переходам состояний от одного к другому.

342
Несмотря на то, что в этой книге мы не собираемся погружаться вглубь реализации
CircuitBreaker
, важно, что вы можете выполнить перехват с помощью условно сложного кода.
П одсказка
Если вы интересуетесь реализацией класса
CircuitBreaker
, то она доступна в коде, присоединенном к данной книге.
Чтобы скомпоновать
ProductManagementAgent с помощью добавленной функциональности, вы можете обернуть ее в другой реализации: var timeout = TimeSpan.FromMinutes(1);
ICircuitBreaker breaker = new CircuitBreaker(timeout);
IProductManagementAgent circuitBreakerAgent = new CircuitBreakerProductManagementAgent(wcfAgent, breaker);
В листинге 7-10 вы компоновали WPF приложение из нескольких зависимостей, включая экземпляр
WcfProductManagementAgent
. Вы можете дополнить эту переменную wcfAgent путем внедрения ее в экземпляр
CircuitBreakerProductManagementAgent
, который реализует тот же интерфейс. В данном кокретном примере вы создаете новый экземпляр класса
CircuitBreaker всякий раз при разрешении зависимостей, а это соответствует стилю существования Transient.
В WPF приложении, в котором вы разрешаете зависимости всего лишь раз, использование
T ransient Circuit Breaker не является проблемой, но в целом это не есть оптимальный стиль существования для такой функционально сти. На другой стороне будет только один веб- сервис. Если данный сервис станет недоступным, Circuit Breaker должен прервать все попытки подключения к этому сервису. Если используется несколько экземпляров
CircuitBreakerProductManagementAgent
, то это должно выполняться для каждого из них.
Более компактный IC ircuitBreaker
Как представлено в данном разделе, интерфейс
ICircuitBreaker содержит три члена:
Guard
,
Succeed и
Trip
. В альтернативно м определении интерфейса мог использоваться стиль передачи продолжений для того, чтобы сократить объем до нескольких единожды используемых методов: public interface ICircuitBreaker
{ void Execute(Action action);
T Execute(Func action);
}
Это позволило бы нам более сжато применять
ICircuitBreaker в каждом методе, подобно тому, как это показано ниже: public void InsertProduct(ProductEditorViewModel product)
{ this.breaker.Execute(() => this.innerAgent.InsertProduct(product));
}

343
Я решил использовать более явную и старомодную версию
ICircuitBreaker
, поскольку мне хочется, чтобы вы могли сконцентрироваться на текущей теме – механизм перехвата.
Несмотря на то, что лично мне нравится стиль передачи продолжений, я считаю лямбда и дженерики по-своему продвинутыми тематиками, и думаю, что в данном контексте они могли бы скорее отвлечь наше внимание, нежели стать полезными.
Предпочтем ли мы, в конечном итоге, одно определение интерфейса другому, не меняет результат текущей главы.
Это очевидный случай для задания стиля существования Singleton для
CircuitBreaker
, но также это означает, что
CircuitBreaker должен быть потоко-безопасным. Согласно своей сущности
CircuitBreaker сохраняет состояние; потоко-безопасность должна быть реализована явным образом. Это делает реализацию даже более сложной.
Несмотря на сложность
CircuitBreaker
, вы с легкостью можете перехватить экземпляр
IProductManagementAgent с помощью Circuit Breaker. Хотя первый пример механизма перехвата в разделе 9.1.1 "Пример: реализация аудита" был довольно простым, пример
Circuit Breaker демонстрирует, что вы можете перехватить класс с помощью сквозных сущностей, чья реализация просто более сложна, чем первоначальная реализация.
Паттерн Circuit Breaker гарантирует, что приложение незамедлительно прервет свое выполнение вместо того, чтобы связывать драгоценные ресурсы; но в идеале приложение не будет полностью разрушено. Для того чтобы справиться с данной проблемой, вы можете реализовать некоторые виды обработки ошибок с помощью механизма перехвата.
Обработка исключений
Вероятно, зависимости время от времени выдают исключения. Даже написанный наилучшим образом код будет (и должен) выдавать исключения, если сталкивается с ситуациями, с которыми не может справиться. Клиенты, использующие внешние ресурсы, попадают в данную категорию. Класс, подобный классу
WcfProductManagementAgent из шаблонного WPF-приложения, является одним из примеров таких зависимостей. Если веб-сервис недоступен, агент начинает выдавать исключения.
Circuit Breaker не изменяет этому фундаментально му свойству. Несмотря на то, что он перехватывает WCF клиента, он все равно выдает исключения.
Вместо сбоя приложения, вы могли бы выбрать возможность предоставления окна сообщения, которое сообщало бы вам о том, что операция не выполнилась, и что вам необходимо повторить попытку позднее.
Вы можете использовать механизм перехвата для того, чтобы добавить обработку ошибок в стиле SOLID. Вы не хотите нагружать зависимость обработкой ошибок. Поскольку зависимость должна рассматриваться как повторно используемый компонент, который можно использовать во множестве различных сценариев, было бы невозможно добавить в саму зависимость стратегию обработки исключений, которая бы подходила для всех сценариев. Если бы вы так сделали, то это противоречило бы принципу единственной ответственности.

344
Используя механизм перехвата для обработки исключений, вы соблюдаете принцип открытости/закр ытости. Это позволяет вам реализовать самую лучшую стратегию обработки ошибок для любой конкретной ситуации. Давайте рассмотрим пример.
П ример: обработка исключений
В предыдущем примере вы обертывали
WcfProductManagementAgent в Circuit Breaker для использования в клиентском приложении Product Management , впервые введенном в разделе 7.4.2 "Пример: присоединение ценного клиента управления товарами". Circuit
Breaker справляется с ошибками, определяя, что клиент незамедлительно прервет свое выполнение, но он все равно будет выдавать исключения. Оставшись необработанными эти ошибки приведут к сбою приложения, поэтому вам следует реализовать Decorator, который знает, как обрабатывать некоторые из этих ошибок. При выдаче исключения должно всплывать сообщение, как это показано на рисунке 9-7.
Рисунок 9-7: Приложение Product Managem ent обрабатывает исключения, касающиеся взаимодейст вия, путем предоставления пользователю сообщения. Обратите внимание, что в данном примере сообщение об ошибке порождается Circuit Breaker, а не вышеупомянутым нарушением взаимодейст вия.
Реализовать такое поведение легко. Т аким же образом, как вы делали это в разделе 9.2.1
"Осуществление перехвата с помощью Circuit Breaker", вы добавляете новый класс
ErrorHandlingProductManagementAgent
, который дополняет интерфейс
IProductManagementAgent
. Следующий листинг демонстрирует шаблон одного из методов данного интерфейса, но все эти методы похожи друг на друга.

345
Листинг 9-5: Обработка исключений
1.
public void InsertProduct(ProductEditorViewModel product)
2.
{
3.
try
4.
{
5.
this.innerAgent.InsertProduct(product);
6.
}
7.
catch (CommunicationException e)
8.
{
9.
this.AlertUser(e.Message);
10.
}
11.
catch (InvalidOperationException e)
12.
{
13.
this.AlertUser(e.Message);
14.
}
15.
}
Строка 5: Делегирует полномочия расширенному агенту
Строка 9, 13: Предупреждает пользователя об опасности
Метод
InsertProduct
– это представитель целостной реализации класса
ErrorHandlingProductManagementAgent
. При возникновении исключения вы пытаетесь вызвать расширенного агента и предупредить пользователя об опасности с помощью сообщения об исключении. Обратите внимание, что вы обрабатываете только определенный набор известных исключений, потому что пресекать все исключения было бы опасно.
Предупреждение пользователя об опасности включает в себя форматирование строки и демонстрацию этой строки пользователю посредством метода
MessageBox.Show
И снова вы добавили функциональность к первоначальной реализации
(
WcfProductManagementAgent
) путем реализации Decorator'а. Вы строго соблюдаете как принцип единственной ответственности, так и принцип открытости/закр ытост и, последовательно добавляя новые типы вместо модификации существующего кода. К настоящему моменту вы должны были уже начать видеть паттерн, который подразумевает более общую систематизацию, нежели Decorator.
Для данной сквозной сущности реализация, основанная на Decorator, склонна к повторяемости. Реализация Circuit Breaker включает в себя применение одного и того же шаблона кода ко всем методам интерфейса
IProductManagementAgent
. Если бы вы хотели добавить Circuit Breaker в другую абстракцию, то вам пришлось бы применить этот же самый код еще и к другим методам. Несмотря на то, что шаблоны отличаются, то же самое справедливо и для кода обработки исключений, который мы только что рассматривали.
Для того чтобы закрепить этот вопрос, давайте вкратце рассмотрим реализацию аспекта безопасности. Данный аспект будет предполагать более общий подход к компоновке сквозных сущностей, который мы в дальнейшем рассмотрим в разделе 9.3.

346
Добавление функциональности обеспечения безопасности
Безопасность – это еще одна универсальная сквозная сущность. Мы хотели бы, насколько это возможно, обезопасить наши приложения от несанкционированного доступа к важной функциональности.
П римечание
Безопасность – это обширная тема для обсуждения, которая охватывает множество областей, включая раскрытие важной информации и взлом сетей. В данном разделе я лишь вкратце коснусь такой темы, как авторизация – т.е. возможность убедиться в том, что только авторизованные пользователи (или системы) могут выполнять определенные действия.
Аналогично тому, как мы использовали Circuit Breaker, нам хотелось бы перехватить вызов метода и проверить, должен ли этот вызов быть разрешен. Если не должен, то вместо разрешения вызова будет выдано исключение. Принцип тот же: разница заключается в критерии, который мы используем для определения обоснованности вызова.
Универсальный подход к реализации логики авторизации – применить защиту на основании ролей с помощью
Thread.CurrentPrincipal
. Вы могли бы начать с Decorat or'а
SecureProductRepository
. Поскольку, как вы уже видели в предыдущих разделах, все методы похожи друг на друга, следующий листинг демонстрирует всего лишь реализацию шаблонного метода.
Листинг 9-6: Явная проверка авторизации public override void InsertProduct(Product product)
{ if (!Thread.CurrentPrincipal.IsInRole("ProductManager"))
{ throw new SecurityException();
} this.innerRepository.InsertProduct(product);
}
Метод
InsertProduct начинается с граничного оператора, который явным образом вызывает
Thread.CurrentPrincipal и запрашивает, обладает ли он ролью
ProductManager
. Если он не обладает данной ролью, то он незамедлительно выдает исключение. Только если вызываемый
IPrincipal имеет требуемую роль, вы позволяете ему обойти граничный оператор и вызвать расширенный репозиторий.
П римечание
Запомните, что
Thread.CurrentPrincipal
– это пример паттерна Ambient Cont ext .
Т о, что
Thread.CurrentPrincipal инкапсулируется в классе
System.Security.Permissions.PrincipalPermission
, является всеобщей идиомой кодирования; поэтому вы могли бы написать предыду щий пример более сжато:

347 public override void InsertProduct(Product product)
{ new PrincipalPermission(null, "ProductManager").Demand(); this.innerRepository.InsertProduct(product);
}
Класс
PrincipalPermission инкапсулирует запрос о том, имеет ли текущий
IPrincipal определенную роль. Вызов метода
Demand приведет к выдаче исключения, если
Thread.CurrentPrincipal не имеет ролей
ProductManager
. Данный пример функционально эквивалентен листингу 9-6.
Когда единственное, что вы требуете, – чтобы текущий
IPrincipal имел определенну ю роль, вы можете перейти к чисто декларативному стилю:
[PrincipalPermission(SecurityAction.Demand, Role = "ProductManager")] public override void InsertProduct(Product product)
{ this.innerRepository.InsertProduct(product);
}
Аттрибут
PrincipalPermission предлагает ту же самую функциональност ь, что и класс
PrincipalPermission
, но раскрывается в виде атрибута. Поскольку .NET Fram ework понимает этот атрибут, где бы он его ни встречал, он выполняет соответствующее требование
PrincipalPermission
На данном этапе наличие отдельного Decorator только с целью применения атрибута выглядит слегка уничтожающим. Почему бы не применить атрибут напрямую к самому первоначальному классу? Несмотря на то, что это кажется довольно привлекательным, существует несколько причин, почему вы можете не захотеть поступить именно так:

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

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

1   ...   20   21   22   23   24   25   26   27   ...   43


написать администратору сайта