Dependency Inject (DI) "freundliche" Bibliothek

230

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?

Pete
quelle
Der neue Verweis auf einen defekten Linkartikel (SOLID): butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
M.Hassan

Antworten:

360

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:

public class Service : IService
{
    private readonly ISomeDependency dep;

    public Service(ISomeDependency dep)
    {
        if (dep == null)
        {
            throw new ArgumentNullException("dep");
        }

        this.dep = dep;
    }

    public ISomeDependency Dependency
    {
        get { return this.dep; }
    }
}

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 readonlySchlü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:

public class MyFacade
{
    private IMyDependency dep;

    public MyFacade()
    {
        this.dep = new DefaultDependency();
    }

    public MyFacade WithDependency(IMyDependency dependency)
    {
        this.dep = dependency;
        return this;
    }

    public Foo CreateFoo()
    {
        return new Foo(this.dep);
    }
}

Dies würde es einem Benutzer ermöglichen, ein Standard-Foo durch Schreiben zu erstellen

var foo = new MyFacade().CreateFoo();

Es wäre jedoch sehr auffindbar, dass es möglich ist, eine benutzerdefinierte Abhängigkeit anzugeben, und Sie könnten schreiben

var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();

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 .

Mark Seemann
quelle
21
Während dies auf dem Papier großartig klingt, müssen meiner Erfahrung nach viele innere Komponenten, die auf komplexe Weise interagieren, viele Fabriken verwalten, was die Wartung erschwert. Außerdem müssen Fabriken den Lebensstil ihrer erstellten Komponenten verwalten. Sobald Sie die Bibliothek in einem realen Container installiert haben, steht dies im Widerspruch zum eigenen Lifestyle-Management des Containers. Fabriken und Fassaden stören die realen Container.
Mauricio Scheffer
4
Ich habe noch kein Projekt gefunden, das all dies tut .
Mauricio Scheffer
31
So entwickeln wir Software bei Safewhere, damit wir Ihre Erfahrungen nicht teilen ...
Mark Seemann
19
Ich denke, dass die Fassaden von Hand codiert werden sollten, da sie bekannte (und vermutlich übliche) Kombinationen von Komponenten darstellen. Ein DI-Container sollte nicht erforderlich sein, da alles von Hand verkabelt werden kann (denken Sie an DI des armen Mannes). Denken Sie daran, dass eine Fassade nur eine optionale Komfortklasse für Benutzer Ihrer API ist. Fortgeschrittene Benutzer möchten möglicherweise weiterhin die Fassade umgehen und die Komponenten nach ihren Wünschen verkabeln. Sie möchten möglicherweise ihren eigenen DI-Contaier dafür verwenden, daher halte ich es für unfreundlich, ihnen einen bestimmten DI-Container aufzuzwingen, wenn sie ihn nicht verwenden. Möglich, aber nicht ratsam
Mark Seemann
8
Dies ist vielleicht die beste Antwort, die ich je auf SO gesehen habe.
Nick Hodges
40

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:

public class Service
{
    public Service()
    {
    }

    public void DoSomething()
    {
        SqlConnection connection = new SqlConnection("some connection string");
        WindowsIdentity identity = WindowsIdentity.GetCurrent();
        // Do something with connection and identity variables
    }
}

Du schreibst es so:

public class Service
{
    public Service(IDbConnection connection, IIdentity identity)
    {
        this.Connection = connection;
        this.Identity = identity;
    }

    public void DoSomething()
    {
        // Do something with Connection and Identity properties
    }

    protected IDbConnection Connection { get; private set; }
    protected IIdentity Identity { get; private set; }
}

Das heißt, Sie tun zwei Dinge, wenn Sie Ihren Code schreiben:

  1. Verlassen Sie sich auf Schnittstellen anstelle von Klassen, wenn Sie der Meinung sind, dass die Implementierung möglicherweise geändert werden muss.

  2. 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äte IList<T>. Wenn Sie Ihre Klasse für die Verwendung IList<T>(oder IEnumerable<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ächlich IList<T>ein Konstruktorargument, z. B. BindingList<T>das für mehrere Datenbindungsfunktionen verwendet wird.

  • Linq to SQL und EF basieren vollständig auf den IDbConnectionund 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 INameCreationServiceoder IExtenderProviderService. Sie wissen nicht einmal wirklich, was die konkreten Klassen sind . .NET verfügt tatsächlich über einen eigenen IoC-Container, IContainerder dafür verwendet wird, und die ComponentKlasse verfügt über eine GetServiceMethode, die der eigentliche Service Locator ist. Natürlich hindert Sie nichts daran, eine oder alle dieser Schnittstellen ohne den IContaineroder 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 IServiceBehaviorsind, 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. IContainerist eine gute Wahl, da es Teil von .NET Framework selbst ist.

Aaronaught
quelle
2
Die eigentliche Abstraktion für einen Container ist IServiceProvider, nicht IContainer.
Mauricio Scheffer
2
@Mauricio: Sie haben natürlich Recht, aber versuchen Sie jemandem zu erklären, der das LWC-System noch nie benutzt hat, warum IContainerder Container nicht in einem Absatz enthalten ist. ;)
Aaronaught
Aaron, nur neugierig, warum Sie ein privates Set verwenden; anstatt nur die Felder als schreibgeschützt anzugeben?
3.
@Jreeter: Du meinst, warum sind sie keine private readonlyFelder? 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 ein private readonlyFeld 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.
Aaronaught
Danke für das Aufklären! Ich nahm an, dass schreibgeschützte Felder für das Kind zugänglich sind.
jr3
5

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, newden 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.

Mauricio Scheffer
quelle
12
Obwohl ich Ihre Meinung respektiere, finde ich Ihre übermäßige "Alle anderen Antworten hier sind veraltet und sollten vermieden werden" äußerst naiv. Wenn wir seitdem etwas gelernt haben, ist die Abhängigkeitsinjektion eine Antwort auf Sprachbeschränkungen. Ohne unsere derzeitigen Sprachen weiterzuentwickeln oder aufzugeben, brauchen wir DI, um unsere Architekturen zu reduzieren und Modularität zu erreichen. IoC als allgemeines Konzept ist nur eine Folge dieser Ziele und definitiv eine wünschenswerte. IoC- Container sind weder für IoC noch für DI unbedingt erforderlich, und ihre Nützlichkeit hängt von den von uns erstellten Modulzusammensetzungen ab.
Der
@tne Ihre Erwähnung, dass ich "extrem naiv" bin, ist ein nicht willkommenes Ad-Hominem. Ich habe komplexe Anwendungen ohne DI oder IoC in C #, VB.NET, Java geschrieben (Sprachen, von denen Sie denken, dass sie IoC "brauchen", anscheinend nach Ihrer Beschreibung), es geht definitiv nicht um Sprachbeschränkungen. Es geht um konzeptionelle Einschränkungen. Ich lade Sie ein, sich mit funktionaler Programmierung vertraut zu machen, anstatt auf Ad-Hominem-Angriffe und schlechte Argumente zurückzugreifen.
Mauricio Scheffer
18
Ich erwähnte, ich fand Ihre Aussage naiv; das sagt nichts über dich persönlich aus und dreht sich alles um meine meinung. Bitte fühlen Sie sich nicht von einem zufälligen Fremden beleidigt. Es ist auch nicht hilfreich zu behaupten, dass ich nicht alle relevanten funktionalen Programmieransätze kenne. du weißt das nicht und du liegst falsch. Ich wusste, dass Sie sich dort auskennen ( ich habe mir schnell Ihren Blog angesehen), weshalb ich es ärgerlich fand, dass ein scheinbar sachkundiger Mann Dinge wie "Wir brauchen kein IoC" sagte. IoC geschieht buchstäblich überall in der funktionalen Programmierung (..)
tne
4
und in High-Level-Software im Allgemeinen. Tatsächlich beweisen funktionale Sprachen (und viele andere Stile), dass sie IoC ohne DI lösen. Ich denke, wir sind beide damit einverstanden, und das ist alles, was ich sagen wollte. Wenn Sie in den von Ihnen genannten Sprachen ohne DI auskommen, wie haben Sie das gemacht? Ich nehme nicht an mit 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.)
25.
1
IoC-Container sind global veränderbare Wörterbücher, die die Parametrizität als Argumentationsinstrument wegnehmen und daher vermieden werden sollten. IoC (das Konzept) wie in "Rufen Sie uns nicht an, wir rufen Sie an" gilt technisch jedes Mal, wenn Sie eine Funktion als Wert übergeben. Modellieren Sie anstelle von DI Ihre Domain mit ADTs und schreiben Sie dann Interpreter (siehe Free).
Mauricio Scheffer
1

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.

Igor Zevaka
quelle