-2
votes

Injection de dépendances: comment intégrer les valeurs reçues des paramètres de méthode dans les dépendances que je souhaite résoudre en tant que paramètres de constructeur?

S'il vous plaît, soyez patient - c'est une question compliquée, et je l'ai simplifiée autant que possible. (J'utilise l'API Web ASP.NET et AutoFac et j'ai omis un tas de configuration par souci de concision.)

Mon objectif est de maximiser la mesure dans laquelle l'injection de dépendances est gérée par un framework DI, dans une situation où toutes les dépendances de certains objets ne peuvent être connues avant l'exécution.

Nos joueurs sont:

  • une classe CONTROLLER qui accepte les requêtes Web, le point d'entrée de l'application - l'entrée comprend repoName

  • une classe repoName , une fabrique qui résout un repoName en un repoName spécifique. Voici sa mise en œuvre:

public class DIValuesController : ApiController
{
    private readonly IRepositoryResolver repoSource;
    private readonly IBusinessLogic businessLogic;

    public DIValuesController(
        IRepositoryResolver repoSource,
        IBusinessLogic businessLogic)
    {
        this.repoSource = repoSource;
        this.businessLogic = businessLogic;
    }

    public IEnumerable<string> Get(string repoName)
    {
        var repo = repoSource.Resolve(repoName);
        /* ...handwaving to integrate repo into businessLogic... */
        return businessLogic.Compute();
    }
}
  • une classe REPOSITORY (représentant un DB ou autre). Voici sa mise en œuvre:
public class PureDIController : ApiController
{
    public ProceduralValuesController() { }

    public IEnumerable<string> Get(string repoName)
    {
        IRepositoryResolver repoSource = new RepositoryResolver();
        IRepository repo = repoSource.Resolve(repoName);
        IBusinessLogic businessLogic = new BusinessLogic(repo);
        return businessLogic.Compute();
    }
}
  • une classe BUSINESS LOGIC qui calcule un résultat. La classe BUSINESS LOGIC dépend d'un référentiel unique; il ne sait rien de plusieurs référentiels ou du résolveur de référentiel car il ne les concerne pas. Voici sa mise en œuvre:
    public class BusinessLogic : IBusinessLogic
    {
        private readonly IRepository repository;

        public BusinessLogic(IRepository repository)
        {
            this.repository = repository;
        }

        public string[] Compute()
        {
            return repository.Get();
        }
    }

Sur le plan de la procédure, ce que j'essaie d'accomplir (dans cet exemple de jouet dépouillé) est très simple. Voici un exemple d'implémentation du contrôleur:

Réponse proposée n ° 1 - DI pur (sans contenant)

    public class Repository : IRepository
    {
        private readonly Input input; // proxy for connection string or other identifying information
    
        public Repository (Input input)
        {
            this.input = input;
        }

        public string[] Get()
        {
            return new[] { "I", "am", "a", input.RepoName };
        }
    }

... et cela fonctionne, mais évidemment je n'utilise pas de conteneur DI ici. Lors de l'utilisation de la DI pure comme celle-ci, les modifications apportées à un joueur ont tendance à avoir des effets d'entraînement au-delà de ses collaborateurs immédiats et potentiellement à travers de nombreuses couches (non représentées ici); J'ai l'impression que déplacer cette logique de composition dans un conteneur DI réduira beaucoup ce type de refactoring. C'est la proposition de valeur de cette question.

Cependant, lorsque j'essaie de réécrire cette classe en utilisant l'injection de dépendances, je rencontre un problème: la LOGIQUE D'AFFAIRES dépend du REPOSITORY, elle ne peut donc pas être résolue par un conteneur DI pré-créé. Par conséquent, je ne peux pas résoudre le commentaire agitant la main ici:

    public class RepositoryResolver : IRepositoryResolver
    {
        public IRepository Resolve(string repoName)
        {
            return new Repository(new Input { RepoName = repoName });
        }
    }

... car IBusinessLogic ne peut pas être résolu au moment où le contrôleur est instancié.

J'ai développé plusieurs solutions possibles, et je les ajouterai comme réponses potentielles. Cependant, je n'aime aucun d'entre eux, d'où le post. ¯_ (ム„) _ / ¯ Veuillez me surprendre avec quelque chose auquel je n'ai pas encore pensé!


0 commentaires

5 Réponses :


0
votes

Réponse n ° 2 - Passez les paramètres si nécessaire

Cette solution abandonne la prémisse originale selon laquelle nous pouvons convertir avec succès un paramètre de méthode en paramètre de constructeur. Au lieu de cela, il suppose que le repoName (ou un différenciateur équivalent) devra être passé dans toute fonction qui nécessite ces informations. Voici un exemple d'implémentation possible du contrôleur (notez que BusinessLogic nécessite désormais un paramètre supplémentaire):

public class BusinessLogic : IBusinessLogic
{
    private readonly IRepositoryResolver repositoryResolver;

    public BusinessLogic(IRepository repositoryResolver)
    {
        this.repositoryResolver = repositoryResolver;
    }

    public string[] Compute(string repoName)
    {
        var repo = repositoryResolver.Resolve(repoName);
        return repo.Get();
    }
}

Et voici la nouvelle implémentation de BusinessLogic :

public class ParameterPassingController : ApiController
{
    private readonly IBusinessLogic businessLogic;

    public ParameterPassingController(
        IBusinessLogic businessLogic)
    {
        this.businessLogic = businessLogic;
    }

    public IEnumerable<string> Get(string repoName)
    {
        return businessLogic.Compute(repoName);
    }
}

Cette solution semble très gênante car elle modifie la classe BusinessLogic pour qu'elle dépende d'un objet moins direct qu'auparavant. Les dépendances d'une classe doivent être déterminées par la classe et non par les besoins de l'appelant. La classe BusinessLogic était meilleure qu'avant, et toute solution qui nous oblige à la compliquer n'est probablement pas la bonne.


0 commentaires

-1
votes

Réponse n ° 3 - Introduire des résolveurs supplémentaires

Cette solution ajoute un BUSINESS LOGIC RESOLVER, en utilisant le même modèle que le REPOSITORY RESOLVER. (Modèle d'usine?)

public class BusinessLogicResolverController : ApiController
{
    private readonly IRepositoryResolver repoSource;
    private readonly IBusinessLogicResolver businessLogicSource;

    public BusinessLogicResolverController(
        IRepositoryResolver repoSource,
        IBusinessLogicResolver businessLogicSource)
    {
        this.repoSource = repoSource;
        this.businessLogicSource = businessLogicSource;
    }

    public IEnumerable<string> Get(string repoName)
    {
        var repo = repoSource.Resolve(repoName);
        var businessLogic = businessLogicSource.Resolve(repo);
        return businessLogic.Compute();
    }
}

Ce que je n'aime pas à ce sujet, c'est que s'il y a beaucoup de classes qui dépendent d'un seul IRepository (et dans mon exemple non trivial, il y en a beaucoup), je dois créer un résolveur pour chacune d'elles. Cela complique également d'autres choses que les conteneurs DI peuvent aider, comme les applications de décorateur et autres.


0 commentaires

-1
votes

Réponse # 4 - Introduire le couplage temporel

Cette solution remplace l'implémentation enregistrée d'IRepository par une classe qui implémente également IRepositoryManager, lui permettant d'être pointé vers le référentiel approprié au moment de l'exécution. Voici à quoi ressemble le contrôleur maintenant:

        repoManager.Set(repoName);
        return businessLogic.Compute();
        

... et voici l'implémentation d'IRepositoryManager:

public class RepositoryManager : IRepositoryManager, IRepository
{
    private readonly IRepositoryResolver resolver;
    private IRepository repo = null;

    public RepositoryManager(IRepositoryResolver resolver)
    {
        this.resolver = resolver;
    }

    void IRepositoryManager.Set(string repoName)
    {
        this.repo = resolver.Resolve(repoName);
    }

    string[] IRepository.Get()
    {
        if (repo == null)
            throw new InvalidOperationException($"{nameof(IRepositoryManager.Set)} must be called first.");
        else
            return repo.Get();
    }
}

Cette solution permet certainement au contrôleur de rester petit, mais d'après mon expérience, le couplage temporel fait presque toujours plus mal que cela n'aide. De plus, il n'est pas clair que ces deux lignes du contrôleur aient quelque chose à voir l'une avec l'autre:

public class TemporallyCoupledController : ApiController
{
    private readonly IRepositoryManager repoManager;
    private readonly IBusinessLogic businessLogic;

    public TemporallyCoupledController(
        IRepositoryManager repoManager,
        IBusinessLogic businessLogic)
    {
        this.repoManager = repoManager;
        this.businessLogic = businessLogic;
    }

    public IEnumerable<string> Get(string repoName)
    {
        repoManager.Set(repoName);
        return businessLogic.Compute();
    }
}

... mais évidemment ils le font. C'est donc une très mauvaise solution.


0 commentaires

-1
votes

Réponse n ° 5 - Injectez la dépendance basée sur les paramètres à l'intérieur du contrôleur

Cette solution fait du contrôleur une partie de la racine de la composition, plutôt que de le résoudre par injection de dépendances. (La ligne où le conteneur construit est récupéré peut être effectuée d'une autre manière, mais ce n'est pas la partie importante - l'idée principale est que nous devons accéder à sa méthode BeginLifetimeScope directement après avoir les paramètres d'entrée.)

public class SelfAwareDIController : ApiController
{
    public SelfAwareDIController() { }

    public IEnumerable<string> Get(string repoName)
    {
        var container = (AutofacWebApiDependencyResolver)GlobalConfiguration.Configuration.DependencyResolver;

        using (var scope = container.Container.BeginLifetimeScope(builder =>
            builder.RegisterInstance(new Input { RepoName = repoName }).AsSelf()))
        {
            var businessLogic = scope.Resolve<IBusinessLogic>();
            return businessLogic.Compute();
        }
    }
}

Cette solution évite le couplage temporel (puisque businessLogic ne peut pas exister en dehors de la durée de vie où il peut être résolu). Cela nous permet également de supprimer le REPOSITORY RESOLVER; J'étais déjà mal à l'aise avec REPOSITORY RESOLVER car c'est une extension de la racine de composition qui interfère avec la gestion centralisée de la création d'objets par le conteneur DI. Un inconvénient est qu'il déplace une partie du code lié au conteneur dans le contrôleur plutôt que de tout garder dans la configuration du service. (Encore une fois, dans un exemple non trivial, il peut y avoir de nombreux contrôleurs qui doivent implémenter une logique similaire.) Cela empêche également le contrôleur lui-même d'être instancié par le conteneur DI (ce que vous pouvez faire avec le package AutoFac.WebApi2 ). Pourtant, parce que cela restreint la connaissance de ce nouveau contexte au contrôleur (et supprime la nécessité d'avoir une classe d'usine), c'est probablement la moins répréhensible des solutions que j'ai identifiées.


0 commentaires

0
votes

Encore une autre possibilité - transmettre IBusinessLogic au Controller non pas en tant qu'instance, mais en tant qu'usine (c'est-à-dire Func <string, IBusinessLogic>) et dans l'usine Methode Compute Fall avec repoName. Voir, par exemple: https://autofaccn.readthedocs.io/en/latest/advanced/delegate-factories.html


1 commentaires

C'est une fonctionnalité intéressante, mais je ne trouve pas de moyen de l'implémenter qui ne conduise pas à un ou plusieurs des problèmes avec la solution déjà publiée.