Сквозной функционал через обертки

от автора

При разработке мы не редко сталкиваемся с ситуацией, когда при выполнении какой-либо бизнес-логики требуется записать логи, аудиты, разослать оповещения. В общем реализовать некоторый сквозной функционал.
Когда масштабы производства небольшие, можно особо не усердствовать и все это делать прямо в методах. Постепенно, конструктор сервиса начинает обрастать входящими сервисами для выполнения БЛ и сквозного функционала. А это уже ILogger, IAuditService, INotifiesSerice.
Не знаю как Вы, а я не люблю много инъекций и большие методы, которые выполняют много действий за раз.

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

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

Спойлер. На самом деле, мне удалось решить чуть больше проблем, чем я описал выше. Например, я могу отдать разработку БЛ одному разработчику, а навешивание сквозного функционала и даже валидации входящих данных — другому одновременно.

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

Итак, что я хочу как разработчик?

  • При реализации БЛ не отвлекаться на левый функционал.
  • Иметь возможность в юнит тестах тестировать только БЛ. Причем я не люблю делать 100500 моков, чтобы отключить весь вспомогательный функционал. 2-3 — еще ладно, но больше не хочу.
  • Понимать, что происходит, не имея 7 пядей во лбу. 🙂
  • Иметь возможность управлять временем жизни сервиса и каждой его обертки ОТДЕЛЬНО!

Что я хочу, как проектировщик и лидер команды?

  • Иметь возможность декомпозировать задачи наиболее оптимально и с наименьшей связностью, чтобы одновременно можно было задействовать как можно больше разработчиков на разные задачи и при этом чтобы они тратили как можно меньше времени на исследование (если разработчику надо разработать БЛ, а параллельно думать, что и как залогировать, он потратит больше времени на исследование. И так с каждым куском БЛ. Куда проще взяться за записи аудитов и распихать их по всему проекту).
  • Оправлять порядком выполнения кода отдельно от его разработки.

Поможет мне в этом вот такой интерфейс.

    /// <summary>     ///     Обертка для сервиса.     /// </summary>     /// <typeparam name="T"> Класс сервиса. </typeparam>     public interface IDecorator<T>     {         /// <summary>         ///     Делегат для работы декоратора.         /// </summary>         Func<T> NextDelegate { get; set; }     }

Можно использовать как то так

interface IService {     Response Method(Request request); }  class Service : IService {     public Response Method(Request request)     {         // BL     } }  class Wrapper : IDecorator<IService>, IService {     public Func<IService> NextDelegate { get; set; }      public Response Method(Request request)     {         // code before         var result = NextDelegate().Method(request);         // code after         return result;     } }

Таким образом, действие у нас будет уходить в глубину.
wrapper1
wrapper2
service
end wrapper2
end wrapper1

Но, постойте. Это же уже есть в ООП и называется наследование. 😀

class Service {} class Wrapper1: Service {} class Wrapper2: Wrapper1 {}

Я как представил, что появится дополнительный сквозной функционал, который придется внедрять по всему приложению в середину или менять местами уже имеющиеся, так у меня на спине волосы дыбом встали.
Но моя лень — это не уважительная причина. Уважительная причина в том, что будут большие проблемы при модульном тестировании функционала в классах Wrapper1 и Wrapper2, тогда как в моем примере NextDelegate можно просто замокать. Более того, у сервиса и каждой обертки свой собственный набор инструментов, которые инжектятся в конструктор, тогда как при наследовании последняя обертка обязана иметь ненужные инструменты, чтобы передать их родителям.

Итак, подход принят, осталось придумать, где, как и когда назначать NextDelegate.

Я решил, что самым логичным решением будет делать это там, где я регистрирую сервисы. (Startup.sc, по умолчанию).

Вот как это выглядит в базовом варианте

            services.AddScoped<Service>();             services.AddTransient<Wrapper1>();             services.AddSingleton<Wrapper2>();             services.AddSingleton<IService>(sp =>             {                 var wrapper2 = sp.GetService<Wrapper2>();                 wrapper2.NextDelegate = () =>                 {                     var wrapper1 = sp.GetService<Wrapper1>();                     wrapper1.NextDelegate = () =>                     {                         return sp.GetService<Service>();                     };                      return wrapper1;                 };                  return wrapper2;             });

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

Эту проблему можно решить перебором или рекурсией. Но под капотом. Внешне все должно выглядеть просто и понятно.

Вот чего мне удалось добиться

            services.AddDecoratedScoped<IService, Service>(builder =>             {                 builder.AddSingletonDecorator<Wrapper1>();                 builder.AddTransientDecorator<Wrapper2>();                 builder.AddScopedDecorator<Wrapper3>();             });

А помогли мне в этом вот эти методы расширения

А помогли мне в этом вот эти методы расширения

    /// <summary>     ///     Методы расширения для декораторов.     /// </summary>     public static class DecorationExtensions     {         /// <summary>         ///     Метод регистрации декорируемого сервиса.         /// </summary>         /// <typeparam name="TDefinition"> Интерфейс сервиса. </typeparam>         /// <typeparam name="TImplementation"> Реализация сервиса. </typeparam>         /// <param name="lifeTime"></param>         /// <param name="serviceCollection"> Коллекция сервисов. </param>         /// <param name="decorationBuilder"> Построитель декораций. </param>         /// <returns> Коллекцию сервисов после регистрации декораторов. </returns>         public static IServiceCollection AddDecorated<TDefinition, TImplementation>(             this IServiceCollection serviceCollection, ServiceLifetime lifeTime,             Action<DecorationBuilder<TDefinition>> decorationBuilder)             where TImplementation : TDefinition         {             var builder = new DecorationBuilder<TDefinition>();             decorationBuilder(builder);              var types = builder.ServiceDescriptors.Select(k => k.ImplementationType).ToArray();              var serviceDescriptor = new ServiceDescriptor(typeof(TImplementation), typeof(TImplementation), lifeTime);              serviceCollection.Add(serviceDescriptor);              foreach (var descriptor in builder.ServiceDescriptors)             {                 serviceCollection.Add(descriptor);             }              var resultDescriptor = new ServiceDescriptor(typeof(TDefinition),                 ConstructServiceFactory<TDefinition>(typeof(TImplementation), types), ServiceLifetime.Transient);             serviceCollection.Add(resultDescriptor);              return serviceCollection;         }          /// <summary>         ///     Метод регистрации декорируемого сервиса с временем жизни Scoped.         /// </summary>         /// <typeparam name="TDefinition"> Интерфейс сервиса. </typeparam>         /// <typeparam name="TImplementation"> Реализация сервиса. </typeparam>         /// <param name="serviceCollection"> Коллекция сервисов. </param>         /// <param name="decorationBuilder"> Построитель декораций. </param>         /// <returns> Коллекцию сервисов после регистрации декораторов. </returns>         public static IServiceCollection AddDecoratedScoped<TDefinition, TImplementation>(             this IServiceCollection serviceCollection,             Action<DecorationBuilder<TDefinition>> decorationBuilder)             where TImplementation : TDefinition         {             return serviceCollection.AddDecorated<TDefinition, TImplementation>(ServiceLifetime.Scoped,                 decorationBuilder);         }          /// <summary>         ///     Метод регистрации декорируемого сервиса с временем жизни Singleton.         /// </summary>         /// <typeparam name="TDefinition"> Интерфейс сервиса. </typeparam>         /// <typeparam name="TImplementation"> Реализация сервиса. </typeparam>         /// <param name="serviceCollection"> Коллекция сервисов. </param>         /// <param name="decorationBuilder"> Построитель декораций. </param>         /// <returns> Коллекцию сервисов после регистрации декораторов. </returns>         public static IServiceCollection AddDecoratedSingleton<TDefinition, TImplementation>(             this IServiceCollection serviceCollection,             Action<DecorationBuilder<TDefinition>> decorationBuilder)             where TImplementation : TDefinition         {             return serviceCollection.AddDecorated<TDefinition, TImplementation>(ServiceLifetime.Singleton,                 decorationBuilder);         }          /// <summary>         ///     Метод регистрации декорируемого сервиса с временем жизни Transient.         /// </summary>         /// <typeparam name="TDefinition"> Интерфейс сервиса. </typeparam>         /// <typeparam name="TImplementation"> Реализация сервиса. </typeparam>         /// <param name="serviceCollection"> Коллекция сервисов. </param>         /// <param name="decorationBuilder"> Построитель декораций. </param>         /// <returns> Коллекцию сервисов после регистрации декораторов. </returns>         public static IServiceCollection AddDecoratedTransient<TDefinition, TImplementation>(             this IServiceCollection serviceCollection,             Action<DecorationBuilder<TDefinition>> decorationBuilder)             where TImplementation : TDefinition         {             return serviceCollection.AddDecorated<TDefinition, TImplementation>(ServiceLifetime.Transient,                 decorationBuilder);         }          /// <summary>         ///     Метод         /// </summary>         /// <typeparam name="TService"></typeparam>         /// <param name="implType"></param>         /// <param name="next"></param>         /// <returns></returns>         private static Func<IServiceProvider, TService> ConstructDecorationActivation<TService>(Type implType,             Func<IServiceProvider, TService> next)         {             return x =>             {                 var service = (TService) x.GetService(implType);                  if (service is IDecorator<TService> decorator)                     decorator.NextDelegate = () => next(x);                 else                     throw new InvalidOperationException("Ожидался декоратор");                  return service;             };         }          /// <summary>         ///     Создание фабрики для декорируемого сервиса.         /// </summary>         /// <typeparam name="TDefinition"> Тип контракта сервиса. </typeparam>         /// <param name="serviceType"> Тип реализации сервиса. </param>         /// <param name="decoratorTypes"> Типы делегатов в требуемом порядке. </param>         /// <returns> Фабрику создания сервиса через DI. </returns>         private static Func<IServiceProvider, object> ConstructServiceFactory<TDefinition>(Type serviceType,             Type[] decoratorTypes)         {             return sp =>             {                 Func<IServiceProvider, TDefinition> currentFunc = x =>                     (TDefinition) x.GetService(serviceType);                 foreach (var decorator in decoratorTypes)                 {                     currentFunc = ConstructDecorationActivation(decorator, currentFunc);                 }                  return currentFunc(sp);             };         }     }

Теперь немного сахара для функциональщиков

Теперь немного сахара для функциональщиков

    /// <summary>     ///     Базовый класс декоратора.     /// </summary>     /// <typeparam name="T"> Тип декорируемого сервиса. </typeparam>     public class DecoratorBase<T> : IDecorator<T>     {         /// <summary>         ///     Делегат для получения следующего декоратора или сервиса.         /// </summary>         public Func<T> NextDelegate { get; set; }          /// <summary>         ///     Выполнить код декоратора с вызовом следующего декоратора.         /// </summary>         /// <typeparam name="TResult"> Тип возвращаемого значения. </typeparam>         /// <param name="lambda"> Выполняемый код. </param>         /// <returns></returns>         protected Task<TResult> ExecuteAsync<TResult>(Func<T, Task<TResult>> lambda)         {             return lambda(NextDelegate());         }          /// <summary>         ///     Выполнить код декоратора с вызовом следующего декоратора.         /// </summary>         /// <param name="lambda"> Выполняемый код. </param>         /// <returns></returns>         protected Task ExecuteAsync(Func<T, Task> lambda)         {             return lambda(NextDelegate());         }     }

Имея такой базовый класс, в декораторе, который его наследует, можно писать как то так

    public Task<Response> MethodAsync(Request request)     {         return ExecuteAsync(async next =>         {             // code before             var result = await next.MethodAsync(request);             // code after             return result;         });     }

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

    public Task<Response> MethodAsync(Request request)     {         return ExecuteAsync(next => next.MethodAsync(request));     }

Немного магии все же осталось. А именно — назначение свойства NextDelegate. Сходу не понятно, что это и как использовать, но опытный программист найдет, а неопытному надо 1 раз объяснить. Это как DbSet’ы в DbContext 🙂

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

В заключении хочу ничего не говорить 🙂

ссылка на оригинал статьи https://habr.com/ru/post/511190/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *