Ich erhalte eine Abhängigkeitsinjektion, aber kann mir jemand helfen, die Notwendigkeit eines IoC-Containers zu verstehen?

15

Ich entschuldige mich, wenn dies wie eine weitere Wiederholung der Frage erscheint, aber jedes Mal, wenn ich einen Artikel zum Thema finde, wird meist nur darüber gesprochen, was DI ist. Also, ich bekomme DI, aber ich versuche zu verstehen, dass ein IoC-Container benötigt wird, in den sich offenbar jeder hineinzuversetzen scheint. Ist der Sinn eines IoC-Containers wirklich nur, die konkrete Implementierung der Abhängigkeiten "automatisch aufzulösen"? Vielleicht haben meine Klassen nicht viele Abhängigkeiten, und vielleicht sehe ich deshalb nicht die große Sache, aber ich möchte sicherstellen, dass ich den Nutzen des Containers richtig verstehe.

Normalerweise teile ich meine Geschäftslogik in eine Klasse auf, die ungefähr so ​​aussieht:

public class SomeBusinessOperation
{
    private readonly IDataRepository _repository;

    public SomeBusinessOperation(IDataRespository repository = null)
    {
        _repository = repository ?? new ConcreteRepository();
    }

    public SomeType Run(SomeRequestType request)
    {
        // do work...
        var results = _repository.GetThings(request);

        return results;
    }
}

Es hat also nur die eine Abhängigkeit, und in einigen Fällen hat es vielleicht eine zweite oder dritte, aber nicht allzu oft. So kann alles, was dies aufruft, sein eigenes Repo übergeben oder es erlauben, das Standard-Repo zu verwenden.

Nach meinem derzeitigen Verständnis eines IoC-Containers ist alles, was der Container tut, die Auflösung von IDataRepository. Aber wenn das alles ist, sehe ich nicht viel Wert darin, da meine Operationsklassen bereits einen Fallback definieren, wenn keine Abhängigkeit übergeben wird. Der einzige andere Vorteil, den ich mir vorstellen kann, ist, dass ich mehrere Operationen wie habe Wenn Sie dasselbe Fallback-Repo verwenden, kann ich dieses Repo an einer Stelle ändern, die die Registrierung / Fabrik / Container ist. Und das ist toll, aber ist es das?

Sinaesthetic
quelle
1
Oft ist eine Standard-Fallback-Version der Abhängigkeit nicht wirklich sinnvoll.
Ben Aaronson
Wie meinst Du das? Der "Fallback" ist die konkrete Klasse, die fast immer verwendet wird, mit Ausnahme von Unit-Tests. Tatsächlich wäre dies die gleiche Klasse, die im Container registriert ist.
Sinaesthetic
Ja, aber mit dem Container: (1) Alle anderen Objekte im Container erhalten dieselbe Instanz von ConcreteRepositoryund (2) Sie können zusätzliche Abhängigkeiten für angeben ConcreteRepository(eine Datenbankverbindung wäre beispielsweise üblich).
Jules
@Sinaesthetic Ich meine nicht, dass es immer eine schlechte Idee ist, aber oft ist es nicht angebracht. Dies würde zum Beispiel verhindern, dass Sie der Zwiebelarchitektur mit Ihren Projektreferenzen folgen. Möglicherweise gibt es auch keine eindeutige Standardimplementierung. Und wie Jules sagt, verwalten IOC-Container nicht nur die Auswahl des Abhängigkeitstyps, sondern auch die gemeinsame Nutzung von Instanzen und das Lebenszyklusmanagement
Ben Aaronson,
Ich werde ein T-Shirt mit der Aufschrift "Function Parameters - The ORIGINAL Dependency Injection!" Herstellen lassen.
Graham

Antworten:

2

Im IoC-Container geht es nicht um den Fall, dass Sie eine Abhängigkeit haben. Es geht um den Fall, dass Sie 3 Abhängigkeiten haben und diese mehrere Abhängigkeiten haben, die Abhängigkeiten usw. haben.

Sie können damit auch die Auflösung einer Abhängigkeit zentralisieren und den Lebenszyklus von Abhängigkeiten verwalten.

Zeichen
quelle
10

Es gibt eine Reihe von Gründen, warum Sie einen IoC-Container verwenden möchten.

Nicht referenzierte DLLs

Sie können einen IoC-Container verwenden, um eine konkrete Klasse aus einer nicht referenzierten DLL aufzulösen. Dies bedeutet, dass Sie Abhängigkeiten vollständig von der Abstraktion - dh der Schnittstelle - übernehmen können.

Vermeiden Sie die Verwendung von new

Ein IoC-Container bedeutet, dass Sie die Verwendung des newSchlüsselworts zum Erstellen einer Klasse vollständig entfernen können . Dies hat zwei Auswirkungen. Das erste ist, dass es Ihre Klassen entkoppelt. Das zweite (was damit zusammenhängt) ist, dass Sie Mocks für Unit-Tests einwerfen können. Dies ist unglaublich nützlich, insbesondere wenn Sie mit einem lang laufenden Prozess interagieren.

Schreiben Sie gegen Abstraktionen

Wenn Sie einen IoC-Container verwenden, um Ihre konkreten Abhängigkeiten für Sie aufzulösen, können Sie Ihren Code gegen Abstraktionen schreiben, anstatt jede konkrete Klasse nach Bedarf zu implementieren. Möglicherweise benötigen Sie Ihren Code, um Daten aus einer Datenbank zu lesen. Anstatt die Datenbank-Interaktionsklasse zu schreiben, schreiben Sie einfach eine Schnittstelle dafür und codieren dagegen. Sie können einen Mock verwenden, um die Funktionalität des Codes zu testen, den Sie während der Entwicklung entwickeln, anstatt sich auf die Entwicklung der konkreten Datenbankinteraktionsklasse zu verlassen, bevor Sie den anderen Code testen können.

Vermeiden Sie spröden Code

Ein weiterer Grund für die Verwendung eines IoC-Containers besteht darin, dass Sie nicht jeden einzelnen Aufruf eines Klassenkonstruktors ändern müssen, wenn Sie eine Abhängigkeit hinzufügen oder entfernen, indem Sie sich auf den IoC-Container verlassen, um Ihre Abhängigkeiten aufzulösen. Der IoC-Container löst Ihre Abhängigkeiten automatisch auf. Dies ist kein großes Problem, wenn Sie eine Klasse einmal erstellen, aber ein riesiges Problem, wenn Sie die Klasse an hundert Orten erstellen.

Lebensdauerverwaltung und nicht verwaltete Ressourcenbereinigung

Der letzte Grund, den ich erwähne, ist die Verwaltung der Objektlebensdauer. IoC-Container bieten häufig die Möglichkeit, die Lebensdauer eines Objekts anzugeben. Es ist sehr sinnvoll, die Lebensdauer eines Objekts in einem IoC-Container anzugeben, anstatt zu versuchen, es manuell im Code zu verwalten. Das manuelle Lebensdauermanagement kann sehr schwierig sein. Dies kann nützlich sein, wenn Sie mit Objekten arbeiten, die entsorgt werden müssen. Anstatt die Entsorgung Ihrer Objekte manuell zu verwalten, verwalten einige IoC-Container die Entsorgung für Sie, was dazu beitragen kann, Speicherverluste zu vermeiden und Ihre Codebasis zu vereinfachen.

Das Problem mit dem von Ihnen bereitgestellten Beispielcode besteht darin, dass die von Ihnen geschriebene Klasse eine konkrete Abhängigkeit von der ConcreteRepository-Klasse aufweist. Ein IoC-Container würde diese Abhängigkeit entfernen.

Stephen
quelle
22
Dies sind keine Vorteile von IoC-Containern, sondern Vorteile der Abhängigkeitsinjektion, die mit DI
Ben Aaronson,
Das Schreiben eines guten DI-Codes ohne einen IoC-Container kann tatsächlich ziemlich schwierig sein. Ja, es gibt einige Überschneidungen bei den Vorteilen, aber all diese Vorteile lassen sich am besten mit einem IoC-Container nutzen.
Stephen
Nun, die letzten beiden Gründe, die Sie seit meinem Kommentar hinzugefügt haben, sind container-spezifischer und sind meiner Meinung nach beide sehr starke Argumente
Ben Aaronson
"Vermeiden Sie die Verwendung von Neuem" - behindert auch die statische Code-Analyse, sodass Sie mit der Verwendung von hmemcpy.github.io/AgentMulder beginnen müssen . Weitere Vorteile, die Sie in diesem Abschnitt beschreiben, beziehen sich auf DI und nicht auf IoC. Auch Ihre Klassen bleiben gekoppelt, wenn Sie keine neuen verwenden, sondern konkrete Typen anstelle von Schnittstellen für Parameter verwenden.
Den
1
Insgesamt wird IoC als etwas ohne Mängel beschrieben, zum Beispiel wird der große Nachteil nicht erwähnt: Eine Klasse von Fehlern wird in die Laufzeit geschoben, anstatt in die Kompilierungszeit.
Den
2

Nach dem Prinzip der Einzelverantwortung darf jede Klasse nur eine einzige Verantwortung haben. Das Erstellen neuer Instanzen von Klassen ist nur eine weitere Aufgabe. Sie müssen diese Art von Code in eine oder mehrere Klassen einschließen. Sie können dazu beliebige Muster verwenden, z. B. Fabriken, Builder, DI-Container usw.

Es gibt andere Prinzipien wie die Inversion der Kontrolle und die Inversion der Abhängigkeit. In diesem Zusammenhang beziehen sie sich auf die Instanziierung von Abhängigkeiten. Sie geben an, dass die High-Level-Klassen von den von ihnen verwendeten Low-Level-Klassen (Abhängigkeiten) entkoppelt werden müssen. Wir können Dinge entkoppeln, indem wir Schnittstellen schaffen. Daher müssen die Klassen auf niedriger Ebene spezifische Schnittstellen implementieren, und die Klassen auf hoher Ebene müssen Instanzen von Klassen verwenden, die diese Schnittstellen implementieren. (Hinweis: Die einheitliche REST-Schnittstellenbeschränkung wendet denselben Ansatz auf Systemebene an.)

Die Kombination dieser Prinzipien anhand eines Beispiels (Entschuldigung für den Code mit niedriger Qualität, ich habe eine Ad-hoc-Sprache anstelle von C # verwendet, da ich das nicht weiß):

  1. Kein SRP, kein IoC

    class SomeHighLevelService
    {
        public doFooBar(){
            Crap crap = doFoo();
            doBar(crap);
        }
    
        public Crap doFoo(){
            //...
            return crap;
        }
    
        public doBar(Crap crap){
            //...
        }
    }
    
    SomeHighLevelService service = new SomeHighLevelService();
    service.doFooBar();
  2. Näher an SRP, kein IoC

    class SomeHighLevelService
    {
        public SomeHighLevelService(){
            Foo foo = new Foo();
            Bar bar = new Bar();
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    }
    
    class Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    SomeHighLevelService service = new SomeHighLevelService();
    service.doFooBar();
  3. Ja SRP, nein IoC

    class HighLevelServiceProvider {
        public SomeHighLevelService getSomeHighLevelService(){
            SomeHighLevelService service = new SomeHighLevelService();
            service.setFoo(this.getFoo());
            service.getBar(this.getBar());
            return service;
        }
    
        private Foo getFoo(){
            return new Foo();
        }
    
        private Bar getBar(){
            return new Bar();
        }
    }
    
    class SomeHighLevelService
    {           
        public setFoo(Foo foo){
            this.foo = foo;
        }
    
        public setBar(Bar bar){
            this.bar = bar;
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    
    }
    
    class Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    HighLevelServiceProvider provider = new HighLevelServiceProvider();
    SomeHighLevelService service = provider.getSomeHighLevelService();
    service.doFooBar();
  4. Ja SRP, ja IoC

    interface HighLevelServiceProvider {
        SomeHighLevelService getSomeHighLevelService();
    }
    
    interface SomeHighLevelService {
        doFooBar();
    }
    
    interface Foo {
        Crap doFoo();
    }
    
    interface Bar {
        doBar(Crap crap);
    }
    
    
    class ConcreteHighLevelServiceContainer implements HighLevelServiceProvider {
        public SomeHighLevelService getSomeHighLevelService(){
            SomeHighLevelService service = new ConcreteHighLevelService();
            service.setFoo(this.getFoo());
            service.getBar(this.getBar());
            return service;
        }
    
        private Foo getFoo(){
            return new ConcreteFoo();
        }
    
        private Bar getBar(){
            return new ConcreteBar();
        }
    }
    
    class ConcreteHighLevelService implements SomeHighLevelService
    {           
        public setFoo(Foo foo){
            this.foo = foo;
        }
    
        public setBar(Bar bar){
            this.bar = bar;
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    
    }
    
    class ConcreteFoo implements Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class ConcreteBar implements Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    
    HighLevelServiceProvider provider = new ConcreteHighLevelServiceContainer();
    SomeHighLevelService service = provider.getSomeHighLevelService();
    service.doFooBar();

So entstand ein Code, in dem Sie jede konkrete Implementierung durch eine andere ersetzen können, die dieselbe Schnittstelle ofc implementiert. Das ist gut so, denn die teilnehmenden Klassen sind voneinander entkoppelt, sie kennen nur die Schnittstellen. Ein weiterer Vorteil ist, dass der Code der Instanziierung wiederverwendbar ist.

inf3rno
quelle