Ich denke über das Design einer C # -Bibliothek nach, die verschiedene Funktionen auf hoher Ebene haben wird. Natürlich werden diese Funktionen auf hoher Ebene so weit wie möglich unter Verwendung der SOLID- Klassenentwurfsprinzipien implementiert . Daher wird es wahrscheinlich Klassen geben, die von Verbrauchern regelmäßig direkt verwendet werden sollen, und "Unterstützungsklassen", die Abhängigkeiten von diesen häufigeren "Endbenutzer" -Klassen sind.
Die Frage ist, wie die Bibliothek am besten so gestaltet werden kann:
- DI-Agnostiker - Obwohl das Hinzufügen einer grundlegenden "Unterstützung" für eine oder zwei der gängigen DI-Bibliotheken (StructureMap, Ninject usw.) vernünftig erscheint, möchte ich, dass Verbraucher die Bibliothek mit jedem DI-Framework verwenden können.
- Nicht DI verwendbar - Wenn ein Benutzer der Bibliothek kein DI verwendet, sollte die Bibliothek dennoch so einfach wie möglich zu verwenden sein, wodurch der Arbeitsaufwand für einen Benutzer verringert wird, um all diese "unwichtigen" Abhängigkeiten zu erstellen die "echten" Klassen, die sie verwenden möchten.
Mein derzeitiger Gedanke ist es, einige "DI-Registrierungsmodule" für die allgemeinen DI-Bibliotheken (z. B. eine StructureMap-Registrierung, ein Ninject-Modul) und eine Menge oder Factory-Klassen bereitzustellen, die keine DI-Klassen sind und die Kopplung an diese wenigen Fabriken enthalten.
Gedanken?
Antworten:
Dies ist eigentlich einfach, wenn Sie erst einmal verstanden haben, dass es bei DI um Muster und Prinzipien geht , nicht um Technologie.
Befolgen Sie diese allgemeinen Prinzipien, um die API DI Container-unabhängig zu gestalten:
Programm auf eine Schnittstelle, keine Implementierung
Dieses Prinzip ist eigentlich ein Zitat (allerdings aus dem Gedächtnis) aus Design Patterns , aber es sollte immer Ihr eigentliches Ziel sein . DI ist nur ein Mittel, um dieses Ziel zu erreichen .
Wenden Sie das Hollywood-Prinzip an
Das Hollywood-Prinzip in DI-Begriffen lautet: Rufen Sie nicht den DI-Container an, er ruft Sie an .
Fragen Sie niemals direkt nach einer Abhängigkeit, indem Sie einen Container aus Ihrem Code heraus aufrufen. Fragen Sie implizit mit Constructor Injection danach .
Verwenden Sie die Konstruktorinjektion
Wenn Sie eine Abhängigkeit benötigen, fordern Sie diese statisch über den Konstruktor an:
Beachten Sie, wie die Service-Klasse ihre Invarianten garantiert. Sobald eine Instanz erstellt wurde, ist die Abhängigkeit aufgrund der Kombination aus der Guard-Klausel und dem
readonly
Schlüsselwort garantiert verfügbar .Verwenden Sie Abstract Factory, wenn Sie ein kurzlebiges Objekt benötigen
Mit Constructor Injection injizierte Abhängigkeiten sind in der Regel langlebig, aber manchmal benötigen Sie ein kurzlebiges Objekt oder erstellen die Abhängigkeit basierend auf einem Wert, der nur zur Laufzeit bekannt ist.
Sehen Sie diese für weitere Informationen.
Verfassen Sie nur im letzten verantwortlichen Moment
Halten Sie Objekte bis zum Ende entkoppelt. Normalerweise können Sie warten und alles am Einstiegspunkt der Anwendung verkabeln. Dies wird als Kompositionswurzel bezeichnet .
Weitere Details hier:
Vereinfachen Sie die Verwendung einer Fassade
Wenn Sie der Meinung sind, dass die resultierende API für Anfänger zu komplex wird, können Sie immer einige Facade- Klassen bereitstellen , die allgemeine Abhängigkeitskombinationen enthalten.
Um eine flexible Fassade mit einem hohen Grad an Auffindbarkeit bereitzustellen, können Sie Fluent Builders bereitstellen. Etwas wie das:
Dies würde es einem Benutzer ermöglichen, ein Standard-Foo durch Schreiben zu erstellen
Es wäre jedoch sehr auffindbar, dass es möglich ist, eine benutzerdefinierte Abhängigkeit anzugeben, und Sie könnten schreiben
Wenn Sie sich vorstellen, dass die MyFacade-Klasse viele verschiedene Abhängigkeiten kapselt, hoffe ich, dass klar ist, wie sie die richtigen Standardeinstellungen liefert und gleichzeitig die Erweiterbarkeit erkennbar macht.
FWIW, lange nachdem ich diese Antwort geschrieben hatte, erweiterte ich die hierin enthaltenen Konzepte und schrieb einen längeren Blog-Beitrag über DI-freundliche Bibliotheken und einen Begleitbeitrag über DI-freundliche Frameworks .
quelle
Der Begriff "Abhängigkeitsinjektion" hat überhaupt nichts mit einem IoC-Container zu tun, obwohl Sie dazu neigen, sie zusammen zu sehen. Es bedeutet einfach, dass anstatt Ihren Code wie folgt zu schreiben:
Du schreibst es so:
Das heißt, Sie tun zwei Dinge, wenn Sie Ihren Code schreiben:
Verlassen Sie sich auf Schnittstellen anstelle von Klassen, wenn Sie der Meinung sind, dass die Implementierung möglicherweise geändert werden muss.
Anstatt Instanzen dieser Schnittstellen innerhalb einer Klasse zu erstellen, übergeben Sie sie als Konstruktorargumente (alternativ könnten sie öffentlichen Eigenschaften zugewiesen werden; ersteres ist Konstruktorinjektion , letzteres ist Eigenschaftsinjektion ).
Nichts davon setzt die Existenz einer DI-Bibliothek voraus, und es macht es nicht wirklich schwieriger, den Code ohne eine zu schreiben.
Wenn Sie nach einem Beispiel dafür suchen, sind Sie bei .NET Framework selbst genau richtig:
List<T>
GeräteIList<T>
. Wenn Sie Ihre Klasse für die VerwendungIList<T>
(oderIEnumerable<T>
) entwerfen , können Sie Konzepte wie Lazy-Loading nutzen, wie dies Linq to SQL, Linq to Entities und NHibernate hinter den Kulissen tun, normalerweise durch Eigenschaftsinjektion. Einige Framework-Klassen akzeptieren tatsächlichIList<T>
ein Konstruktorargument, z. B.BindingList<T>
das für mehrere Datenbindungsfunktionen verwendet wird.Linq to SQL und EF basieren vollständig auf den
IDbConnection
und verwandten Schnittstellen, die über die öffentlichen Konstruktoren übergeben werden können. Sie müssen sie jedoch nicht verwenden. Die Standardkonstruktoren funktionieren einwandfrei mit einer Verbindungszeichenfolge, die sich irgendwo in einer Konfigurationsdatei befindet.Wenn Sie jemals an WinForms-Komponenten arbeiten, beschäftigen Sie sich mit "Diensten" wie
INameCreationService
oderIExtenderProviderService
. Sie wissen nicht einmal wirklich, was die konkreten Klassen sind . .NET verfügt tatsächlich über einen eigenen IoC-Container,IContainer
der dafür verwendet wird, und dieComponent
Klasse verfügt über eineGetService
Methode, die der eigentliche Service Locator ist. Natürlich hindert Sie nichts daran, eine oder alle dieser Schnittstellen ohne denIContainer
oder diesen bestimmten Locator zu verwenden. Die Dienste selbst sind nur lose mit dem Container verbunden.Verträge in WCF basieren ausschließlich auf Schnittstellen. Die tatsächliche konkrete Serviceklasse wird normalerweise in einer Konfigurationsdatei, die im Wesentlichen DI ist, namentlich referenziert. Viele Leute wissen das nicht, aber es ist durchaus möglich, dieses Konfigurationssystem gegen einen anderen IoC-Container auszutauschen. Vielleicht interessanter ist, dass das Serviceverhalten alle Instanzen
IServiceBehavior
sind, die später hinzugefügt werden können. Auch hier können Sie dies problemlos in einen IoC-Container einbinden und das relevante Verhalten auswählen lassen, aber die Funktion kann ohne eines vollständig verwendet werden.Und so weiter und so fort. Sie finden DI überall in .NET. Normalerweise geschieht dies nur so nahtlos, dass Sie es nicht einmal als DI betrachten.
Wenn Sie Ihre DI-fähige Bibliothek für maximale Benutzerfreundlichkeit entwerfen möchten, ist es wahrscheinlich der beste Vorschlag, Ihre eigene Standard-IoC-Implementierung mithilfe eines kompakten Containers bereitzustellen.
IContainer
ist eine gute Wahl, da es Teil von .NET Framework selbst ist.quelle
IContainer
der Container nicht in einem Absatz enthalten ist. ;)private readonly
Felder? Das ist großartig, wenn sie nur von der deklarierenden Klasse verwendet werden, das OP jedoch angibt, dass dies Code auf Framework- / Bibliotheksebene ist, was eine Unterklasse impliziert. In solchen Fällen möchten Sie normalerweise wichtige Abhängigkeiten, die Unterklassen ausgesetzt sind. Ich hätte einprivate readonly
Feld und eine Eigenschaft mit expliziten Getter / Setzern schreiben können , aber ... verschwendete Platz in einem Beispiel und mehr Code, um ihn in der Praxis zu pflegen, ohne wirklichen Nutzen.EDIT 2015 : Die Zeit ist vergangen, ich erkenne jetzt, dass diese ganze Sache ein großer Fehler war. IoC-Behälter sind schrecklich und DI ist ein sehr schlechter Weg, um mit Nebenwirkungen umzugehen. Tatsächlich sind alle Antworten hier (und die Frage selbst) zu vermeiden. Seien Sie sich einfach der Nebenwirkungen bewusst, trennen Sie sie vom reinen Code, und alles andere passt entweder zusammen oder ist irrelevant und unnötig komplex.
Die ursprüngliche Antwort folgt:
Bei der Entwicklung von SolrNet musste ich mich derselben Entscheidung stellen . Ich begann mit dem Ziel, DI-freundlich und containerunabhängig zu sein, aber als ich immer mehr interne Komponenten hinzufügte, wurden die internen Fabriken schnell unüberschaubar und die resultierende Bibliothek war unflexibel.
Am Ende habe ich meine eigenen geschrieben Am sehr einfachen eingebetteten IoC-Container geschrieben und gleichzeitig eine Windsor-Einrichtung und ein Ninject-Modul bereitgestellt . Die Integration der Bibliothek in andere Container ist nur eine Frage der ordnungsgemäßen Verkabelung der Komponenten, sodass ich sie problemlos in Autofac, Unity, StructureMap usw. integrieren kann.
Der Nachteil dabei ist, dass ich nicht mehr in der Lage war,
new
den Service zu verbessern. Ich habe auch eine Abhängigkeit von CommonServiceLocator übernommen, die ich hätte vermeiden können (ich könnte sie in Zukunft umgestalten), um die Implementierung des eingebetteten Containers zu vereinfachen.Weitere Details dazu Blogbeitrag .
MassTransit scheint sich auf etwas Ähnliches zu verlassen. Es verfügt über eine IObjectBuilder- Schnittstelle, bei der es sich tatsächlich um den IServiceLocator von CommonServiceLocator mit einigen weiteren Methoden handelt. Anschließend wird diese für jeden Container implementiert, dh NinjectObjectBuilder und ein reguläres Modul / eine reguläre Einrichtung, dh MassTransitModule . Dann ist es auf IObjectBuilder angewiesen, um zu instanziieren, was es benötigt. Dies ist natürlich ein gültiger Ansatz, aber ich persönlich mag ihn nicht sehr, da er tatsächlich zu viel um den Container herumgeht und ihn als Service-Locator verwendet.
MonoRail implementiert auch einen eigenen Container , der den guten alten IServiceProvider implementiert . Dieser Container wird in diesem Framework über eine Schnittstelle verwendet, die bekannte Dienste verfügbar macht . Um den Betoncontainer zu erhalten, verfügt er über einen integrierten Dienstanbieter-Locator . Die Windsor-Einrichtung verweist diesen Dienstanbieter-Locator auf Windsor und macht ihn zum ausgewählten Dienstanbieter.
Fazit: Es gibt keine perfekte Lösung. Wie bei jeder Entwurfsentscheidung erfordert dieses Problem ein Gleichgewicht zwischen Flexibilität, Wartbarkeit und Komfort.
quelle
ServiceLocator
. Wenn Sie funktionale Techniken anwenden, erhalten Sie das Äquivalent von Closures, bei denen es sich um Klassen mit Abhängigkeitsinjektion handelt (wobei Abhängigkeiten die geschlossenen Variablen sind). Wie sonst? (Ich bin wirklich neugierig, weil ich DI auch nicht mag.)Ich würde meine Bibliothek in einer DI-Container-agnostischen Weise entwerfen, um die Abhängigkeit vom Container so weit wie möglich zu begrenzen. Dies ermöglicht es, den DI-Container bei Bedarf gegen einen anderen auszutauschen.
Stellen Sie dann die Ebene über der DI-Logik den Benutzern der Bibliothek zur Verfügung, damit diese das von Ihnen ausgewählte Framework über Ihre Benutzeroberfläche verwenden können. Auf diese Weise können sie weiterhin die von Ihnen bereitgestellten DI-Funktionen verwenden und jedes andere Framework für ihre eigenen Zwecke verwenden.
Es scheint mir ein bisschen falsch zu sein, den Benutzern der Bibliothek zu erlauben, ihr eigenes DI-Framework anzuschließen, da dies den Wartungsaufwand dramatisch erhöht. Dies wird dann auch eher eine Plugin-Umgebung als eine reine DI.
quelle