Was sind die Vorteile der Abhängigkeitsinjektion in Fällen, in denen fast jeder Zugriff auf eine gemeinsame Datenstruktur benötigt?


20

Es gibt viele Gründe, warum Globale in OOP böse sind .

Wenn die Anzahl oder Größe der Objekte, die gemeinsam genutzt werden müssen, zu groß ist, um effizient in Funktionsparametern weitergegeben zu werden, empfiehlt normalerweise jeder die Abhängigkeitsinjektion anstelle eines globalen Objekts.

In dem Fall, in dem fast jeder eine bestimmte Datenstruktur kennen muss, warum ist Dependency Injection besser als ein globales Objekt?

Beispiel (ein vereinfachtes Beispiel , um den Punkt allgemein darzustellen, ohne zu tief in eine bestimmte Anwendung einzutauchen)

Es gibt eine Reihe von virtuellen Fahrzeugen mit einer Vielzahl von Eigenschaften und Zuständen, von Typ, Name, Farbe bis zu Geschwindigkeit, Position usw. Eine Reihe von Benutzern kann sie fernsteuern, und eine Vielzahl von Ereignissen (beides initiiert und automatisch) können viele ihrer Zustände oder Eigenschaften ändern.

Die naive Lösung wäre, einfach einen globalen Container daraus zu machen

vector<Vehicle> vehicles;

die von überall zugegriffen werden kann.

Die OOP-freundlichere Lösung wäre, dass der Container Mitglied der Klasse ist, die die Hauptereignisschleife behandelt, und in seinem Konstruktor instanziiert wird. Jede Klasse, die es benötigt und Mitglied des Hauptthreads ist, erhält über einen Zeiger in ihrem Konstruktor Zugriff auf den Container. Wenn beispielsweise eine externe Nachricht über eine Netzwerkverbindung eingeht, übernimmt eine Klasse (eine für jede Verbindung), die das Parsing ausführt, und der Parser hat über einen Zeiger oder eine Referenz Zugriff auf den Container. Wenn die analysierte Nachricht zu einer Änderung in einem Element des Containers führt oder einige Daten benötigt, um eine Aktion auszuführen, kann sie verarbeitet werden, ohne dass Tausende von Variablen durch Signale und Slots geworfen werden müssen (oder schlimmer noch, Speichern Sie sie im Parser, damit sie später von demjenigen abgerufen werden können, der den Parser aufgerufen hat. Natürlich sind alle Klassen, die über Dependency Injection Zugriff auf den Container erhalten, Teil desselben Threads. Verschiedene Threads greifen nicht direkt darauf zu, sondern erledigen ihre Aufgabe und senden dann Signale an den Haupt-Thread. Die Slots im Haupt-Thread aktualisieren den Container.

Wenn jedoch die Mehrheit der Klassen Zugriff auf den Container erhält, was unterscheidet ihn dann wirklich von einem globalen? Wenn so viele Klassen die Daten im Container benötigen, ist die "Abhängigkeitsinjektionsmethode" dann nicht nur eine verschleierte globale Methode?

Eine Antwort wäre Thread-Sicherheit: Auch wenn ich darauf achte, den globalen Container nicht zu missbrauchen, wird möglicherweise ein anderer Entwickler in Zukunft unter dem Druck einer kurzen Frist den globalen Container in einem anderen Thread verwenden, ohne sich um alles zu kümmern die Kollisionsfälle. Selbst im Fall der Abhängigkeitsinjektion kann man jedoch einen Zeiger auf jemanden geben, der in einem anderen Thread ausgeführt wird, was zu denselben Problemen führt.


6
Warten Sie, Sie sprechen von nicht veränderlichen Globalen und verwenden einen Link zu "Warum ist der globale Zustand schlecht?", Um dies zu rechtfertigen. WTF?
Telastyn

1
Ich rechtfertige überhaupt nicht Globals. Ich denke, es ist klar, dass ich der Tatsache zustimme, dass Globals keine gute Lösung sind. Ich spreche nur von einer Situation, in der selbst die Verwendung von Abhängigkeitsinjektion nicht viel besser sein kann als (oder vielleicht sogar fast nicht zu unterscheiden von) Globals. So eine Antwort noch oder hinweisen könnten andere versteckte Vorteile der Dependency Injection in dieser Situation verwendet wird , dass andere Ansätze besser als di wäre
VSZ

9
Es gibt jedoch einen entscheidenden Unterschied zwischen schreibgeschützten globalen Elementen und dem globalen Status.
Telastyn


1
@vsz nicht wirklich - die Frage bezieht sich auf Service Locator-Fabriken, um das Problem vieler unterschiedlicher Objekte zu umgehen. Ihre Antworten beantworten jedoch auch Ihre Frage, ob die Klassen auf die globalen Daten zugreifen dürfen, anstatt die globalen Daten an die Klassen weiterzuleiten.
Gbjbaanb

Antworten:


37

Warum ist Dependency Injection besser als ein globales Objekt, wenn fast jeder etwas über eine bestimmte Datenstruktur wissen muss?

Abhängigkeitsinjektion ist die beste Sache seit geschnittenem Brot , während globale Objekte seit Jahrzehnten dafür bekannt sind, die Quelle allen Übels zu sein , daher ist dies eine ziemlich interessante Frage.

Der Punkt der Abhängigkeitsinjektion besteht nicht einfach darin, sicherzustellen, dass jeder Akteur, der eine Ressource benötigt, diese haben kann, denn wenn Sie alle Ressourcen globalisieren, hat natürlich jeder Akteur Zugriff auf jede Ressource, das Problem ist gelöst, oder?

Der Punkt der Abhängigkeitsinjektion ist:

  1. Den Akteuren den bedarfsgerechten Zugang zu Ressourcen zu ermöglichen und
  2. Kontrolle darüber zu haben, auf welche Instanz einer Ressource ein bestimmter Akteur zugreift.

Die Tatsache, dass in Ihrer speziellen Konfiguration alle Akteure Zugriff auf dieselbe Ressourceninstanz benötigen, ist irrelevant. Vertrauen Sie mir, eines Tages müssen Sie die Dinge neu konfigurieren, damit die Akteure Zugriff auf verschiedene Instanzen der Ressource haben, und dann werden Sie feststellen, dass Sie sich selbst in eine Ecke gemalt haben. Einige Antworten haben bereits auf eine solche Konfiguration hingewiesen: Testen .

Ein weiteres Beispiel: Angenommen, Sie teilen Ihre Anwendung in Client-Server auf. Alle Akteure auf dem Client verwenden denselben Satz zentraler Ressourcen auf dem Client, und alle Akteure auf dem Server verwenden denselben Satz zentraler Ressourcen auf dem Server. Angenommen, Sie möchten eines Tages eine "eigenständige" Version Ihrer Client-Server-Anwendung erstellen, bei der sowohl der Client als auch der Server in einer einzigen ausführbaren Datei zusammengefasst sind und auf derselben virtuellen Maschine ausgeführt werden. (Oder Laufzeitumgebung, abhängig von der Sprache Ihrer Wahl.)

Wenn Sie die Abhängigkeitsinjektion verwenden, können Sie auf einfache Weise sicherstellen, dass alle Client-Akteure die Client-Ressourceninstanzen erhalten, mit denen sie arbeiten können, während alle Server-Akteure die Server-Ressourceninstanzen erhalten.

Wenn Sie keine Abhängigkeitsinjektion verwenden, haben Sie kein Glück, da in einer virtuellen Maschine nur eine globale Instanz jeder Ressource vorhanden sein kann.

Dann muss man bedenken: Brauchen wirklich alle Akteure Zugang zu dieser Ressource? Ja wirklich?

Möglicherweise haben Sie den Fehler begangen, diese Ressource in ein Gott-Objekt zu verwandeln (so muss natürlich jeder darauf zugreifen können), oder Sie überschätzen die Anzahl der Akteure in Ihrem Projekt, die tatsächlich Zugriff auf diese Ressource benötigen, grob .

Mit Globals hat jede einzelne Codezeile in Ihrer gesamten Anwendung Zugriff auf jede einzelne globale Ressource. Bei der Abhängigkeitsinjektion ist jede Ressourceninstanz nur für diejenigen Akteure sichtbar, die sie tatsächlich benötigen. Wenn die beiden identisch sind (die Akteure, die eine bestimmte Ressource benötigen, machen 100% der Quellcodezeilen in Ihrem Projekt aus), müssen Sie einen Fehler in Ihrem Entwurf gemacht haben. Also entweder

  • Zerlegen Sie diese große, große, große Gott-Ressource in kleinere Subressourcen, sodass verschiedene Schauspieler Zugriff auf verschiedene Teile davon benötigen, aber selten benötigt ein Schauspieler alle Teile davon oder

  • Überarbeiten Sie Ihre Akteure so, dass sie nur die Teilmengen des Problems als Parameter akzeptieren, an denen sie arbeiten müssen, damit sie nicht ständig auf eine große, große, zentrale Ressource zurückgreifen müssen.


Ich bin mir nicht sicher, ob ich das verstehe. Einerseits sagen Sie, dass Sie mit DI mehrere Instanzen einer Ressource verwenden können und mit Globals nicht. Dies ist einfach Unsinn, es sei denn, Sie konfligieren eine einzelne statische Variable mit mehrfach instanziierten Objekten - es gibt keinen Grund dass "das globale" entweder eine einzelne Instanz oder eine einzelne Klasse sein muss.
gbjbaanb,

3
@gbjbaanb Angenommen, die Ressource ist ein Zork. Das OP sagt, dass nicht nur jedes einzelne Objekt in seinem System einen Zork benötigt, sondern auch, dass es immer nur einen Zork geben wird und dass alle seine Objekte nur auf diese eine Instanz von Zork zugreifen müssen. Ich sage, dass keine dieser beiden Annahmen vernünftig ist: Es ist wahrscheinlich nicht wahr, dass alle seine Objekte einen Zork benötigen, und es wird Zeiten geben, in denen einige der Objekte eine bestimmte Instanz von Zork benötigen, während andere Objekte einen benötigen andere Instanz von Zork.
Mike Nakis

2
@gbjbaanb Ich nehme nicht an, dass Sie mich bitten, zu erklären, warum es dumm wäre, zwei globale Instanzen von Zork zu haben, sie ZorkA und ZorkB zu nennen und die Objekte, die ZorkA und ZorkB verwenden werden, fest zu codieren. Recht?
Mike Nakis

1
Ich habe nicht alle gesagt, ich habe fast alle gesagt . Der Punkt der Frage war, ob DI neben der Verweigerung des Zugangs von den wenigen Akteuren, die ihn nicht benötigen, noch andere Vorteile hat. Ich weiß, dass "Gottobjekte" ein Anti-Muster sind, aber es kann auch eines sein, wenn man Best Practices blind befolgt. Es kann vorkommen, dass der gesamte Zweck eines Programms darin besteht, verschiedene Aufgaben mit einer bestimmten Ressource auszuführen. In diesem Fall benötigt fast jeder Zugriff auf diese Ressource. Ein gutes Beispiel wäre ein Programm, das an einem Bild arbeitet: Fast alles darin hat etwas mit den Bilddaten zu tun.
vsz

1
@gbjbaanb Ich glaube, es ist die Idee, eine Client-Klasse zu haben, die in der Lage ist, ausschließlich mit ZorkA oder ZorkB zu arbeiten, und nicht, dass die Client-Klasse entscheidet, welche von ihnen ergriffen werden soll.
Eugene Ryabtsev

39

Es gibt viele Gründe, warum nicht veränderliche Globale in OOP böse sind.

Das ist eine zweifelhafte Behauptung. Der Link , den Sie als Beweismittel verwenden bezieht sich auf Zustand - wandelbar Globals. Sie sind entschieden böse. Read only Globals sind nur Konstanten. Konstanten sind relativ gesund. Ich meine, Sie werden nicht in all Ihren Klassen den Wert von pi angeben, oder?

Wenn die Anzahl oder Größe der Objekte, die gemeinsam genutzt werden müssen, zu groß ist, um effizient in Funktionsparametern weitergegeben zu werden, empfiehlt normalerweise jeder die Abhängigkeitsinjektion anstelle eines globalen Objekts.

Nein, das tun sie nicht.

Wenn Ihre Funktionen / Klassen zu viele Abhängigkeiten aufweisen, empfehlen die Benutzer, anzuhalten und zu prüfen, warum Ihr Design so schlecht ist. Eine Schiffsladung von Abhängigkeiten ist ein Zeichen dafür, dass Ihre Klasse / Funktion wahrscheinlich zu viele Aufgaben ausführt und / oder dass Sie keine ausreichende Abstraktion haben.

Es gibt eine Reihe von virtuellen Fahrzeugen mit einer Vielzahl von Eigenschaften und Zuständen, von Typ, Name, Farbe bis zu Geschwindigkeit, Position usw. Eine Reihe von Benutzern kann sie fernsteuern, und eine Vielzahl von Ereignissen (beides initiiert und automatisch) können viele ihrer Zustände oder Eigenschaften ändern.

Dies ist ein Design-Albtraum. Sie haben eine Reihe von Dingen, die ohne jede Validierung und ohne jede Einschränkung der Parallelität verwendet / missbraucht werden können - es ist nur eine vollständige Kopplung.

Wenn jedoch die Mehrheit der Klassen Zugriff auf den Container erhält, was unterscheidet ihn dann wirklich von einem globalen? Wenn so viele Klassen die Daten im Container benötigen, ist die "Abhängigkeitsinjektionsmethode" dann nicht nur eine verschleierte globale Methode?

Ja, mit Kopplung an gemeinsamen Daten überall ist nicht allzu verschieden von einem globalen, und als solche immer noch schlecht.

Der Vorteil ist, dass Sie die Verbraucher von der globalen Variablen selbst entkoppeln. Dies bietet Ihnen einige Flexibilität, wie die Instanz erstellt wird. Es zwingt Sie auch nicht dazu , nur eine Instanz zu haben. Sie können verschiedene Produkte erstellen, die an die verschiedenen Verbraucher weitergegeben werden. Sie können dann auch verschiedene Implementierungen Ihrer Benutzeroberfläche pro Benutzer erstellen, falls dies erforderlich sein sollte.

Und aufgrund der Veränderungen, die unvermeidlich mit den sich wandelnden Anforderungen von Software einhergehen, ist ein derart grundlegendes Maß an Flexibilität von entscheidender Bedeutung .


entschuldigung für die verwechslung mit "nicht veränderlich", wollte ich veränderlich sagen und habe meinen fehler irgendwie nicht erkannt.
vsz

"Dies ist ein Design-Albtraum" - ich weiß, dass es so ist. Aber wenn die Voraussetzungen sind , dass alle diese Ereignisse müssen die Lage sein , die Änderungen in den Daten durchzuführen, werden die Ereignisse auf die Daten gekoppelt werden , auch wenn ich gespalten und verstecke sie unter einer Schicht von Abstraktionen. Das ganze Programm handelt von diesen Daten. Wenn ein Programm beispielsweise viele verschiedene Operationen an einem Bild ausführen muss, werden alle oder fast alle seiner Klassen irgendwie mit den Bilddaten gekoppelt. Nur zu sagen "Lass uns ein Programm schreiben, das etwas anderes macht" ist nicht akzeptabel.
vsz

2
Anwendungen mit vielen Objekten, die routinemäßig auf eine Datenbank zugreifen, sind nicht gerade beispiellos.
Robert Harvey

@RobertHarvey - sicher, aber ist die Schnittstelle, von der sie abhängen, "auf eine Datenbank zugreifen können" oder "auf einen persistenten Datenspeicher für foos"?
Telastyn

1
Erinnert mich an einen Dilbert, bei dem PHB nach 500 Features fragt und Dilbert Nein sagt. Also fragt PHB nach 1 Feature und Dilbert sagt sicher. Am nächsten Tag erhält Dilbert jeweils 500 Anfragen für 1 Feature. Wenn etwas nicht skaliert, skaliert es einfach nicht, egal wie du es anziehst.
CorsiKa

16

Es gibt drei Hauptgründe, die Sie berücksichtigen sollten.

  1. Lesbarkeit. Wenn jede Codeeinheit über alles verfügt, was sie als Parameter bearbeiten oder übergeben muss, ist es einfach, den Code zu betrachten und sofort zu sehen, was er tut. Dies gibt Ihnen eine Lokalität der Funktion, die es Ihnen auch ermöglicht, Bedenken besser zu trennen und Sie auch zum Nachdenken über ...
  2. Modularität. Warum sollte der Parser über die gesamte Liste der Fahrzeuge und deren Eigenschaften Bescheid wissen? Das tut es wahrscheinlich nicht. Möglicherweise muss abgefragt werden, ob eine Fahrzeug-ID vorhanden ist oder ob Fahrzeug X die Eigenschaft Y hat. Großartig, das heißt, Sie können einen Dienst schreiben, der dies tut, und nur diesen Dienst in Ihren Parser einfügen. Plötzlich erhalten Sie ein Design, das weitaus sinnvoller ist, da jedes Codebit nur Daten verarbeitet, die für dieses relevant sind. Welches führt uns zu ...
  3. Testbarkeit. Für einen typischen Komponententest möchten Sie Ihre Umgebung für viele verschiedene Szenarien einrichten, und die Implementierung ist sehr einfach. Möchten Sie, um noch einmal auf das Parser-Beispiel zurückzukommen, wirklich immer eine vollständige Liste vollwertiger Fahrzeuge für jeden Testfall erstellen, den Sie für Ihren Parser schreiben? Oder würden Sie einfach eine Scheinimplementierung des oben genannten Serviceobjekts erstellen und dabei eine unterschiedliche Anzahl von IDs zurückgeben? Ich weiß, welches ich wählen würde.

Um es zusammenzufassen: DI ist kein Ziel, es ist nur ein Werkzeug, mit dem es möglich ist, ein Ziel zu erreichen. Wenn Sie es missbrauchen (wie es in Ihrem Beispiel der Fall ist), werden Sie dem Ziel nicht näher kommen. Es gibt keine magische Eigenschaft von DI, die ein schlechtes Design gut macht.


+1 für eine präzise Antwort auf Wartungsprobleme. Weitere P: SE-Antworten sollten so lauten.
dodgethesteamroller

1
Ihre Summe ist so gut. Soooo gut. Wenn Sie glauben, dass [Entwurfsmuster des Monats] oder [Modewort des Monats] Ihre Probleme auf magische Weise lösen, werden Sie eine schlechte Zeit haben.
corsiKa

@corsiKa Ich war DI (oder genauer gesagt Spring) lange abgeneigt, weil ich den Grund dafür nicht erkennen konnte. Es schien nur eine Menge Ärger ohne spürbaren Gewinn (dies war in den Tagen der XML-Konfiguration zurück). Ich wünschte, ich hätte eine Dokumentation darüber gefunden, was DI Sie damals tatsächlich gekauft hat. Aber ich habe es nicht getan, also musste ich es selbst herausfinden. Es dauerte eine lange Zeit. :)
biziclop

3

Es geht wirklich um Testbarkeit - normalerweise ist ein globales Objekt sehr wartbar und leicht zu lesen / verstehen (wenn Sie es erst einmal kennen, natürlich), aber es ist ein fester Bestandteil, und wenn Sie Ihre Klassen in isolierte Tests aufteilen, stellen Sie fest, dass Ihre Das globale Objekt steckt fest und sagt "Was ist mit mir?". Daher erfordern Ihre isolierten Tests, dass das globale Objekt in ihnen enthalten ist. Es hilft nicht, dass die meisten Test-Frameworks dieses Szenario nicht einfach unterstützen.

Also ja, es ist immer noch global. Der grundlegende Grund dafür hat sich nicht geändert, nur die Art und Weise, wie darauf zugegriffen wird.

Was die Initialisierung betrifft, ist dies immer noch ein Problem. Sie müssen die Konfiguration immer noch so einstellen, dass Ihre Tests ausgeführt werden können, sodass Sie dort nichts gewinnen. Ich nehme an, Sie könnten ein großes abhängiges Objekt in viele kleinere Objekte aufteilen und das entsprechende an die Klassen übergeben, die es benötigen, aber Sie werden wahrscheinlich noch komplexer, wenn Sie die Vorteile überwiegen.

Unabhängig davon muss getestet werden, wie die Codebasis aufgebaut ist (ähnliche Probleme treten bei Elementen wie statischen Klassen auf, z. B. aktuelle Datums- und Uhrzeitangaben).

Persönlich ziehe ich es vor, alle meine globalen Daten in ein globales Objekt (oder mehrere) zu packen, aber Zugriffsmethoden bereitzustellen, um die Bits zu erhalten, die meine Klassen benötigen. Dann kann ich diejenigen verspotten, um die Daten zurückzugeben, die ich beim Testen ohne die zusätzliche Komplexität von DI haben möchte.


"Normalerweise ist ein globales Objekt sehr wartbar" - nicht, wenn es veränderlich ist. Nach meiner Erfahrung kann es ein Albtraum sein, einen so wandelbaren globalen Staat zu haben (obwohl es Möglichkeiten gibt, ihn erträglich zu machen).
Sleske

3

Zusätzlich zu den Antworten, die Sie bereits haben,

Es gibt eine Reihe von virtuellen Fahrzeugen mit einer Vielzahl von Eigenschaften und Zuständen, von Typ, Name, Farbe bis zu Geschwindigkeit, Position usw. Eine Reihe von Benutzern kann sie fernsteuern, und eine Vielzahl von Ereignissen (beides initiiert und automatisch) können viele ihrer Zustände oder Eigenschaften ändern.

Eines der Merkmale der OOP-Programmierung ist, nur die Eigenschaften beizubehalten, die Sie wirklich für ein bestimmtes Objekt benötigen.

WENN Ihr Objekt zu viele Eigenschaften hat, sollten Sie Ihr Objekt weiter in Unterobjekte aufteilen.

Bearbeiten

Bereits erwähnt, warum globale Objekte schlecht sind, möchte ich ein Beispiel hinzufügen.

Sie müssen Unit-Tests mit dem GUI-Code eines Windows-Formulars durchführen. Wenn Sie global verwenden, müssen Sie alle Schaltflächen und Ereignisse erstellen, um beispielsweise einen richtigen Testfall zu erstellen.


Richtig, aber zum Beispiel muss der Parser alles wissen, da möglicherweise ein Befehl empfangen wird, der Änderungen an allem vornehmen kann.
vsz

1
"Bevor ich zu deiner eigentlichen Frage komme" ... wirst du seine Frage beantworten?
gbjbaanb

@ gbjbaanb Frage hat eine Menge Text, bitte erlauben Sie mir etwas Zeit, um es richtig zu lesen
Mathematik
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.