Wie vermeide ich den Wahnsinn eines Abhängigkeitsinjektionskonstruktors?

300

Ich finde, dass meine Konstruktoren so aussehen:

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

mit ständig wachsender Parameterliste. Da "Container" mein Abhängigkeitsinjektionscontainer ist, warum kann ich das nicht einfach so machen:

public MyClass(Container con)

für jede Klasse? Was sind die Nachteile? Wenn ich das mache, fühlt es sich an, als würde ich eine verherrlichte Statik verwenden. Bitte teilen Sie Ihre Gedanken zu IoC und Dependency Injection Madness mit.

JP Richardson
quelle
64
Warum kommst du am Container vorbei? Ich denke, Sie können IOC
Paul Creasey
33
Wenn Ihre Konstruktoren mehr oder mehr Parameter fordern, tun Sie möglicherweise zu viel in diesen Klassen.
Austin Salonen
38
So macht man keine Konstruktorinjektion. Objekte kennen den IoC-Container überhaupt nicht und sollten es auch nicht.
Duffymo
Sie können einfach einen leeren Konstruktor erstellen, in dem Sie den DI direkt aufrufen und nach dem benötigten Material fragen. Dadurch werden die Konstruktor-Madnes entfernt, aber Sie müssen sicherstellen, dass Sie eine DI-Schnittstelle verwenden. Falls Sie Ihr DI-System zur Hälfte der Entwicklungszeit ändern. Ehrlich gesagt ... niemand wird es auf diese Weise unterstützen, obwohl DI dies tut, um in Ihren Konstruktor zu injizieren. Doh
Piotr Kula

Antworten:

409

Sie haben Recht, wenn Sie den Container als Service Locator verwenden, handelt es sich mehr oder weniger um eine verherrlichte statische Fabrik. Aus vielen Gründen halte ich dies für ein Anti-Muster .

Einer der wunderbaren Vorteile von Constructor Injection besteht darin, dass Verstöße gegen das Prinzip der Einzelverantwortung offensichtlich werden.

In diesem Fall ist es an der Zeit, die Facade Services zu überarbeiten . Kurz gesagt, erstellen Sie eine neue, grobkörnigere Schnittstelle, die die Interaktion zwischen einigen oder allen feinkörnigen Abhängigkeiten verbirgt, die Sie derzeit benötigen.

Mark Seemann
quelle
8
+1 zur Quantifizierung des Refactoring-Aufwands in einem einzigen Konzept; genial :)
Ryan Emerle
46
wirklich? Sie haben gerade eine Indirektion erstellt, um diese Parameter in eine andere Klasse zu verschieben, aber sie sind immer noch da! nur komplizierter mit ihnen umzugehen.
unwiderlegbar
23
@irreputable: In dem entarteten Fall, in dem wir alle Abhängigkeiten in einen aggregierten Dienst verschieben, stimme ich zu, dass es sich nur um eine andere Indirektionsebene handelt, die keinen Nutzen bringt, sodass meine Wortwahl leicht abweicht. Der Punkt ist jedoch, dass wir nur einige der feinkörnigen Abhängigkeiten in einen Aggregatdienst verschieben. Dies begrenzt die Anzahl der Abhängigkeitspermutationen sowohl im neuen Aggregatdienst als auch für die zurückgelassenen Abhängigkeiten. Dies macht es einfacher, mit beiden umzugehen.
Mark Seemann
92
Die beste Bemerkung: "Einer der wunderbaren Vorteile von Constructor Injection ist, dass Verstöße gegen das Prinzip der Einzelverantwortung offensichtlich werden."
Igor Popov
2
@DonBox In diesem Fall können Sie Nullobjektimplementierungen schreiben, um die Rekursion zu stoppen. Nicht das, was Sie brauchen, aber der Punkt ist, dass die Konstruktorinjektion Zyklen nicht verhindert - es macht nur deutlich, dass sie da sind.
Mark Seemann
66

Ich denke nicht, dass Ihre Klassenkonstruktoren einen Verweis auf Ihre IOC-Containerperiode haben sollten. Dies stellt eine unnötige Abhängigkeit zwischen Ihrer Klasse und dem Container dar (die Art der Abhängigkeit, die das IOC zu vermeiden versucht!).

Ableitung
quelle
+1 Abhängig von einem IoC-Container ist es schwierig, diesen Container später zu ändern, ohne eine Menge Code in allen anderen Klassen zu ändern
Tseng
1
Wie würden Sie IOC implementieren, ohne Schnittstellenparameter auf den Konstruktoren zu haben? Lese ich deinen Beitrag falsch?
J Hunt
@J Hunt Ich verstehe deinen Kommentar nicht. Schnittstellenparameter sind für mich Parameter, die Schnittstellen für die Abhängigkeiten sind, dh wenn Ihr Abhängigkeitsinjektionscontainer initialisiert wird MyClass myClass = new MyClass(IDependency1 interface1, IDependency2 interface2)(Schnittstellenparameter). Dies hat nichts mit dem Beitrag von @ derivation zu tun, den ich so interpretiere, dass ein Abhängigkeitsinjektionscontainer sich nicht in seine Objekte injizieren sollte, dhMyClass myClass = new MyClass(this)
John Doe
25

Die Schwierigkeit, die Parameter zu übergeben, ist nicht das Problem. Das Problem ist, dass Ihre Klasse zu viel tut und mehr aufgeschlüsselt werden sollte.

Die Abhängigkeitsinjektion kann als Frühwarnung für zu große Klassen dienen, insbesondere aufgrund der zunehmenden Schmerzen beim Übergeben aller Abhängigkeiten.

Kyoryu
quelle
44
Korrigieren Sie mich, wenn ich falsch liege, aber irgendwann müssen Sie "alles zusammenkleben", und daher müssen Sie mehr als ein paar Abhängigkeiten dafür bekommen. Wenn Sie beispielsweise in der Ansichtsebene Vorlagen und Daten für diese erstellen, müssen Sie alle Daten aus verschiedenen Abhängigkeiten (z. B. "Dienste") abrufen und diese Daten dann in die Vorlage und auf den Bildschirm einfügen. Wenn meine Webseite 10 verschiedene Informationsblöcke enthält, benötige ich 10 verschiedene Klassen, um diese Daten bereitzustellen. Ich benötige also 10 Abhängigkeiten in meiner View / Template-Klasse?
Andrew
4

Ich stieß auf eine ähnliche Frage zur konstruktorbasierten Abhängigkeitsinjektion und wie komplex es wurde, alle Abhängigkeiten zu übergeben.

Ein Ansatz, den ich in der Vergangenheit verwendet habe, ist die Verwendung des Anwendungsfassadenmusters unter Verwendung einer Serviceschicht. Dies hätte eine grobe API. Wenn dieser Dienst von Repositorys abhängt, wird eine Setter-Injection der privaten Eigenschaften verwendet. Dies erfordert das Erstellen einer abstrakten Factory und das Verschieben der Logik zum Erstellen der Repositorys in eine Factory.

Detaillierten Code mit Erläuterungen finden Sie hier

Best Practices für IoC in komplexen Serviceschichten

samsur
quelle
3

Ich habe diesen ganzen Thread zweimal gelesen und ich denke, die Leute antworten mit dem, was sie wissen, nicht mit dem, was gefragt wird.

JPs ursprüngliche Frage sieht so aus, als würde er Objekte konstruieren, indem er einen Resolver und dann eine Reihe von Klassen sendet. Wir gehen jedoch davon aus, dass diese Klassen / Objekte selbst Dienste sind, die für die Injektion reif sind. Was ist, wenn sie nicht sind?

JP, wenn Sie DI nutzen möchten und den Ruhm des Mischens von Injektion mit Kontextdaten wünschen, spricht keines dieser Muster (oder angebliche "Anti-Muster") dies speziell an. Es läuft tatsächlich darauf hinaus, ein Paket zu verwenden, das Sie bei einem solchen Unterfangen unterstützt.

Container.GetSevice<MyClass>(someObject1, someObject2)

... dieses Format wird selten unterstützt. Ich glaube, dass die Schwierigkeit, eine solche Unterstützung zu programmieren, zusätzlich zu der miserablen Leistung, die mit der Implementierung verbunden wäre, sie für Open Source-Entwickler unattraktiv macht.

Aber es sollte getan werden, weil ich in der Lage sein sollte, eine Factory für MyClass zu erstellen und zu registrieren, und diese Factory sollte in der Lage sein, Daten / Eingaben zu empfangen, die nicht nur zum Zweck des Bestehens zu einem "Service" werden Daten. Wenn es bei "Anti-Pattern" um negative Konsequenzen geht, ist es sicherlich negativ, die Existenz künstlicher Servicetypen für die Übergabe von Daten / Modellen zu erzwingen (genau wie Sie das Gefühl haben, Ihre Klassen in einen Container zu packen. Der gleiche Instinkt gilt).

Es gibt jedoch Rahmenbedingungen, die helfen können, auch wenn sie etwas hässlich aussehen. Zum Beispiel Ninject:

Erstellen einer Instanz mit Ninject mit zusätzlichen Parametern im Konstruktor

Das ist für .NET, beliebt und nirgends so sauber wie es sein sollte, aber ich bin mir sicher, dass es in jeder Sprache, die Sie verwenden, etwas gibt.

Craig Brunetti
quelle
3

Das Injizieren des Containers ist eine Abkürzung, die Sie letztendlich bereuen werden.

Eine Überinjektion ist nicht das Problem, sondern in der Regel ein Symptom für andere strukturelle Mängel, insbesondere für die Trennung von Bedenken. Dies ist kein Problem, kann aber viele Ursachen haben, und was es so schwierig macht, dies zu beheben, ist, dass Sie sich manchmal gleichzeitig mit allen befassen müssen (denken Sie daran, Spaghetti zu entwirren).

Hier ist eine unvollständige Liste der Dinge, auf die Sie achten müssen

Schlechtes Domain-Design (aggregierte Roots… usw.)

Schlechte Trennung von Bedenken (Servicezusammensetzung, Befehle, Abfragen) Siehe CQRS und Event Sourcing.

ODER Mapper (seien Sie vorsichtig, diese Dinge können Sie in Schwierigkeiten bringen)

Modelle und andere DTOs anzeigen (Niemals eines wiederverwenden und versuchen, sie auf ein Minimum zu beschränken !!!!)

Marius
quelle
2

Problem :

1) Konstruktor mit ständig wachsender Parameterliste.

2) Wenn eine Klasse geerbt wird (Beispiel :), führt eine RepositoryBaseÄnderung der Konstruktorsignatur zu einer Änderung der abgeleiteten Klassen.

Lösung 1

Pass IoC Containerzum Konstruktor

Warum

  • Keine ständig wachsende Parameterliste mehr
  • Die Signatur des Konstruktors wird einfach

Warum nicht

  • Lässt Sie die Klasse eng mit dem IoC-Container verbinden. (Dies führt zu Problemen, wenn 1. Sie diese Klasse in anderen Projekten verwenden möchten, in denen Sie einen anderen IoC-Container verwenden. 2. Sie entscheiden, den IoC-Container zu ändern.)
  • Macht Ihre Klasse weniger beschreibend. (Sie können sich den Klassenkonstruktor nicht wirklich ansehen und sagen, was er zum Funktionieren benötigt.)
  • Die Klasse kann möglicherweise auf alle Dienste zugreifen.

Lösung 2

Erstellen Sie eine Klasse, die alle Dienste gruppiert und an den Konstruktor übergibt

 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;
    }
  }

Abgeleitete Klasse

  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;
      }
   }

Warum

  • Das Hinzufügen einer neuen Abhängigkeit zur Klasse wirkt sich nicht auf abgeleitete Klassen aus
  • Die Klasse ist unabhängig vom IoC-Container
  • Klasse ist beschreibend (in Bezug auf ihre Abhängigkeiten). Wenn Sie wissen möchten, von welcher Klasse Aabhängt, werden diese Informationen gemäß der Konvention gesammeltA.Dependency
  • Die Konstruktorsignatur wird einfach

Warum nicht

  • müssen zusätzliche Klasse erstellen
  • Service-Registrierung wird komplex (Sie müssen jede X.Dependencyseparat registrieren )
  • Konzeptionell dasselbe wie Bestehen IoC Container
  • ..

Lösung 2 ist jedoch nur eine rohe, wenn es solide Argumente dagegen gibt, wäre ein beschreibender Kommentar willkommen

tchelidze
quelle
1

Dies ist der Ansatz, den ich benutze

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; }


}

Hier ist ein grober Ansatz, wie die Injektionen durchgeführt und der Konstruktor nach dem Injizieren von Werten ausgeführt wird. Dies ist ein voll funktionsfähiges Programm.

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

    }
}

Ich arbeite gerade an einem Hobbyprojekt, das so funktioniert folgt https://github.com/Jokine/ToolProject/tree/Core

Ronijo
quelle
-8

Welches Abhängigkeitsinjektionsframework verwenden Sie? Haben Sie versucht, stattdessen eine Setter-basierte Injektion zu verwenden?

Der Vorteil der konstruktorbasierten Injektion besteht darin, dass sie für Java-Programmierer, die keine DI-Frameworks verwenden, natürlich aussieht. Sie benötigen 5 Dinge, um eine Klasse zu initialisieren, dann haben Sie 5 Argumente für Ihren Konstruktor. Der Nachteil ist, was Sie bemerkt haben, es wird unhandlich, wenn Sie viele Abhängigkeiten haben.

Mit Spring können Sie die erforderlichen Werte stattdessen mit Setzern übergeben und @required-Annotationen verwenden, um zu erzwingen, dass sie injiziert werden. Der Nachteil ist, dass Sie den Initialisierungscode vom Konstruktor auf eine andere Methode verschieben und Spring aufrufen müssen, nachdem alle Abhängigkeiten durch Markieren mit @PostConstruct eingefügt wurden. Ich bin mir bei anderen Frameworks nicht sicher, aber ich gehe davon aus, dass sie etwas Ähnliches tun.

Beides funktioniert, es ist eine Frage der Präferenz.

David W Crook
quelle
21
Der Grund für die Konstruktorinjektion besteht darin, Abhängigkeiten offensichtlich zu machen, nicht weil dies für Java-Entwickler natürlicher aussieht.
L-Four
8
Später Kommentar, aber diese Antwort brachte mich zum Lachen :)
Frederik Prijck
1
+1 für Setter-basierte Injektionen. Wenn ich Services und Repositorys in meiner Klasse definiert habe, ist es verdammt offensichtlich, dass es sich um Abhängigkeiten handelt. Ich muss keine massiven VB6-Konstruktoren schreiben und im Konstruktor dummen Code zuweisen. Es ist ziemlich offensichtlich, welche Abhängigkeiten von den erforderlichen Feldern bestehen.
Piotr Kula
Ab 2018 empfiehlt Spring offiziell, keine Setter-Injection zu verwenden, außer für Abhängigkeiten, die einen angemessenen Standardwert haben. Wie in, wenn die Abhängigkeit für die Klasse obligatorisch ist, wird die Konstruktorinjektion empfohlen. Siehe Diskussion über Setter gegen Ctor DI
John Doe