Praktiken der Abhängigkeitsinjektion / IoC-Container beim Schreiben von Frameworks

18

Ich habe verschiedene IoC-Container (Castle.Windsor, Autofac, MEF usw.) für .Net in einer Reihe von Projekten verwendet. Ich habe festgestellt, dass sie häufig missbraucht werden und eine Reihe von schlechten Praktiken fördern.

Gibt es etablierte Praktiken für die Verwendung von IoC-Containern, insbesondere bei der Bereitstellung einer Plattform / eines Frameworks? Mein Ziel als Framework-Schreiber ist es, Code so einfach und benutzerfreundlich wie möglich zu gestalten. Ich würde lieber eine Codezeile schreiben, um ein Objekt zu konstruieren, als zehn oder sogar nur zwei.

Ein paar Code-Gerüche, die mir aufgefallen sind und für die ich keine guten Vorschläge habe:

  1. Große Anzahl von Parametern (> 5) für Konstruktoren. Das Erstellen von Diensten ist in der Regel komplex. Alle Abhängigkeiten werden über den Konstruktor eingefügt - trotz der Tatsache, dass die Komponenten selten optional sind (außer vielleicht beim Testen).

  2. Mangel an privaten und internen Klassen; Dies mag eine spezielle Einschränkung der Verwendung von C # und Silverlight sein, aber ich bin daran interessiert, wie es gelöst wird. Es ist schwierig zu sagen, was eine Framework-Schnittstelle ist, wenn alle Klassen öffentlich sind. Es ermöglicht mir den Zugriff auf private Teile, die ich wahrscheinlich nicht berühren sollte.

  3. Kopplung des Objektlebenszyklus an den IoC-Container. Es ist oft schwierig, die zum Erstellen von Objekten erforderlichen Abhängigkeiten manuell zu erstellen. Der Objektlebenszyklus wird zu oft vom IoC-Framework verwaltet. Ich habe Projekte gesehen, bei denen die meisten Klassen als Singletons registriert sind. Sie haben einen Mangel an expliziter Kontrolle und sind auch gezwungen, die Interna zu verwalten (dies bezieht sich auf den obigen Punkt, alle Klassen sind öffentlich und Sie müssen sie injizieren).

Das .Net-Framework verfügt beispielsweise über viele statische Methoden. wie z. B. DateTime.UtcNow. Oft habe ich dies als Konstruktionsparameter verpackt und injiziert gesehen.

Je nach konkreter Implementierung ist mein Code nur schwer zu testen. Das Injizieren einer Abhängigkeit erschwert die Verwendung meines Codes - insbesondere, wenn die Klasse viele Parameter enthält.

Wie stelle ich sowohl eine testbare als auch eine benutzerfreundliche Schnittstelle zur Verfügung? Was sind die Best Practices?

Dave Hillier
quelle
1
Welche schlechten Praktiken fördern sie? Für Privatunterricht sollten Sie nicht viele benötigen, wenn Sie eine gute Kapselung haben. Zum einen sind sie viel schwerer zu testen.
Aaronaught
Err, 1 und 2 sind die schlechten Praktiken. Große Konstruktoren und keine Kapselung, um eine minimale Schnittstelle zu erstellen? Auch sagte ich privat und intern (Interna sichtbar verwenden, um zu testen). Ich habe noch nie ein Framework verwendet, das mich zur Verwendung eines bestimmten IoC-Containers zwingt.
Dave Hillier
2
Ist es möglich, dass ein Anzeichen dafür, dass viele Parameter für Ihre Klassenkonstruktoren benötigt werden, der Code-Geruch ist und das DIP dies nur deutlicher macht?
Dreza
3
Die Verwendung eines bestimmten IoC-Containers in einem Framework ist im Allgemeinen keine gute Praxis. Die Verwendung der Abhängigkeitsinjektion ist eine gute Praxis. Kennen Sie den Unterschied?
Aaronaught
1
@Aaronaught (zurück von den Toten). Die Verwendung von "Abhängigkeitsinjektion ist eine gute Praxis" ist falsch. Die Abhängigkeitsinjektion ist ein Tool, das nur bei Bedarf verwendet werden sollte. Die ständige Verwendung von Dependency Injection ist eine schlechte Praxis.
Dave Hillier

Antworten:

27

Das einzige legitime Anti-Pattern für die Abhängigkeitsinjektion, das mir bekannt ist, ist das Service Locator- Pattern, bei dem es sich um ein Anti-Pattern handelt, wenn ein DI-Framework dafür verwendet wird.

Alle anderen so genannten DI-Anti-Patterns, von denen ich hier oder anderswo gehört habe, sind nur etwas spezifischere Fälle von allgemeinen OO / Software-Design-Anti-Patterns. Zum Beispiel:

  • Die Überinjektion von Konstruktoren ist eine Verletzung des Single-Responsibility-Prinzips . Zu viele Konstruktorargumente weisen auf zu viele Abhängigkeiten hin. Zu viele Abhängigkeiten deuten darauf hin, dass die Klasse versucht, zu viel zu tun. Normalerweise korreliert dieser Fehler mit anderen Code-Gerüchen, wie ungewöhnlich langen oder mehrdeutigen Klassennamen ("Manager"). Statische Analysewerkzeuge können leicht übermäßige afferente / efferente Kopplung erkennen.

  • Das Injizieren von Daten ist im Gegensatz zum Verhalten ein Subtyp des poltergeistischen Anti-Musters, wobei der Geist in diesem Fall der Container ist. Wenn eine Klasse das aktuelle Datum und die aktuelle Uhrzeit kennen muss, injizieren Sie keine DateTimeDaten. Stattdessen injizieren Sie eine Abstraktion über die Systemuhr (normalerweise nenne ich meine ISystemClock, obwohl ich denke, dass es eine allgemeinere im SystemWrappers- Projekt gibt). Dies gilt nicht nur für DI; Dies ist für die Testbarkeit unbedingt erforderlich, damit Sie zeitlich veränderliche Funktionen testen können, ohne darauf warten zu müssen.

  • Jeden Lebenszyklus als Singleton zu deklarieren, ist für mich ein perfektes Beispiel für Frachtkultprogrammierung und in geringerem Maße die umgangssprachliche Bezeichnung " Objekt-Senkgrube ". Ich habe mehr Missbrauch von Singletons gesehen, als mir einfällt, und nur sehr wenig davon betrifft DI.

  • Ein weiterer häufiger Fehler sind implementierungsspezifische Schnittstellentypen (mit seltsamen Namen wie IOracleRepository), die nur ausgeführt werden, um sie im Container registrieren zu können. Dies ist an und für sich ein Verstoß gegen das Prinzip der Inversion von Abhängigkeiten (nur weil es sich um eine Schnittstelle handelt, bedeutet dies nicht, dass es wirklich abstrakt ist) und beinhaltet häufig auch ein Aufblähen der Schnittstelle, das das Prinzip der Schnittstellentrennung verletzt .

  • Der letzte Fehler, den ich normalerweise sehe, ist die "optionale Abhängigkeit", die sie in NerdDinner gemacht haben . Mit anderen Worten, es gibt einen Konstruktor, der die Abhängigkeitsinjektion akzeptiert, aber auch einen anderen Konstruktor, der eine "Standard" -Implementierung verwendet. Dies auch verletzt die DIP und neigt zu führen LSP als Entwickler Verletzungen als auch, im Laufe der Zeit beginnen Annahmen um die Default - Implementierung zu machen und / oder starten neu-ing up Instanzen des Standardkonstruktors verwenden.

Wie das alte Sprichwort sagt, können Sie FORTRAN in jeder Sprache schreiben . Dependency Injection ist kein Allheilmittel , die ihr Abhängigkeitsmanagement verhindern werden die Entwickler von vermasseln, aber es tut verhindert eine Reihe von gemeinsamen Fehlern / anti-Muster:

...und so weiter.

Natürlich möchten Sie kein Framework entwerfen, das von einer bestimmten IoC-Container- Implementierung wie Unity oder AutoFac abhängt. Dies verstößt erneut gegen das DIP. Wenn Sie jedoch einmal darüber nachdenken, müssen Sie bereits einige Entwurfsfehler gemacht haben, da die Abhängigkeitsinjektion eine allgemeine Abhängigkeitsverwaltungstechnik ist und nicht an das Konzept eines IoC-Containers gebunden ist.

Jeder kann einen Abhängigkeitsbaum erstellen. Vielleicht ist es ein IoC-Container, vielleicht ist es ein Unit-Test mit einer Reihe von Mocks, vielleicht ist es ein Testtreiber, der Dummy-Daten liefert. Ihr Framework sollte sich nicht darum kümmern, und die meisten Frameworks, die ich gesehen habe, interessieren sich nicht dafür, nutzen jedoch die Abhängigkeitsinjektion in hohem Maße, damit sie problemlos in den IoC-Container der Wahl des Endbenutzers integriert werden kann.

DI ist keine Raketenwissenschaft. Versuchen Sie nur zu vermeiden newund es sei staticdenn, es gibt einen zwingenden Grund, sie zu verwenden, z. B. eine Utility - Methode, die keine externen Abhängigkeiten aufweist, oder eine Utility - Klasse, die möglicherweise keinen Zweck außerhalb des Frameworks hat (Interop - Wrapper und Dictionary - Schlüssel sind häufige Beispiele dafür) Dies).

Viele Probleme mit IoC-Frameworks treten auf, wenn Entwickler erst lernen, wie sie verwendet werden, und anstatt die Art und Weise, wie sie mit Abhängigkeiten und Abstraktionen umgehen, an das IoC-Modell anzupassen, versuchen Sie stattdessen, den IoC-Container so zu manipulieren, dass er den Erwartungen der Entwickler entspricht alter Codierungsstil, der häufig eine hohe Kopplung und eine geringe Kohäsion beinhaltet. Schlechter Code ist schlechter Code, unabhängig davon, ob DI-Techniken verwendet werden oder nicht.

Aaronaught
quelle
Ich habe ein paar Fragen. Bei einer auf NuGet verteilten Bibliothek kann ich mich nicht auf ein IoC-System verlassen, da dies von der Anwendung, die die Bibliothek verwendet, und nicht von der Bibliothek selbst durchgeführt wird. In vielen Fällen kümmert sich der Benutzer der Bibliothek überhaupt nicht um ihre internen Abhängigkeiten. Gibt es ein Problem mit einem Standardkonstruktor, der die internen Abhängigkeiten initiiert, und einem längeren Konstruktor für Komponententests und / oder angepasste Implementierungen? Außerdem sagst du "neu" zu vermeiden. Gilt dies für Dinge wie StringBuilder, DateTime und TimeSpan?
Etienne Charland