Реализация сквозных сущностей

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

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

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

Рисунок 9-4: Чаще всего мы представляем сквозные сущности на диаграммах архитектуры приложения с помощью вертикальных блоков, которые охватывают все уровни. В данном примере безопасность является сквозной сущностью.

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

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

Осуществление перехвата с помощью Circuit Breaker

Любое приложение, взаимодействующее с внешними ресурсами, может сталкиваться с ситуациями, когда ресурс недоступен. Разрываются сетевые соединения, переходят в автономный режим базы данных, веб-сервисы засоряются DDos-атаками (Distributed Denial of Service). В таких случаях приложение, передающее сигнал, должно уметь восстанавливаться и незамедлительно решать проблему.

Большинство .NET API имеют время ожидания по умолчанию, гарантирующее, что внешний вызов не заблокирует навсегда потребляющий поток. Однако в ситуации, когда вы только что получили исключение, касающееся времени ожидания, как вам относиться к следующему вызову ресурса, который является виновником возникшего исключения? Попытаться ли вам вызвать ресурс снова? Поскольку время ожидания чаще всего указывает на то, что другая сторона либо находится в автономном режиме, либо засорена запросами, осуществление нового блокирующего вызова может стать не очень хорошей идеей. Будет лучше допустить самое худшее и незамедлительно выдать исключение. Это и есть логическое обоснование паттерна Circuit Breaker.

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

Сам по себе паттерн Circuit Breaker слегка сложный и его может быть сложно реализовывать, но нам нужно сделать эти вложения всего лишь раз. Мы даже могли бы при желании реализовать его в повторно используемой библиотеке. Имея повторно используемый Circuit Breaker, мы можем с легкостью применить его к разнообразным компонентам, используя паттерн Decorator.

Circuit Breaker

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

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

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

Следующий рисунок иллюстрирует упрощенное представление переключения состояний в Circuit Breaker.

Упрощенная диаграмма переключения состояний паттерна Circuit Breaker. Паттерн Circuit Breaker начинается с состояния Closed, указывающего на то, что цепь закрыта, и сообщения могут поступать. При возникновении ошибки отключается прерыватель, и состояние переключается на Open. В данном состоянии прерыватель не позволяет ни одного вызова удаленной системы; вместо этого он незамедлительно выдает исключение. После таймаута состояние переключается на Half-Open (Полуоткрытое), при котором разрешается прохождение только одного удаленного вызова. При успешном выполнении состояние возвращается к Closed, но если вызов не проходит, прерыватель возвращается к состоянию Open, начиная новый таймаут.

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

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

Пример: Реализация Circuit Breaker

В разделе 7.4.2 "Пример: присоединение ценного клиента управления товарами" мы создали WPF приложение, которое взаимодействует с WCF сервисом при помощи интерфейса IProductManagementAgent. Несмотря на то, что мы возвращались к этому приложению в разделе 8.2.1 "Использование устраняемых зависимостей", мы никогда не изучали его подробно.

В предыдущих примерах вы использовали WcfProductManagementAgent, который реализует интерфейс посредством вызова операций WCF сервиса. Поскольку данная реализация не обладает возможностью явной обработки ошибок, любая ошибка взаимодействия будет передаваться вызывающему объекту.

Это отличная ситуация для Circuit Breaker. Вам хотелось бы незамедлительно прерываться, как только начинают возникать исключения; таким образом, вы не будете блокировать вызывающий поток и засорять сервис. Как демонстрирует рисунок 9-5, вы начинаете с объявления Decorator'а для IProductManagementAgent и запроса необходимых зависимостей посредством Constructor Injection.

Рисунок 9-5: CircuitBreakerProductManagementAgent – это Decorator для 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 или похожие типы исключений редко указывают на скачкообразные ошибки, нет причины отключать прерыватель в таких случаях.

Как при состоянии 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, подразумевающему возможность реконфигурации, который изменяется подобно переходам состояний от одного к другому.

Несмотря на то, что в этой книге мы не собираемся погружаться вглубь реализации 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 приложении, в котором вы разрешаете зависимости всего лишь раз, использование Transient Circuit Breaker не является проблемой, но в целом это не есть оптимальный стиль существования для такой функциональности. На другой стороне будет только один веб-сервис. Если данный сервис станет недоступным, Circuit Breaker должен прервать все попытки подключения к этому сервису. Если используется несколько экземпляров CircuitBreakerProductManagementAgent, то это должно выполняться для каждого из них.

Более компактный ICircuitBreaker

Как представлено в данном разделе, интерфейс ICircuitBreaker содержит три члена: Guard, Succeed и Trip. В альтернативном определении интерфейса мог использоваться стиль передачи продолжений для того, чтобы сократить объем до нескольких единожды используемых методов:

public interface ICircuitBreaker
{
	void Execute(Action action);
	T Execute<T>(Func<T> action);
}

Это позволило бы нам более сжато применять ICircuitBreaker в каждом методе, подобно тому, как это показано ниже:

public void InsertProduct(ProductEditorViewModel product)
{
	this.breaker.Execute(() =>
		this.innerAgent.InsertProduct(product));
}

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

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

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

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

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

Обработка исключений

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

Circuit Breaker не изменяет этому фундаментальному свойству. Несмотря на то, что он перехватывает WCF клиента, он все равно выдает исключения.

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

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

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

Пример: обработка исключений

В предыдущем примере вы обертывали WcfProductManagementAgent в Circuit Breaker для использования в клиентском приложении Product Management, впервые введенном в разделе 7.4.2 "Пример: присоединение ценного клиента управления товарами". Circuit Breaker справляется с ошибками, определяя, что клиент незамедлительно прервет свое выполнение, но он все равно будет выдавать исключения. Оставшись необработанными эти ошибки приведут к сбою приложения, поэтому вам следует реализовать Decorator, который знает, как обрабатывать некоторые из этих ошибок. При выдаче исключения должно всплывать сообщение, как это показано на рисунке 9-7.

Рисунок 9-7: Приложение Product Management обрабатывает исключения, касающиеся взаимодействия, путем предоставления пользователю сообщения. Обратите внимание, что в данном примере сообщение об ошибке порождается Circuit Breaker, а не вышеупомянутым нарушением взаимодействия.

Реализовать такое поведение легко. Таким же образом, как вы делали это в разделе 9.2.1 "Осуществление перехвата с помощью Circuit Breaker", вы добавляете новый класс ErrorHandlingProductManagementAgent, который дополняет интерфейс IProductManagementAgent. Следующий листинг демонстрирует шаблон одного из методов данного интерфейса, но все эти методы похожи друг на друга.

Листинг 9-5: Обработка исключений
public void InsertProduct(ProductEditorViewModel product)
{
	try
	{
		this.innerAgent.InsertProduct(product);
	}
	catch (CommunicationException e)
	{
		this.AlertUser(e.Message);
	}
	catch (InvalidOperationException e)
	{
		this.AlertUser(e.Message);
	}
}

Строка 5: Делегирует полномочия расширенному агенту

Строка 9, 13: Предупреждает пользователя об опасности

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

Предупреждение пользователя об опасности включает в себя форматирование строки и демонстрацию этой строки пользователю посредством метода MessageBox.Show.

И снова вы добавили функциональность к первоначальной реализации (WcfProductManagementAgent) путем реализации Decorator'а. Вы строго соблюдаете как принцип единственной ответственности, так и принцип открытости/закрытости, последовательно добавляя новые типы вместо модификации существующего кода. К настоящему моменту вы должны были уже начать видеть паттерн, который подразумевает более общую систематизацию, нежели Decorator.

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

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

Добавление функциональности обеспечения безопасности

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

Примечание

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

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

Универсальный подход к реализации логики авторизации – применить защиту на основании ролей с помощью Thread.CurrentPrincipal. Вы могли бы начать с Decorator'а 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 Context.

То, что Thread.CurrentPrincipal инкапсулируется в классе System.Security.Permissions.PrincipalPermission, является всеобщей идиомой кодирования; поэтому вы могли бы написать предыдущий пример более сжато:

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 Framework понимает этот атрибут, где бы он его ни встречал, он выполняет соответствующее требование PrincipalPermission.

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

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

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

или RSS канал: Что новенького на smarly.net