Русский
Русский
English
Статистика
Реклама

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

При разработке мы не редко сталкиваемся с ситуацией, когда при выполнении какой-либо бизнес-логики требуется записать логи, аудиты, разослать оповещения. В общем реализовать некоторый сквозной функционал.
Когда масштабы производства небольшие, можно особо не усердствовать и все это делать прямо в методах. Постепенно, конструктор сервиса начинает обрастать входящими сервисами для выполнения БЛ и сквозного функционала. А это уже 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


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

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 :)

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

В заключении хочу ничего не говорить :)
Источник: habr.com
К списку статей
Опубликовано: 16.07.2020 10:19:49
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Net

C

Net core

Dependency injection

Aop

Категории

Последние комментарии

© 2006-2020, personeltest.ru