Funcionalidade ponta a ponta via wrappers

Durante o desenvolvimento, frequentemente nos deparamos com uma situação em que, ao executar qualquer lógica de negócios, é necessário escrever logs, auditorias e enviar alertas. Em geral, implemente alguma funcionalidade ponta a ponta.



Quando a escala de produção é pequena, não dá para ser muito zeloso e fazer tudo isso certinho nos métodos. Gradualmente, o construtor de serviço começa a crescer coberto de serviços de entrada para a implementação de BL e funcionalidade ponta a ponta. E este é ILogger, IAuditService, INotifiesSerice.

Não sei sobre você, mas não gosto de muitas injeções e métodos grandes que realizam muitas ações ao mesmo tempo.



Você pode encerrar qualquer implementação AOP no código. Na pilha .NET, essas implementações injetam-se em seu aplicativo nos lugares certos, parecem mágicas de nível 80 e geralmente apresentam problemas de digitação e depuração.



Tentei encontrar um meio-termo. Se esses problemas não o pouparam, bem-vindo em gato.



Spoiler. Na verdade, consegui resolver um pouco mais problemas do que os descritos acima. Por exemplo, posso fornecer o desenvolvimento de BL para um desenvolvedor e suspender a funcionalidade ponta a ponta e até mesmo a validação de dados recebidos - para outro ao mesmo tempo .



E decoradores e um suplemento de DI me ajudaram com isso. Alguém ainda dirá que este é um proxy, terei prazer em discutir isso nos comentários.



Então, o que eu quero como desenvolvedor?



  • Ao implementar BL, não se distraia com o funcional esquerdo.
  • Ser capaz de testar apenas BL em testes de unidade. E eu não gosto de fazer 100.500 simulações para desabilitar todas as funcionalidades auxiliares. 2-3 - tudo bem, mas eu não quero.
  • Entenda o que está acontecendo sem ter 7 vãos em sua testa. :)
  • Ser capaz de gerenciar a vida útil do serviço e cada um de seus invólucros SEPARADAMENTE!


O que eu quero como designer e líder de equipe?



  • Ser capaz de decompor tarefas da maneira mais otimizada e com o mínimo de coerência, de modo que ao mesmo tempo seja possível envolver tantos desenvolvedores quanto possível em diferentes tarefas e ao mesmo tempo para que eles gastem o menos tempo possível em pesquisa (se o desenvolvedor precisar desenvolver um BL, e em paralelo pensar sobre o que e como proteger , ele gastará mais tempo em pesquisa. E assim, com cada parte do BL. É muito mais fácil pegar registros de auditoria e empilhá-los ao longo do projeto).
  • Manter a ordem em que o código é executado separadamente de seu desenvolvimento.


Essa interface vai me ajudar com isso.



    /// <summary>
    ///       .
    /// </summary>
    /// <typeparam name="T">  . </typeparam>
    public interface IDecorator<T>
    {
        /// <summary>
        ///        .
        /// </summary>
        Func<T> NextDelegate { get; set; }
    }


Você pode usar algo assim
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;
    }
}




Assim, nossa ação se aprofundará.



wrapper1
    wrapper2
        service
    end wrapper2
end wrapper1


Mas espere. Isso já está em OOP e é chamado de herança. : D



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


Como imaginei que surgisse uma funcionalidade ponta a ponta adicional, que teria de ser implementada em todo o aplicativo no meio, ou para trocar as existentes, fiquei com os cabelos em pé.



Mas minha preguiça não é um bom motivo. A boa razão é que haverá grandes problemas durante o teste de unidade da funcionalidade nas classes Wrapper1 e Wrapper2, enquanto no meu exemplo NextDelegate pode ser simplesmente simulado. Além disso, o serviço e cada wrapper possuem seu próprio conjunto de ferramentas que são injetadas no construtor, ao passo que, com a herança, o último wrapper deve ter ferramentas desnecessárias para passá-las aos pais.



Assim, a abordagem é aceita, resta descobrir onde, como e quando atribuir NextDelegate.



Decidi que a solução mais lógica seria fazer isso onde eu registro os serviços. (Startup.sc, padrão).



É assim que fica na versão básica.
            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;
            });




Em geral, todos os requisitos foram atendidos, mas apareceu outro problema - o aninhamento.



Este problema pode ser resolvido por força bruta ou recursão. Mas sob o capô. Exteriormente, tudo deve parecer simples e compreensível.



Isso é o que eu consegui alcançar
            services.AddDecoratedScoped<IService, Service>(builder =>
            {
                builder.AddSingletonDecorator<Wrapper1>();
                builder.AddTransientDecorator<Wrapper2>();
                builder.AddScopedDecorator<Wrapper3>();
            });




E esses métodos de extensão me ajudaram com isso.



E esses métodos de extensão e o construtor de cenário me ajudaram com isso.
    /// <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="TService">  . </typeparam>
    public class DecorationBuilder<TService>
    {
        private readonly List<ServiceDescriptor> _serviceDescriptors = new List<ServiceDescriptor>();

        /// <summary>
        ///       .
        /// </summary>
        public IReadOnlyCollection<ServiceDescriptor> ServiceDescriptors => _serviceDescriptors;

        /// <summary>
        ///      .
        /// </summary>
        /// <typeparam name="TDecorator">  . </typeparam>
        /// <param name="lifeTime">   . </param>
        public void AddDecorator<TDecorator>(ServiceLifetime lifeTime) where TDecorator : TService, IDecorator<TService>
        {
            var container = new ServiceDescriptor(typeof(TDecorator), typeof(TDecorator), lifeTime);
            _serviceDescriptors.Add(container);
        }

        /// <summary>
        ///          Scoped.
        /// </summary>
        /// <typeparam name="TDecorator">  . </typeparam>
        public void AddScopedDecorator<TDecorator>() where TDecorator : TService, IDecorator<TService>
        {
            AddDecorator<TDecorator>(ServiceLifetime.Scoped);
        }

        /// <summary>
        ///          Singleton.
        /// </summary>
        /// <typeparam name="TDecorator">  . </typeparam>
        public void AddSingletonDecorator<TDecorator>() where TDecorator : TService, IDecorator<TService>
        {
            AddDecorator<TDecorator>(ServiceLifetime.Singleton);
        }

        /// <summary>
        ///          Transient.
        /// </summary>
        /// <typeparam name="TDecorator">  . </typeparam>
        public void AddTransientDecorator<TDecorator>() where TDecorator : TService, IDecorator<TService>
        {
            AddDecorator<TDecorator>(ServiceLifetime.Transient);
        }
    }




Agora um pouco de açúcar para funcionalistas



Agora, um pouco de açúcar para funcionalistas
    /// <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));
    }




Ainda resta um pouco de magia. Ou seja, o objetivo da propriedade NextDelegate. Não está imediatamente claro o que é e como usá-lo, mas um programador experiente vai descobrir, mas um inexperiente precisa explicar uma vez. É como DbSets em DbContext.



Eu não coloquei no hub git. Não há muito código, já está generalizado, então você pode puxá-lo daqui.



Concluindo, não quero dizer nada.



All Articles