DI vs. AOP: внедрение логгирования
StepOneЛоггирование является одним из самых часто используемых примеров сквозной функциональности. Однако, каким образом внедрять этот функционал в приложение максимально просто и эффективно? Давайте разберёмся.
Шаблон проектирования "Декоратор"
"Декоратор" это структурный паттерн, который позволяет добавить объекту новое поведение без его непосредственного изменения, с помощью "упаковки" объекта внутрь специальной обёртки, реализующей тот же контракт.
Согласен, на первый взгляд, звучит достаточно заумно, разберёмся на примере.
Пусть у нас есть некий сервис, который что-то делает:
public interface IMyService
{
void DoStuff();
}
И нам необходимо логгировать его вызов. Однако, внутри метода DoStuff расположена бизнес-логика (ключевой функционал), поэтому мы не хотим раздувать его реализацию инфраструктурой. Здесь нам может помочь этот паттерн:
public class MyServiceDecorator : IMyService
{
private readonly ILogger _logger;
private readonly IMyService _decoratee;
public MyServiceDecorator(ILogger logger, IMyService decoratee)
{
_logger = logger;
_decoratee = decoratee;
}
public void DoStuff()
{
_logger.Log("Logging DoStuff() call");
_decoratee.DoStuff();
}
}
Как видно из примера, мы взяли реализацию нашего сервиса, поместили её в декоратор. Затем, добавили туда логгирование через ту же инъекцию DI, затем реализовали контракт сервиса, обернув декорируемую реализацию. В итоге конечный потребитель сервиса получит реализацию, декорированную возможностью логгировать, и даже об этом не догадается.
Всё выглядит круто до тех пор, пока мы не приходим в реальный мир коммерческой разработки. Этих сервисов сотни, а может быть и тысячи. И нам нужно столько же декораторов. А потом ещё раздувать контейнер этим числом дополнительных регистраций. Выходит, что мы начинаем нарушать DRY. В этом случае рассмотрим следующую опцию.
Перехват вызовов
Как уже было сказано, логгирование является примером cross-cutting concern, сущности, послужившей одной из причин возникновения аспектно-ориентированного программирования. Если приглядеться к предыдущему примеру, то, концептуально, наши действия можно описать, как "перехват вызова" некоего сервиса.
Оказывается есть АОП инструментарий, который позволяет это сделать, реализуя специальные "перехватчики" (interceptors). Выглядит это примерно следующим образом:
public class LogInterceptor : IInterceptor
{
private readonly ILogger _logger;
public LogInterceptor(ILogger logger) => _logger = logger;
public void Intercept(IInvocation invocation)
{
_logger.Log($"Logging invocation of {invocation.MethodName}");
invocation.Procceed();
}
}
Таким образом, мы создали единую логику перехвата вызовов для логгирования и зарегистрировали её в контейнере один раз. Затем достаточно просто пометить сервис, который нужно перехватить.
Такие возможности предоставляет, например, контейнер AutoFac.
© Канал StepOne