Fonctionnalité de bout en bout via des wrappers

Au cours du développement, nous rencontrons souvent une situation où, lors de l'exécution d'une logique métier, il est nécessaire d'écrire des journaux, des audits et d'envoyer des alertes. En général, implémentez des fonctionnalités de bout en bout.



Lorsque l'échelle de production est petite, vous ne pouvez pas être trop zélé et faire tout cela correctement dans les méthodes. Peu à peu, le constructeur du service commence à devenir envahi par les services entrants pour la mise en œuvre de BL et de fonctionnalités de bout en bout. Et voici ILogger, IAuditService, INotifiesSerice.

Je ne sais pas pour vous, mais je n’aime pas beaucoup d’injections et de méthodes volumineuses qui effectuent plusieurs actions à la fois.



Vous pouvez liquider n'importe quelle implémentation AOP sur le code. Dans la pile .NET, ces implémentations s'injectent dans votre application aux bons endroits, ressemblent à de la magie de niveau 80 et ont souvent des problèmes de frappe et de débogage.



J'ai essayé de trouver un terrain d'entente. Si ces problèmes ne vous ont pas épargné, bienvenue sous chat.



Divulgacher. En fait, j'ai pu résoudre un peu plus de problèmes que ce que j'ai décrit ci-dessus. Par exemple, je peux confier le développement BL à un développeur et suspendre la fonctionnalité de bout en bout et même la validation des données entrantes - à un autre en même temps .



Et les décorateurs et un add-on DI m'ont aidé avec cela. Quelqu'un dira en outre qu'il s'agit d'un proxy, je serai heureux d'en discuter dans les commentaires.



Alors qu'est-ce que je veux en tant que développeur?



  • Lors de la mise en œuvre de BL, ne vous laissez pas distraire par le fonctionnel gauche.
  • Pour pouvoir tester uniquement BL dans les tests unitaires. Et je n'aime pas faire 100 500 simulations pour désactiver toutes les fonctionnalités auxiliaires. 2-3 - d'accord, mais je ne veux pas.
  • Comprenez ce qui se passe sans avoir 7 travées sur votre front. :)
  • Soyez capable de gérer la durée de vie du service et de chacun de ses wrappers SÉPARÉMENT!


Qu'est-ce que je veux en tant que designer et chef d'équipe?



  • Être capable de décomposer les tâches de la manière la plus optimale et avec le moins de cohérence, afin qu'en même temps, il soit possible d'impliquer autant de développeurs que possible sur différentes tâches et en même temps pour qu'ils passent le moins de temps possible sur la recherche (si le développeur a besoin de développer un BL, et en parallèle réfléchir à quoi et comment sécuriser , il passera plus de temps sur la recherche. Et donc avec chaque morceau de BL. Il est beaucoup plus facile de saisir les enregistrements d'audit et de les entasser tout au long du projet).
  • Conservez l'ordre dans lequel le code est exécuté indépendamment de son développement.


Cette interface m'aidera avec cela.



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


Vous pouvez utiliser quelque chose comme ça
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;
    }
}




Ainsi, notre action ira plus loin.



wrapper1
    wrapper2
        service
    end wrapper2
end wrapper1


Mais attendez une minute. Ceci est déjà en POO et s'appelle l'héritage. : RÉ



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


Comme j'imaginais qu'une fonctionnalité de bout en bout supplémentaire apparaîtrait, qui devrait être mise en œuvre tout au long de l'application au milieu ou pour échanger celles existantes, les cheveux sur mon dos se tenaient debout.



Mais ma paresse n'est pas une bonne raison. La bonne raison est qu'il y aura de gros problèmes lors du test unitaire de la fonctionnalité dans les classes Wrapper1 et Wrapper2, alors que dans mon exemple NextDelegate peut simplement être simulé. De plus, le service et chaque wrapper ont leur propre ensemble d'outils qui sont injectés dans le constructeur, alors qu'avec l'héritage, le dernier wrapper doit avoir des outils inutiles pour les transmettre aux parents.



Donc, l'approche est acceptée, il reste à savoir où, comment et quand attribuer NextDelegate.



J'ai décidé que la solution la plus logique serait de faire cela là où j'enregistre des services. (Startup.sc, par défaut).



Voici à quoi cela ressemble dans la version de base.
            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;
            });




En général, toutes les exigences ont été satisfaites, mais un autre problème est apparu: l'imbrication.



Ce problème peut être résolu par la force brute ou la récursivité. Mais sous le capot. Extérieurement, tout doit paraître simple et compréhensible.



C'est ce que j'ai réussi à réaliser
            services.AddDecoratedScoped<IService, Service>(builder =>
            {
                builder.AddSingletonDecorator<Wrapper1>();
                builder.AddTransientDecorator<Wrapper2>();
                builder.AddScopedDecorator<Wrapper3>();
            });




Et ces méthodes d'extension m'ont aidé avec cela.



Et ces méthodes d'extension et le constructeur de décors m'ont aidé.
    /// <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);
        }
    }




Maintenant du sucre pour les fonctionnalistes



Maintenant du sucre pour les fonctionnalistes
    /// <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));
    }




Il reste encore un peu de magie. À savoir, le but de la propriété NextDelegate. On ne sait pas immédiatement ce que c'est et comment l'utiliser, mais un programmeur expérimenté le trouvera, mais un inexpérimenté doit l'expliquer une fois. C'est comme DbSets dans DbContext.



Je ne l'ai pas mis sur le hub git. Il n'y a pas beaucoup de code, il est déjà généralisé, vous pouvez donc le tirer directement d'ici.



En conclusion, je ne veux rien dire.



All Articles