Comment éviter la folie des constructeurs d'injection de dépendance?


300

Je trouve que mes constructeurs commencent à ressembler à ceci:

public MyClass(Container con, SomeClass1 obj1, SomeClass2, obj2.... )

avec une liste de paramètres toujours croissante. Puisque "Container" est mon conteneur d'injection de dépendances, pourquoi ne puis-je pas simplement faire ceci:

public MyClass(Container con)

pour chaque classe? Quels sont les inconvénients? Si je fais cela, j'ai l'impression d'utiliser une statique glorifiée. Veuillez partager vos réflexions sur l'IoC et la folie de l'injection de dépendance.


64
pourquoi passez-vous le conteneur? Je pense que vous pouvez mal comprendre le CIO
Paul Creasey

33
Si vos constructeurs demandent plus ou plus de paramètres, vous en faites peut-être trop dans ces classes.
Austin Salonen

38
Ce n'est pas comme ça qu'on fait l'injection de constructeur. Les objets ne connaissent pas du tout le conteneur IoC, ils ne devraient pas non plus.
duffymo

Vous pouvez simplement créer un constructeur vide, dans lequel vous appelez directement le DI pour demander ce dont vous avez besoin. Cela supprimera le constructeur madnes mais vous devez vous assurer que vous utilisez une interface DI .. au cas où vous changeriez votre système DI à mi-chemin du développement. Honnêtement .. personne ne le fera de cette façon, même si c'est ce que fait DI pour injecter dans votre constructeur. doh
Piotr Kula

Réponses:


410

Vous avez raison de dire que si vous utilisez le conteneur comme localisateur de service, c'est plus ou moins une fabrique statique glorifiée. Pour de nombreuses raisons, je considère cela comme un anti-modèle .

L'un des merveilleux avantages de Constructor Injection est qu'il rend les violations du principe de responsabilité unique manifestement évidentes.

Lorsque cela se produit, il est temps de refactoriser les services de façade . En bref, créez une nouvelle interface plus grossière qui masque l'interaction entre certaines ou toutes les dépendances fines dont vous avez actuellement besoin.


8
+1 pour quantifier l'effort de refactorisation en un seul concept; génial :)
Ryan Emerle

46
pour de vrai? vous venez de créer une indirection pour déplacer ces paramètres dans une autre classe, mais ils sont toujours là! juste plus compliqué de les gérer.
irréputable

23
@irreputable: Dans le cas dégénéré où nous déplaçons toutes les dépendances dans un service d'agrégation, je conviens que c'est juste un autre niveau d'indirection qui ne présente aucun avantage, donc mon choix de mots était légèrement décalé. Cependant, le fait est que nous ne déplaçons que certaines des dépendances à granularité fine dans un service d'agrégation. Cela limite le nombre de permutations de dépendance à la fois dans le nouveau service d'agrégation et pour les dépendances laissées pour compte. Cela rend les deux plus simples à gérer.
Mark Seemann

92
La meilleure remarque: "L'un des merveilleux avantages de l'injection de constructeur est qu'il rend les violations du principe de responsabilité unique manifestement évidentes."
Igor Popov

2
@DonBox Dans ce cas, vous pouvez écrire des implémentations d'objets nuls pour arrêter la récursivité. Pas ce dont vous avez besoin, mais le fait est que l'injection de constructeur n'empêche pas les cycles - cela indique simplement qu'ils sont là.
Mark Seemann

66

Je ne pense pas que vos constructeurs de classe devraient avoir une référence à votre période de conteneur IOC. Cela représente une dépendance inutile entre votre classe et le conteneur (le type de dépendance qu'IOC essaie d'éviter!).


+1 En fonction d'un conteneur IoC, il est difficile de changer ce conteneur plus tard sans changer de groupe de code dans toutes les autres classes
Tseng

1
Comment implémenteriez-vous IOC sans avoir de paramètres d'interface sur les constructeurs? Ai-je mal lu votre message?
J Hunt

@J Hunt Je ne comprends pas votre commentaire. Pour moi, les paramètres d'interface signifient des paramètres qui sont des interfaces pour les dépendances, c'est-à-dire si votre conteneur d'injection de dépendance s'initialise MyClass myClass = new MyClass(IDependency1 interface1, IDependency2 interface2)(paramètres d'interface). Ceci n'est pas lié au post de @ derivation, que j'interprète comme disant qu'un conteneur d'injection de dépendance ne devrait pas s'injecter dans ses objets, c'est-à-direMyClass myClass = new MyClass(this)
John Doe

25

La difficulté de passer les paramètres n'est pas le problème. Le problème est que votre classe en fait trop et devrait être décomposée davantage.

L'injection de dépendance peut servir d'alerte précoce pour les classes devenant trop grandes, en particulier en raison de la douleur croissante de passer dans toutes les dépendances.


44
Corrigez-moi si je me trompe, mais à un moment donné, vous devez «tout coller ensemble», et donc vous devez obtenir plus de quelques dépendances pour cela. Par exemple, dans la couche View, lorsque vous créez des modèles et des données pour eux, vous devez récupérer toutes les données de diverses dépendances (par exemple, les «services»), puis mettre toutes ces données dans le modèle et à l'écran. Si ma page Web contient 10 «blocs» d'informations différents, j'ai donc besoin de 10 classes différentes pour me fournir ces données. J'ai donc besoin de 10 dépendances dans ma classe View / Template?
Andrew

4

Je suis tombé sur une question similaire à propos de l'injection de dépendances basée sur les constructeurs et à quel point il était difficile de passer dans toutes les dépendances.

L'une des approches que j'ai utilisées dans le passé consiste à utiliser le modèle de façade d'application à l'aide d'une couche de service. Cela aurait une API grossière. Si ce service dépend de référentiels, il utiliserait une injection de setter des propriétés privées. Cela nécessite de créer une usine abstraite et de déplacer la logique de création des référentiels dans une usine.

Le code détaillé avec explication peut être trouvé ici

Meilleures pratiques pour l'IoC dans la couche de service complexe


3

J'ai lu tout ce fil, deux fois, et je pense que les gens répondent par ce qu'ils savent, pas par ce qui est demandé.

La question d'origine de JP semble être la construction d'objets en envoyant un résolveur, puis un tas de classes, mais nous supposons que ces classes / objets sont eux-mêmes des services, mûrs pour l'injection. Et s'ils ne le sont pas?

JP, si vous cherchez à tirer parti de la DI et que vous désirez la gloire de mélanger l'injection avec des données contextuelles, aucun de ces modèles (ou supposés "anti-modèles") ne traite spécifiquement de cela. Cela revient en fait à utiliser un package qui vous soutiendra dans une telle entreprise.

Container.GetSevice<MyClass>(someObject1, someObject2)

... ce format est rarement pris en charge. Je crois que la difficulté de programmer un tel support, ajoutée aux performances misérables qui seraient associées à la mise en œuvre, le rend peu attrayant pour les développeurs open source.

Mais cela devrait être fait, car je devrais pouvoir créer et enregistrer une usine pour MyClass'es, et cette usine devrait pouvoir recevoir des données / entrées qui ne sont pas poussées à devenir un "service" juste pour le plaisir de passer Les données. Si «l'anti-modèle» concerne des conséquences négatives, alors forcer l'existence de types de services artificiels pour transmettre des données / modèles est certainement négatif (comparable à votre sentiment de regrouper vos classes dans un conteneur. Le même instinct s'applique).

Il y a un cadre qui peut aider, même s'il a l'air un peu moche. Par exemple, Ninject:

Création d'une instance à l'aide de Ninject avec des paramètres supplémentaires dans le constructeur

C'est pour .NET, est populaire et n'est toujours pas aussi propre qu'il devrait l'être, mais je suis sûr qu'il y a quelque chose dans la langue que vous choisissez d'utiliser.


3

L'injection du conteneur est un raccourci que vous regretterez finalement.

La surinjection n'est pas le problème, elle est généralement le symptôme d'autres défauts structurels, notamment la séparation des préoccupations. Ce n'est pas un problème, mais il peut avoir de nombreuses sources et ce qui rend cela si difficile à résoudre, c'est que vous devrez les traiter tous, parfois en même temps (pensez à démêler les spaghettis).

Voici une liste incomplète des choses à surveiller

Mauvaise conception de domaine (racine agrégée…. Etc.)

Mauvaise séparation des préoccupations (composition du service, commandes, requêtes) Voir CQRS et Event Sourcing.

OU Mappeurs (attention, ces choses peuvent vous causer des ennuis)

Afficher les modèles et autres DTO (ne jamais en réutiliser un et essayer de les garder au minimum !!!!)


2

Problème:

1) Constructeur avec une liste de paramètres toujours croissante.

2) Si la classe est héritée (Ex RepositoryBase:), la modification de la signature du constructeur entraîne la modification des classes dérivées.

Solution 1

Passer IoC Containerau constructeur

Pourquoi

  • Plus de liste de paramètres sans cesse croissante
  • La signature du constructeur devient simple

Pourquoi pas

  • Rend votre classe étroitement couplée au conteneur IoC. (Cela pose des problèmes lorsque 1. vous souhaitez utiliser cette classe dans d'autres projets où vous utilisez un conteneur IoC différent. 2. vous décidez de changer de conteneur IoC)
  • Rend votre classe moins descriptive. (Vous ne pouvez pas vraiment regarder le constructeur de classe et dire ce dont il a besoin pour fonctionner.)
  • La classe peut accéder potentiellement à tous les services.

Solution 2

Créez une classe qui regroupe tous les services et passez-la au constructeur

 public abstract class EFRepositoryBase 
 {
    public class Dependency
    {
        public DbContext DbContext { get; }
        public IAuditFactory AuditFactory { get; }

         public Dependency(
            DbContext dbContext,
            IAuditFactory auditFactory)
        {
            DbContext = dbContext;
            AuditFactory = auditFactory;
        }
    }

    protected readonly DbContext DbContext;        
    protected readonly IJobariaAuditFactory auditFactory;

    protected EFRepositoryBase(Dependency dependency)
    {
        DbContext = dependency.DbContext;
        auditFactory= dependency.JobariaAuditFactory;
    }
  }

Classe dérivée

  public class ApplicationEfRepository : EFRepositoryBase      
  {
     public new class Dependency : EFRepositoryBase.Dependency
     {
         public IConcreteDependency ConcreteDependency { get; }

         public Dependency(
            DbContext dbContext,
            IAuditFactory auditFactory,
            IConcreteDependency concreteDependency)
        {
            DbContext = dbContext;
            AuditFactory = auditFactory;
            ConcreteDependency = concreteDependency;
        }
     }

      IConcreteDependency _concreteDependency;

      public ApplicationEfRepository(
          Dependency dependency)
          : base(dependency)
      { 
        _concreteDependency = dependency.ConcreteDependency;
      }
   }

Pourquoi

  • L'ajout d'une nouvelle dépendance à la classe n'affecte pas les classes dérivées
  • La classe est indépendante du conteneur IoC
  • La classe est descriptive (sous l'aspect de ses dépendances). Par convention, si vous voulez savoir de quelle classe Adépend, cette information est accumulée dansA.Dependency
  • La signature du constructeur devient simple

Pourquoi pas

  • besoin de créer une classe supplémentaire
  • l'inscription au service devient complexe (vous devez vous enregistrer X.Dependencyséparément)
  • Conceptuellement identique au passage IoC Container
  • ..

La solution 2 n'est cependant qu'une solution brute, s'il existe un argument solide contre elle, un commentaire descriptif serait apprécié


1

C'est l'approche que j'utilise

public class Hero
{

    [Inject]
    private IInventory Inventory { get; set; }

    [Inject]
    private IArmour Armour { get; set; }

    [Inject]
    protected IWeapon Weapon { get; set; }

    [Inject]
    private IAction Jump { get; set; }

    [Inject]
    private IInstanceProvider InstanceProvider { get; set; }


}

Voici une approche grossière pour effectuer les injections et exécuter le constructeur après avoir injecté des valeurs. Il s'agit d'un programme entièrement fonctionnel.

public class InjectAttribute : Attribute
{

}


public class TestClass
{
    [Inject]
    private SomeDependency sd { get; set; }

    public TestClass()
    {
        Console.WriteLine("ctor");
        Console.WriteLine(sd);
    }
}

public class SomeDependency
{

}


class Program
{
    static void Main(string[] args)
    {
        object tc = FormatterServices.GetUninitializedObject(typeof(TestClass));

        // Get all properties with inject tag
        List<PropertyInfo> pi = typeof(TestClass)
            .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public)
            .Where(info => info.GetCustomAttributes(typeof(InjectAttribute), false).Length > 0).ToList();

        // We now happen to know there's only one dependency so we take a shortcut just for the sake of this example and just set value to it without inspecting it
        pi[0].SetValue(tc, new SomeDependency(), null);


        // Find the right constructor and Invoke it. 
        ConstructorInfo ci = typeof(TestClass).GetConstructors()[0];
        ci.Invoke(tc, null);

    }
}

Je travaille actuellement sur un projet de passe-temps qui fonctionne comme ceci https://github.com/Jokine/ToolProject/tree/Core


-8

Quel cadre d'injection de dépendance utilisez-vous? Avez-vous plutôt essayé d'utiliser l'injection basée sur setter?

L'avantage de l'injection basée sur le constructeur est qu'il semble naturel pour les programmeurs Java qui n'utilisent pas de frameworks DI. Vous avez besoin de 5 choses pour initialiser une classe, puis vous avez 5 arguments pour votre constructeur. L'inconvénient est ce que vous avez remarqué, cela devient difficile à gérer lorsque vous avez beaucoup de dépendances.

Avec Spring, vous pouvez transmettre les valeurs requises aux setters à la place et vous pouvez utiliser les annotations @required pour faire en sorte qu'elles soient injectées. L'inconvénient est que vous devez déplacer le code d'initialisation du constructeur vers une autre méthode et demander à Spring d'appeler après toutes les dépendances en le marquant avec @PostConstruct. Je ne suis pas sûr des autres cadres mais je suppose qu'ils font quelque chose de similaire.

Les deux méthodes fonctionnent, c'est une question de préférence.


21
La raison de l'injection de constructeur est de rendre les dépendances évidentes, non pas parce qu'elles semblent plus naturelles pour les développeurs java.
L-Four

8
Commentaire tardif, mais cette réponse m'a fait rire :)
Frederik Prijck

1
+1 pour les injections basées sur setter. Si j'ai des services et des référentiels définis dans ma classe, c'est sacrément évident que ce sont des dépendances .. Je n'ai pas besoin d'écrire d'énormes constructeurs à la recherche de VB6, et de faire du code d'assignation stupide dans le constructeur. C'est assez évident quelles sont les dépendances sur les champs requis.
Piotr Kula

Selon 2018, Spring recommande officiellement de ne pas utiliser l'injection de setter, sauf pour les dépendances qui ont une valeur par défaut raisonnable. Comme dans, si la dépendance est obligatoire pour la classe, l'injection de constructeur est recommandée. Voir la discussion sur setter vs ctor DI
John Doe
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.