Warum sollte ich die Abhängigkeitsinjektion verwenden?


95

Es fällt mir schwer, nach Ressourcen zu suchen, warum ich die Abhängigkeitsinjektion verwenden sollte . Die meisten der angezeigten Ressourcen erklären, dass nur eine Instanz eines Objekts an eine andere Instanz eines Objekts übergeben wird. Aber warum? Ist dies nur für eine sauberere Architektur / Code oder wirkt sich dies auf die Leistung insgesamt aus?

Warum sollte ich folgendes tun?

class Profile {
    public function deactivateProfile(Setting $setting)
    {
        $setting->isActive = false;
    }
}

Anstelle der folgenden?

class Profile {
    public function deactivateProfile()
    {
        $setting = new Setting();
        $setting->isActive = false;
    }
}

8
Sie führen eine fest codierte Abhängigkeit ein, umProfile () zu deaktivieren (was schlecht ist). Sie haben im ersten Code mehr entkoppelten Code, was das Ändern und Testen erleichtert.
Aulis Ronkainen

3
Warum würdest du den ersten machen? Sie übergeben eine Einstellung und ignorieren dann deren Wert.
Phil N DeBlanc

57
Ich bin nicht mit den Abstimmungen einverstanden. Während das Thema für Experten als trivial angesehen werden kann, hat die Frage einen Wert: Wenn eine Abhängigkeitsinversion verwendet werden sollte, sollte es eine Rechtfertigung für die Verwendung geben.
Flater

13
@PhilNDeBlanc: Dieser Code ist deutlich vereinfacht und weist nicht wirklich auf die Logik der realen Welt hin. Dies deactivateProfilelegt mir jedoch nahe, dass das Setzen isActivevon false ohne Rücksicht auf den vorherigen Status hier der richtige Ansatz ist. Das Aufrufen der Methode inhärent bedeutet, dass Sie sie als inaktiv festlegen und nicht ihren aktuellen (in) aktiven Status erhalten möchten.
Flater

2
Ihr Code ist kein Beispiel für eine Abhängigkeitsinjektion oder -inversion. Es ist ein Beispiel für die Parametrisierung (die oft viel besser ist als DI).
jpmc26

Antworten:


104

Der Vorteil ist, dass ohne Abhängigkeitsinjektion Ihre Profilklasse

  • muss wissen, wie man ein Settings-Objekt erstellt (verstößt gegen das Single-Responsibility-Prinzip)
  • Erstellt das Settings-Objekt immer auf dieselbe Weise (stellt eine enge Verbindung zwischen beiden her)

Aber mit Abhängigkeitsinjektion

  • Die Logik zum Erstellen von Settings-Objekten befindet sich an einer anderen Stelle
  • Es ist einfach, verschiedene Arten von Settings-Objekten zu verwenden

Dies mag in diesem speziellen Fall irrelevant erscheinen (oder sogar sein), aber stellen Sie sich vor, dass es sich nicht um ein Settings-Objekt handelt, sondern um ein DataStore-Objekt, das möglicherweise unterschiedliche Implementierungen aufweist. Eines speichert Daten in Dateien, das andere speichert sie in eine Datenbank. Und für automatisierte Tests möchten Sie auch eine Scheinimplementierung. Jetzt möchten Sie wirklich nicht, dass die Profile-Klasse fest codiert, welche sie verwendet - und, was noch wichtiger ist, Sie möchten wirklich, dass die Profile-Klasse nichts über Dateisystempfade, DB-Verbindungen und Kennwörter weiß, also über die Erstellung von DataStore-Objekten muss woanders passieren.


23
This may seem (or even be) irrelevant in this particular caseIch denke, es ist in der Tat sehr relevant. Wie würdest du die Einstellungen bekommen? Viele Systeme, die ich gesehen habe, verfügen über einen fest codierten Standardeinstellungssatz und eine öffentlich zugängliche Konfiguration. Daher müssen Sie beide laden und einige Werte mit den öffentlichen Einstellungen überschreiben. Möglicherweise benötigen Sie sogar mehrere Standardquellen. Vielleicht bekommen Sie sogar einige von der Festplatte, andere von der DB. Die gesamte Logik für das Abrufen von Einstellungen kann und ist daher häufig nicht trivial - auf keinen Fall sollte oder würde sich etwas für verbrauchenden Code interessieren.
VLAZ

Wir könnten auch erwähnen, dass die Objektinitialisierung für eine nicht triviale Komponente, wie einen Webdienst, $setting = new Setting();horrend ineffizient wäre. Injektion und Objektinstanziierung erfolgen einmal.
Vikingsteve

9
Ich denke, die Verwendung von Mocks zum Testen sollte mehr Gewicht haben. Wenn Sie sich nur den Code ansehen, handelt es sich wahrscheinlich immer um ein Settings-Objekt, das sich nie ändern wird. Die Weitergabe des Codes scheint also eine Verschwendung zu sein. Wenn Sie jedoch zum ersten Mal versuchen, ein Profilobjekt selbst zu testen, ohne dass Sie ein Settings-Objekt benötigen (stattdessen ein Scheinobjekt als Lösung verwenden), ist dies sehr offensichtlich.
JPhi1618

2
@ JPhi1618 Ich denke, das Problem beim Hervorheben von "DI is for Unit Testing" ist, dass es nur zu der Frage führt, warum ich Unit Tests benötige. Die Antwort mag Ihnen offensichtlich erscheinen, und die Vorteile sind definitiv vorhanden, aber für jemanden, der gerade erst anfängt und sagt, "Sie müssen dieses kompliziert klingende Ding tun, um dieses andere kompliziert klingende Ding zu tun", ist dies eher etwas eine Abzweigung. Es ist also gut, verschiedene Vorteile zu erwähnen, die möglicherweise besser auf das zutreffen, was sie gerade tun.
IMSoP

1
@ user986730 - das ist ein sehr guter Punkt, und hebt hervor , dass das eigentliche Prinzip hier ist Dependency Inversion , von denen Injection ist nur ein technique.So beispielsweise in C, kann man nicht wirklich Abhängigkeiten injizieren eine IOC - Bibliothek, aber Sie können Invertieren Sie sie beim Kompilieren, indem Sie verschiedene Quelldateien für Ihre Mocks usw. einfügen, was den gleichen Effekt hat.
Stephen Byrne

64

Die Abhängigkeitsinjektion erleichtert das Testen Ihres Codes.

Das habe ich aus erster Hand gelernt, als ich einen schwer zu findenden Fehler in der PayPal-Integration von Magento beheben musste.

Ein Problem trat auf, wenn PayPal Magento über eine fehlgeschlagene Zahlung informierte: Magento konnte den Fehler nicht ordnungsgemäß registrieren.

Das Testen einer potenziellen Korrektur "manuell" wäre sehr mühsam: Sie müssten irgendwie eine PayPal-Benachrichtigung "Fehlgeschlagen" auslösen. Sie müssten einen E-Check einreichen, ihn abbrechen und warten, bis ein Fehler auftritt. Das bedeutet mehr als 3 Tage, um eine Änderung des Ein-Zeichen-Codes zu testen!

Glücklicherweise scheint es, dass die Magento-Kernentwickler, die diese Funktion entwickelt haben, Tests im Auge hatten und ein Abhängigkeitsinjektionsmuster verwendeten, um es trivial zu machen. Dies ermöglicht es uns, unsere Arbeit mit einem einfachen Testfall wie diesem zu verifizieren:

<?php
// This is the dependency we will inject to facilitate our testing
class MockHttpClient extends Varien_Http_Adapter_Curl {
    function read() {
        // Make Magento think that PayPal said "VERIFIED", no matter what they actually said...
        return "HTTP/1.1 200 OK\n\nVERIFIED";
    }
}

// Here, we trick Magento into thinking PayPal actually sent something back.
// Magento will try to verify it against PayPal's API though, and since it's fake data, it'll always fail.
$ipnPayload = array (
  'invoice'        => '100058137',         // Order ID to test against
  'txn_id'         => '04S87540L2309371A', // Test PayPal transaction ID
  'payment_status' => 'Failed'             // New payment status that Magento should ingest
);

// This is what Magento's controller calls during a normal IPN request.
// Instead of letting Magento talk to PayPal, we "inject" our fake HTTP client, which always returns VERIFIED.
Mage::getModel('paypal/ipn')->processIpnRequest($ipnPayload, new MockHttpClient());

Ich bin mir sicher, dass das DI-Muster viele andere Vorteile bietet, aber eine verbesserte Testbarkeit ist der größte Vorteil für mich .

Wenn Sie nach einer Lösung für dieses Problem suchen, lesen Sie das GitHub-Repo hier: https://github.com/bubbleupdev/BUCorefix_Paypalstatus


4
Die Abhängigkeitsinjektion erleichtert das Testen von Code im Vergleich zu Code mit fest codierten Abhängigkeiten. Das Eliminieren von Abhängigkeiten aus der Geschäftslogik ist sogar noch besser.
Ant P

1
Eine der wichtigsten Methoden, um die Vorschläge von @AntP umzusetzen, ist die Parametrisierung . Die Logik zum Umwandeln von Ergebnissen, die aus der Datenbank zurückkehren, in das Objekt, das zum Ausfüllen einer Seitenvorlage (allgemein als "Modell" bezeichnet) verwendet wird, sollte nicht einmal wissen, dass ein Abruf stattfindet. Diese Objekte müssen nur als Eingabe abgerufen werden.
jpmc26

4
@ jpmc26 in der Tat - Ich neige dazu, über den funktionalen Kern, die imperative Shell , nachzudenken , die eigentlich nur ein ausgefallener Name ist, um Daten in Ihre Domain zu übertragen, anstatt Abhängigkeiten in sie einzufügen . Die Domain ist rein, kann Unit-getestet werden und wird dann einfach in eine Shell gewickelt, die Dinge wie Persistenz, Messaging usw. anpasst.
Ant P

1
Ich denke, der alleinige Fokus auf Testbarkeit ist schädlich für die Einführung von DI. Dies macht es für Leute unattraktiv, die das Gefühl haben, dass sie entweder nicht viel testen müssen oder bereits Tests im Griff haben. Ich würde behaupten, es ist praktisch unmöglich, sauberen, wiederverwendbaren Code ohne DI zu schreiben. Das Testen ist weit unten auf der Liste der Vorteile und es ist enttäuschend zu sehen, dass diese Antwort in jeder Frage zu den Vorteilen von DI an erster oder zweiter Stelle steht.
Carl Leth

26

Warum (was ist überhaupt das Problem)?

Warum sollte ich die Abhängigkeitsinjektion verwenden?

Die beste Mnemonik, die ich dafür gefunden habe, ist " new is glue ": Jedes Mal, wenn Sie newin Ihrem Code verwenden, ist dieser Code an diese spezielle Implementierung gebunden . Wenn Sie in Konstruktoren wiederholt new verwenden, erstellen Sie eine Kette spezifischer Implementierungen . Und weil Sie eine Instanz einer Klasse nicht "haben" können, ohne sie zu konstruieren, können Sie diese Kette nicht trennen.

Stellen Sie sich zum Beispiel vor, Sie schreiben ein Rennwagen-Videospiel. Sie haben mit einer Klasse begonnen Game, die eine erstellt RaceTrack, die 8 erstellt Cars, die jeweils eine erstellt Motor. Wenn Sie jetzt 4 zusätzliche Carsmit einer anderen Beschleunigung wünschen , müssen Sie jede erwähnte Klasse ändern , außer vielleicht Game.

Besserer Code

Ist das nur für sauberere Architektur / Code

Ja .

In dieser Situation mag es jedoch sehr wohl weniger klar erscheinen , da es eher ein Beispiel dafür ist, wie man es macht . Der tatsächliche Vorteil zeigt sich nur, wenn mehrere Klassen beteiligt sind, und ist schwieriger zu demonstrieren. Stellen Sie sich jedoch vor, Sie hätten DI im vorherigen Beispiel verwendet. Der Code, der all diese Dinge erstellt, könnte ungefähr so ​​aussehen:

List<Car> cars = new List<Car>();
for(int i=0; i<8; i++){
    float acceleration = 0.3f;
    float maxSpeed = 200.0f;
    Motor motor = new Motor(acceleration, maxSpeed);
    Car car = new Car(motor);
    cars.Add(car);
}
RaceTrack raceTrack = new RaceTrack(cars);
Game game = new Game(raceTrack);

Das Hinzufügen dieser 4 verschiedenen Autos kann nun durch Hinzufügen dieser Zeilen erfolgen:

for(int i=0; i<4; i++){
    float acceleration = 0.5f;
    float maxSpeed = 100.0f;
    Motor motor = new Motor(acceleration, maxSpeed);
    Car car = new Car(motor);
    cars.Add(car);
}
  • Keine Änderungen in RaceTrack, Game, Caroder Motorwaren notwendig - was bedeutet , dass wir 100% sicher sein, dass wir keine neuen Fehler dort nicht eingeführt haben!
  • Anstatt zwischen mehreren Dateien wechseln zu müssen, können Sie die vollständige Änderung gleichzeitig auf Ihrem Bildschirm sehen. Dies ist ein Ergebnis des Erstellens / Einrichtens / Konfigurierens als eigene Verantwortung - es ist nicht die Aufgabe eines Autos, einen Motor zu bauen.

Leistungsaspekte

oder wirkt sich dies auf die Gesamtleistung aus?

Nein . Aber um ganz ehrlich zu sein, könnte es sein.

Aber selbst in diesem Fall ist es eine so lächerlich kleine Menge, dass Sie sich nicht darum kümmern müssen. Wenn zu einem bestimmten Zeitpunkt in der Zukunft, müssen Sie Code für ein Tamagotchi mit dem Äquivalent von 5 MHz CPU und 2 MB RAM schreiben, dann vielleicht Sie vielleicht haben darüber kümmern.

In 99,999% * der Fälle wird die Leistung verbessert, da Sie weniger Zeit für die Behebung von Fehlern und mehr Zeit für die Verbesserung Ihrer ressourcenintensiven Algorithmen aufgewendet haben.

* komplett erfundene Nummer

Info hinzugefügt: "fest codiert"

Machen Sie keinen Fehler, dies ist immer noch sehr "hartcodiert" - die Zahlen werden direkt in den Code geschrieben. Nicht fest codiert würde bedeuten, dass diese Werte in einer Textdatei gespeichert werden - z. B. im JSON-Format - und dann aus dieser Datei gelesen werden.

Dazu müssen Sie Code hinzufügen, um eine Datei zu lesen und anschließend JSON zu analysieren. Wenn Sie das Beispiel noch einmal betrachten; In der Nicht-DI-Version muss a Caroder a Motorjetzt eine Datei lesen. Das klingt nicht so, als ob es zu viel Sinn macht.

In der DI-Version würden Sie es dem Code hinzufügen, der das Spiel einrichtet.


3
Ad-hard-coded gibt es nicht wirklich so viel Unterschied zwischen Code und Konfigurationsdatei. Eine mit der Anwendung gebündelte Datei ist eine Quelle, auch wenn Sie dynamisch lesen. Das Abrufen von Werten aus dem Code in Datendateien im Format json oder im Format config hilft nichts, es sei denn, die Werte sollten vom Benutzer überschrieben werden oder von der Umgebung abhängen.
Jan Hudec

3
Ich habe tatsächlich einmal ein Tamagotchi auf einem Arduino (16MHz, 2KB) gemacht
Jungkook

@ JanHudec Wahr. Ich hatte dort tatsächlich eine längere Erklärung, aber ich entschied mich, sie zu entfernen, um sie kürzer zu halten und mich darauf zu konzentrieren, wie sie sich auf DI bezieht. Es gibt noch mehr Dinge, die nicht zu 100% korrekt sind. Insgesamt ist die Antwort so optimiert, dass "der Punkt" von DI verschoben wird, ohne dass es zu lang wird. Oder anders ausgedrückt, das hätte ich gerne gehört, als ich mit DI angefangen habe.
R. Schmitz

17

Ich war immer verwirrt von der Abhängigkeitsspritze. Es schien nur in Java-Sphären zu existieren, aber diese Sphären sprachen mit großer Ehrfurcht darüber. Es war eines der großen Muster, die Ordnung ins Chaos bringen sollen. Die Beispiele waren jedoch immer verworren und künstlich und stellten ein unproblematisches Problem dar und machten sich dann daran, es zu lösen, indem sie den Code komplizierter machten.

Es machte mehr Sinn, als mir ein Python-Kollege diese Weisheit mitteilte: Es geht nur darum, Argumente an Funktionen weiterzugeben. Es ist überhaupt kein Muster; eher wie eine Erinnerung, dass Sie um etwas als Argument bitten können , selbst wenn Sie möglicherweise selbst einen angemessenen Wert geliefert haben könnten.

Ihre Frage ist also ungefähr gleichbedeutend mit "Warum sollte meine Funktion Argumente verwenden?" und hat viele der gleichen Antworten, nämlich: den Anrufer Entscheidungen treffen zu lassen .

Dies ist zwar mit Kosten, natürlich, denn jetzt bist du zwingt den Anrufer zu machen einige Entscheidung (es sei denn Sie das Argument optional zu machen), und die Schnittstelle ist etwas komplexer. Im Gegenzug gewinnen Sie Flexibilität.

Also: Gibt es einen guten Grund, diesen speziellen SettingTyp / Wert zu verwenden? Gibt es einen guten Grund, warum ein Aufruf von Code einen anderen SettingTyp / Wert haben soll? (Denken Sie daran, Tests sind Code !)


2
Ja. IoC hat mich schließlich angesprochen, als mir klar wurde, dass es einfach darum geht, "den Anrufer Entscheidungen treffen zu lassen". Diese IoC bedeutet, dass die Steuerung der Komponente vom Autor der Komponente auf den Benutzer der Komponente verlagert wird. Und da ich zu diesem Zeitpunkt bereits genug mit Software zu tun hatte, die sich für schlauer hielt als ich, wurde ich sofort für den DI-Ansatz begeistert.
Joker_vD

1
„Es ist nur Argumente an Funktionen übergibt. Es ist kaum ein Muster überhaupt“ Dies ist die einzige gute oder sogar comprehendible Erklärung von DI ich gehört habe (auch die meisten Menschen reden nicht einmal darüber , was es ist , sondern seine Vorteile). Und dann verwenden sie komplizierten Jargon wie "Umkehrung der Kontrolle" und "lose Kopplung", die nur mehr Fragen erfordern, um zu verstehen, wann es eine wirklich einfache Idee ist.
Addison

7

Das Beispiel, das Sie angeben, ist keine Abhängigkeitsinjektion im klassischen Sinne. Unter Abhängigkeitsinjektion versteht man normalerweise das Übergeben von Objekten in einem Konstruktor oder die Verwendung der "Setter-Injektion" unmittelbar nach dem Erstellen des Objekts, um einen Wert für ein Feld in einem neu erstellten Objekt festzulegen.

Ihr Beispiel übergibt ein Objekt als Argument an eine Instanzmethode. Diese Instanzmethode ändert dann ein Feld für dieses Objekt. Abhängigkeitsspritze? Nein. Kapselung aufbrechen und Daten verstecken? Absolut!

Nun, wenn der Code so war:

class Profile {
    private $settings;

    public function __construct(Settings $settings) {
        $this->settings = $settings;
    }

    public function deactive() {
        $this->settings->isActive = false;
    }
}

Dann würde ich sagen, dass Sie Abhängigkeitsinjektion verwenden. Der bemerkenswerte Unterschied ist ein SettingsObjekt, das an den Konstruktor übergeben wird, oder ein ProfileObjekt.

Dies ist hilfreich, wenn das Settings-Objekt teuer oder komplex zu konstruieren ist oder Settings eine Schnittstelle oder eine abstrakte Klasse ist, in der mehrere konkrete Implementierungen vorhanden sind, um das Laufzeitverhalten zu ändern.

Da Sie direkt auf ein Feld im Settings-Objekt zugreifen, anstatt eine Methode aufzurufen, können Sie den Polymorphismus nicht nutzen, der einer der Vorteile der Abhängigkeitsinjektion ist.

Es sieht so aus, als wären die Einstellungen für ein Profil für dieses Profil spezifisch. In diesem Fall würde ich einen der folgenden Schritte ausführen:

  1. Instanziieren Sie das Settings-Objekt im Profile-Konstruktor

  2. Übergeben Sie das Settings-Objekt im Konstruktor und kopieren Sie die einzelnen Felder, die für das Profil gelten

Ehrlich gesagt ist das Übergeben des Settings-Objekts an deactivateProfileein internes Feld des Settings-Objekts und das anschließende Ändern dieses Felds ein Codegeruch. Das Settings-Objekt sollte das einzige sein, das seine internen Felder ändert.


5
The example you give is not dependency injection in the classical sense.- Es ist egal. OO-Leute haben Objekte im Gehirn, aber Sie geben immer noch eine Abhängigkeit von etwas ab.
Robert Harvey

1
Wenn Sie von „ im klassischen Sinne “ sprechen, sprechen Sie, wie @RobertHarvey sagt, nur in OO-Begriffen. In der funktionalen Programmierung zum Beispiel ist das Injizieren einer Funktion in eine andere (Funktionen höherer Ordnung) das klassische Beispiel für das Injizieren von Abhängigkeiten.
David Arno

3
@RobertHarvey: Ich schätze, ich habe mir mit "Abhängigkeitsinjektion" zu viele Freiheiten genommen. Die Stelle, an der die Leute am häufigsten an den Begriff denken und ihn verwenden, bezieht sich auf Felder auf einem Objekt, die zum Zeitpunkt der Objektkonstruktion "injiziert" werden, oder unmittelbar danach im Fall der Setter-Injektion.
Greg Burghardt

@ DavidArno: Ja, Sie sind richtig. Das OP scheint ein objektorientiertes PHP-Code-Snippet in der Frage zu haben, also habe ich nur diesbezüglich geantwortet und nicht die funktionale Programmierung angesprochen - obwohl man mit PHP die gleiche Frage vom Standpunkt der funktionalen Programmierung aus stellen könnte.
Greg Burghardt

7

Ich weiß, dass ich zu spät zu dieser Party komme, aber ich glaube, ein wichtiger Punkt wird übersehen.

Weshalb sollte ich das tun:

class Profile {
    public function deactivateProfile(Setting $setting)
    {
        $setting->isActive = false;
    }
}

Das solltest du nicht. Aber nicht, weil Dependency Injection eine schlechte Idee ist. Es ist, weil das falsch macht.

Schauen wir uns diese Dinge mit Code an. Wir werden das machen:

$profile = new Profile();
$profile->deactivateProfile($setting);

wenn wir ungefähr das Gleiche daraus machen:

$setting->isActive = false; // Deactivate profile

Es scheint also Zeitverschwendung zu sein. Es ist, wenn Sie es auf diese Weise tun. Dies ist nicht die beste Verwendung der Abhängigkeitsinjektion. Es ist nicht einmal die beste Verwendung einer Klasse.

Was wäre, wenn wir stattdessen Folgendes hätten:

$profile = new Profile($setting);

$application = new Application($profile);

$application.start();

Und jetzt applicationist es kostenlos, das zu aktivieren und zu deaktivieren, profileohne etwas Besonderes darüber wissen zu müssen, was sich settingtatsächlich ändert. Warum ist das gut? Falls Sie die Einstellung ändern müssen. Das applicationist von diesen Änderungen abgeschirmt, so dass Sie in einem sicheren, geschlossenen Raum verrückt werden können, ohne dass Sie zusehen müssen, wie alles kaputt geht, sobald Sie etwas berühren.

Dies folgt dem vom Verhaltensprinzip getrennten Aufbau . Das DI-Muster ist hier einfach. Bauen Sie alles, was Sie brauchen, so niedrig wie möglich, verdrahten Sie sie und starten Sie das gesamte Verhalten mit einem einzigen Aufruf.

Das Ergebnis ist, dass Sie einen separaten Ort haben, um zu entscheiden, was mit was verbunden ist, und einen anderen Ort, um zu verwalten, was was mit was sagt.

Versuchen Sie das an etwas, das Sie im Laufe der Zeit pflegen müssen und sehen Sie, ob es nicht hilft.


6

Wenn Sie als Kunde einen Mechaniker beauftragen, etwas mit Ihrem Auto zu tun, erwarten Sie dann, dass dieser Mechaniker ein Auto von Grund auf neu baut, um dann damit zu arbeiten? Nein, Sie geben dem Mechaniker das Auto, an dem Sie arbeiten möchten .

Wenn Sie als Garagenbesitzer einen Mechaniker beauftragen, etwas mit einem Auto zu tun, erwarten Sie, dass der Mechaniker seinen eigenen Schraubenzieher / Schraubenschlüssel / Autoteile herstellt? Nein, Sie stellen dem Mechaniker die Teile / Werkzeuge zur Verfügung, die er verwenden muss

Warum machen wir das? Nun, denk darüber nach. Sie sind ein Garagenbesitzer, der jemanden einstellen möchte, um Ihr Mechaniker zu werden. Du wirst ihnen beibringen, ein Mechaniker zu sein (= du wirst den Code schreiben).

Was wird einfacher:

  • Bringen Sie einem Mechaniker bei, wie man einen Spoiler mit einem Schraubendreher an einem Auto anbringt.
  • Bringen Sie einem Mechaniker bei, ein Auto zu erstellen, einen Spoiler zu erstellen, einen Schraubendreher zu erstellen und den neu erstellten Spoiler dann mit dem neu erstellten Schraubendreher am neu erstellten Auto zu befestigen.

Es hat enorme Vorteile, wenn Ihr Mechaniker nicht alles von Grund auf neu erstellt:

  • Offensichtlich wird die Ausbildung (= Entwicklung) drastisch verkürzt, wenn Sie Ihren Mechaniker nur mit vorhandenen Werkzeugen und Teilen beliefern.
  • Wenn derselbe Mechaniker mehrere Male denselben Job ausführen muss, können Sie sicherstellen, dass er den Schraubendreher wiederverwendet, anstatt immer den alten herauszuwerfen und einen neuen zu erstellen.
  • Außerdem muss ein Mechaniker, der gelernt hat, alles zu erschaffen, viel mehr ein Experte sein und erwartet daher einen höheren Lohn. Die Codierungsanalogie hier ist, dass eine Klasse mit vielen Verantwortlichkeiten viel schwieriger zu pflegen ist als eine Klasse mit einer einzigen genau definierten Verantwortlichkeit.
  • Wenn neue Erfindungen auf den Markt kommen und Spoiler statt aus Kunststoff jetzt aus Carbon hergestellt werden; Sie müssen Ihren erfahrenen Mechaniker umbilden (= neu entwickeln). Aber Ihr "einfacher" Mechaniker muss nicht umgeschult werden, solange der Spoiler noch auf die gleiche Weise angebracht werden kann.
  • Ein Mechaniker, der sich nicht auf ein selbst gebautes Auto verlässt, bedeutet, dass Sie einen Mechaniker haben, der in der Lage ist, jedes Auto zu handhaben , das er erhält. Einschließlich Autos, die es zum Zeitpunkt der Ausbildung des Mechanikers noch nicht gab. Ihr erfahrener Mechaniker kann jedoch keine neueren Autos bauen, die nach dem Training erstellt wurden.

Wenn Sie einen erfahrenen Mechaniker einstellen und ausbilden, werden Sie einen Mitarbeiter finden, der mehr kostet, mehr Zeit für die Durchführung einer eigentlich einfachen Aufgabe benötigt und immer wieder umgeschult werden muss, wenn eine der vielen Aufgaben erforderlich ist aktualisiert werden.

Die Entwicklungsanalogie lautet: Wenn Sie Klassen mit fest codierten Abhängigkeiten verwenden, werden Sie am Ende schwer zu pflegende Klassen haben, die ( Settingsin Ihrem Fall) immer dann neu entwickelt / geändert werden müssen, wenn eine neue Version des Objekts erstellt wird Sie müssen eine interne Logik entwickeln, damit die Klasse verschiedene Objekttypen erstellen Settingskann.

Darüber hinaus muss jeder, der Ihre Klasse konsumiert, jetzt auch die Klasse auffordern, das richtige Objekt zu erstellen , anstatt der Klasse Settingseinfach jedes SettingsObjekt übergeben zu können, das sie passieren möchte. Dies bedeutet eine zusätzliche Entwicklung für den Verbraucher, um herauszufinden, wie er die Klasse auffordern kann, das richtige Werkzeug zu erstellen.


Ja, die Abhängigkeitsinversion erfordert etwas mehr Aufwand beim Schreiben, anstatt die Abhängigkeit hart zu codieren. Ja, es ist ärgerlich, mehr tippen zu müssen.

Dies ist jedoch dasselbe Argument wie die Auswahl von Hardcode-Literalwerten, da "das Deklarieren von Variablen mehr Aufwand erfordert". Technisch korrekt, aber die Vorteile überwiegen die Nachteile um mehrere Größenordnungen .

Der Vorteil der Abhängigkeitsinversion tritt beim Erstellen der ersten Version der Anwendung nicht auf. Der Vorteil der Abhängigkeitsinversion tritt auf, wenn Sie diese ursprüngliche Version ändern oder erweitern müssen. Und täuschen Sie sich nicht in der Annahme, dass Sie es beim ersten Mal richtig machen und den Code nicht erweitern / ändern müssen. Sie müssen die Dinge ändern.


Beeinträchtigt dies die Gesamtleistung?

Dies wirkt sich nicht auf die Laufzeitleistung der Anwendung aus. Dies wirkt sich jedoch massiv auf die Entwicklungszeit (und damit auf die Leistung) des Entwicklers aus .


2
Wenn Sie den Kommentar " Als kleinerer Kommentar konzentriert sich Ihre Frage auf die Abhängigkeitsinversion und nicht auf die Abhängigkeitsinjektion. Die Injektion ist eine Möglichkeit, die Inversion durchzuführen, aber nicht die einzige. " Entfernt haben , wäre dies eine hervorragende Antwort. Die Inversion der Abhängigkeit kann entweder über Injection oder Locator / Globals erfolgen. Die Beispiele der Frage beziehen sich auf die Injektion. Also die Frage ist über Dependency Injection (sowie Abhängigkeits Inversion).
David Arno

12
Ich denke, das ganze Auto ist ein bisschen voll und verwirrend
Ewan

4
@Flater, ein Teil des Problems ist, dass sich niemand wirklich darüber einig zu sein scheint, worin der Unterschied zwischen Abhängigkeitsinjektion, Abhängigkeitsinversion und Kontrollinversion besteht. Eines ist jedoch sicher, ein "Container" wird mit Sicherheit nicht benötigt, um eine Abhängigkeit in eine Methode oder einen Konstruktor einzufügen. Der reine (oder arme Mann) DI beschreibt speziell die manuelle Abhängigkeitsinjektion. Es ist die einzige Abhängigkeitsinjektion, die ich persönlich verwende, da ich die "Magie", die mit Containern verbunden ist, nicht mag.
David Arno

1
@Flater Das Problem mit dem Beispiel ist, dass Sie mit ziemlicher Sicherheit kein Problem in Code auf diese Weise modellieren würden. Es ist daher unnatürlich, erzwungen und fügt mehr Fragen als Antworten hinzu. Das macht es eher irreführend und verwirrend als demonstrierend.
jpmc26

2
@gbjbaanb: Zu keinem Zeitpunkt befasst sich meine Antwort auch nur aus der Ferne mit Polymorphismus. Es gibt keine Vererbung, Schnittstellenimplementierung oder ähnliches wie das Up- / Downcasting von Typen. Sie verstehen die Antwort völlig falsch.
Flater

0

Wie bei allen Mustern gilt es, nach dem Warum zu fragen, um aufgeblähte Designs zu vermeiden.

Für die Abhängigkeitsinjektion ist dies leicht zu erkennen, wenn man sich die beiden wohl wichtigsten Facetten des OOP-Designs vor Augen hält ...

Geringe Kopplung

Kopplung in der Computerprogrammierung :

In der Softwareentwicklung ist Kopplung der Grad der gegenseitigen Abhängigkeit zwischen Softwaremodulen. ein Maß dafür, wie eng zwei Routinen oder Module miteinander verbunden sind; die Stärke der Beziehungen zwischen Modulen.

Sie möchten eine geringe Kopplung erreichen. Zwei Dinge sind eng miteinander verbunden: Wenn Sie das eine ändern, müssen Sie höchstwahrscheinlich das andere ändern. Bugs oder Restriktionen in der einen führen wahrscheinlich zu Bugs / Restriktionen in der anderen. und so weiter.

Eine Klasse, die Objekte der anderen instanziiert, ist eine sehr starke Kopplung, weil die eine über die Existenz der anderen Bescheid wissen muss; Es muss wissen, wie es instanziiert werden soll (welche Argumente der Konstruktor benötigt), und diese Argumente müssen verfügbar sein, wenn der Konstruktor aufgerufen wird. Abhängig davon, ob die Sprache explizit dekonstruiert werden muss (C ++), führt dies zu weiteren Komplikationen. Wenn Sie neue Klassen einführen (z. B. a NextSettingsoder was auch immer), müssen Sie zur ursprünglichen Klasse zurückkehren und ihren Konstruktoren weitere Aufrufe hinzufügen.

Hoher Zusammenhalt

Zusammenhalt :

In der Computerprogrammierung bezieht sich Kohäsion auf den Grad, zu dem die Elemente innerhalb eines Moduls zusammengehören.

Dies ist die andere Seite der Medaille. Wenn Sie sich eine Codeeinheit (eine Methode, eine Klasse, ein Paket usw.) ansehen, möchten Sie, dass sich der gesamte Code in dieser Einheit befindet, um so wenig Verantwortlichkeiten wie möglich zu haben.

Ein grundlegendes Beispiel hierfür wäre das MVC-Muster: Sie trennen das Domänenmodell klar von der Ansicht (GUI) und einer Kontrollschicht, die diese zusammenhält.

Dies vermeidet das Aufblähen von Code, wenn Sie große Stücke erhalten, die viele verschiedene Aufgaben ausführen. Wenn Sie einen Teil davon ändern möchten, müssen Sie auch alle anderen Funktionen im Auge behalten und versuchen, Fehler usw. zu vermeiden. und Sie programmieren sich schnell in ein Loch, in dem es sehr schwer ist, herauszukommen.

Mit der Abhängigkeitsinjektion delegieren Sie das Erstellen oder Verfolgen von Abhängigkeiten an alle Klassen (oder Konfigurationsdateien), die Ihre DI implementieren. Andere Klassen werden nicht viel kümmern , was genau los ist - sie werden mit einigen generischen Schnittstellen arbeiten und haben keine Ahnung , was die tatsächliche Umsetzung ist, das heißt , sie sind nicht verantwortlich für die anderen Sachen.


-1

Sie sollten Techniken verwenden, um die Probleme zu lösen, die sie gut lösen können, wenn Sie diese Probleme haben. Abhängigkeitsinversion und Injektion sind nicht unterschiedlich.

Abhängigkeitsinversion oder -injektion ist eine Technik, mit der Ihr Code entscheiden kann, welche Implementierung einer Methode zur Laufzeit aufgerufen wird. Dies maximiert die Vorteile einer späten Bindung. Die Technik ist erforderlich, wenn die Sprache das Ersetzen von Nicht-Instanz-Funktionen zur Laufzeit nicht unterstützt. Beispielsweise fehlt in Java ein Mechanismus, um Aufrufe einer statischen Methode durch Aufrufe einer anderen Implementierung zu ersetzen. Im Gegensatz zu Python müssen Sie zum Ersetzen des Funktionsaufrufs nur den Namen an eine andere Funktion binden (die Variable, die die Funktion enthält, neu zuweisen).

Warum sollten wir die Implementierung der Funktion variieren wollen? Es gibt zwei Hauptgründe:

  • Wir wollen Fälschungen zu Testzwecken verwenden. Auf diese Weise können wir eine Klasse testen, die von einem Datenbankabruf abhängt, ohne tatsächlich eine Verbindung zur Datenbank herzustellen.
  • Wir müssen mehrere Implementierungen unterstützen. Beispielsweise müssen wir möglicherweise ein System einrichten, das sowohl MySQL- als auch PostgreSQL-Datenbanken unterstützt.

Möglicherweise möchten Sie auch die Inversion von Steuercontainern beachten. Dies ist eine Technik, die Ihnen helfen soll, riesige, verworrene Konstruktionsbäume zu vermeiden, die wie folgt aussehen:

thing5 =  new MyThing5();
thing3 = new MyThing3(thing5, new MyThing10());

myApp = new MyApp(
    new MyAppDependency1(thing5, thing3),
    new MyAppDependency2(
        new Thing1(),
        new Thing2(new Thing3(thing5, new Thing4(thing5)))
    ),
    ...
    new MyAppDependency15(thing5)
);

Sie können Ihre Klassen registrieren und dann die Konstruktion für Sie durchführen:

injector.register(Thing1); // Yes, you'd need some kind of actual class reference.
injector.register(Thing2);
...
injector.register(MyAppDepdency15);
injector.register(MyApp);

myApp = injector.create(MyApp); // The injector fills in all the construction parameters.

Beachten Sie, dass es am einfachsten ist, wenn die registrierten Klassen zustandslose Singletons sein können .

Achtung!

Beachten Sie, dass die Abhängigkeitsinversion nicht Ihre erste Antwort für die Entkopplungslogik sein sollte. Suchen Sie nach Möglichkeiten, stattdessen die Parametrisierung zu verwenden. Betrachten Sie diese Pseudocode-Methode zum Beispiel:

myAverageAboveMin()
{
    dbConn = new DbConnection("my connection string");
    dbQuery = dbConn.makeQuery();
    dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
    dbQuery.setParam("min", 5);
    dbQuery.Execute();
    myData = dbQuery.getAll();
    count = 0;
    total = 0;
    foreach (row in myData)
    {
        count++;
        total += row.x;
    }

    return total / count;
}

Wir könnten die Abhängigkeitsinversion für einige Teile dieser Methode verwenden:

class MyQuerier
{
    private _dbConn;

    MyQueries(dbConn) { this._dbConn = dbConn; }

    fetchAboveMin(min)
    {
        dbQuery = this._dbConn.makeQuery();
        dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
        dbQuery.setParam("min", min);
        dbQuery.Execute();
        return dbQuery.getAll();
    }
}


class Averager
{
    private _querier;

    Averager(querier) { this._querier = querier; }

    myAverageAboveMin(min)
    {
        myData = this._querier.fetchAboveMin(min);
        count = 0;
        total = 0;
        foreach (row in myData)
        {
            count++;
            total += row.x;
        }

        return total / count;
    }

Das sollten wir aber zumindest nicht ganz. Beachten Sie, dass wir mit eine stateful- Klasse erstellt haben Querier. Es enthält jetzt einen Verweis auf ein im Wesentlichen globales Verbindungsobjekt. Dies führt zu Problemen wie dem schwierigen Verständnis des Gesamtzustands des Programms und der Koordinierung der verschiedenen Klassen. Beachten Sie auch, dass wir gezwungen sind, den Abfrager oder die Verbindung auszutricksen, wenn wir die Mittelungslogik testen möchten. Ein besserer Ansatz wäre, die Parametrisierung zu erhöhen :

class MyQuerier
{
    fetchAboveMin(dbConn, min)
    {
        dbQuery = dbConn.makeQuery();
        dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
        dbQuery.setParam("min", min);
        dbQuery.Execute();
        return dbQuery.getAll();
    }
}


class Averager
{
    averageData(myData)
    {
        count = 0;
        total = 0;
        foreach (row in myData)
        {
            count++;
            total += row.x;
        }

        return total / count;
    }

class StuffDoer
{
    private _querier;
    private _averager;

    StuffDoer(querier, averager)
    {
        this._querier = querier;
        this._averager = averager;
    }

    myAverageAboveMin(dbConn, min)
    {
        myData = this._querier.fetchAboveMin(dbConn, min);
        return this._averager.averageData(myData);
    }
}

Und die Verbindung würde auf einer noch höheren Ebene verwaltet, die für den gesamten Vorgang verantwortlich ist und weiß, was mit dieser Ausgabe zu tun ist.

Jetzt können wir die Mittelungslogik völlig unabhängig von der Abfrage testen und sie darüber hinaus in einer Vielzahl von Situationen einsetzen. Wir könnten uns fragen, ob wir überhaupt die MyQuerierund Averager-Objekte benötigen , und vielleicht lautet die Antwort, dass dies nicht der Fall ist, wenn wir nicht beabsichtigen, einen Komponententest durchzuführen StuffDoer, und ein Komponententest StuffDoerwäre absolut vernünftig, da er so eng mit der Datenbank verbunden ist. Es könnte sinnvoller sein, sich nur von Integrationstests abdecken zu lassen. In diesem Fall könnten wir fein machen sein fetchAboveMinund averageDatain statischen Methoden.


2
"Die Abhängigkeitsinjektion ist eine Technik, die Ihnen helfen soll, riesige, verworrene Konstruktionsbäume zu vermeiden ... ". Ihr erstes Beispiel nach dieser Behauptung ist ein Beispiel für die Abhängigkeitsinjektion eines reinen oder armen Mannes. Die zweite ist ein Beispiel für die Verwendung eines IoC-Containers, um das Einfügen dieser Abhängigkeiten zu "automatisieren". Beides sind Beispiele für die Abhängigkeitsinjektion in Aktion.
David Arno

@ DavidArno Ja, du hast recht. Ich habe die Terminologie angepasst.
jpmc26

Es gibt einen dritten Hauptgrund, die Implementierung zu ändern oder zumindest den Code so zu gestalten, dass davon ausgegangen wird, dass sich die Implementierung ändern kann: Dies führt dazu, dass Entwickler eine lose Kopplung haben und vermeiden, dass Code geschrieben wird, der schwer zu ändern sein wird, wenn irgendwann eine neue Implementierung implementiert wird die Zukunft. Und während dies in einigen Projekten möglicherweise keine Priorität hat (z. B. zu wissen, dass die Anwendung nach ihrer ersten Veröffentlichung niemals mehr überprüft wird), ist dies in anderen Projekten der Fall (z. B. wenn das Geschäftsmodell des Unternehmens speziell versucht, eine erweiterte Unterstützung / Erweiterung ihrer Anwendungen anzubieten) ).
Flater

@Flater Dependency Injection führt immer noch zu einer starken Kopplung. Es bindet die Logik an eine bestimmte Schnittstelle und erfordert, dass der betreffende Code über alle Funktionen dieser Schnittstelle informiert ist. Betrachten Sie Code, der aus einer Datenbank abgerufene Ergebnisse umwandelt. Wenn ich DI verwende, um sie zu trennen, muss der Code dennoch wissen, dass ein DB-Abruf stattfindet, und ihn aufrufen. Die bessere Lösung ist, wenn der Transformationscode nicht einmal weiß, dass ein Abruf stattfindet . Sie können dies nur tun, wenn die Ergebnisse des Abrufs von einem Aufrufer übergeben werden, anstatt den Abrufer zu injizieren.
jpmc26

1
@ jpmc26 Aber in deinem (Kommentar-) Beispiel musste die Datenbank nicht unbedingt eine Abhängigkeit sein. Das Vermeiden von Abhängigkeiten ist natürlich besser für lose Kopplungen, aber DI konzentriert sich auf das Implementieren von Abhängigkeiten, die benötigt werden.
Flater
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.