it-swarm.com.de

Muss die Abhängigkeitsinjektion auf Kosten der Verkapselung gehen?

Wenn ich das richtig verstehe, besteht der typische Mechanismus für die Abhängigkeitsinjektion darin, entweder über einen Klassenkonstruktor oder über eine öffentliche Eigenschaft (Member) der Klasse zu injizieren.

Dies macht die Abhängigkeit, die injiziert wird, sichtbar und verletzt das OOP - Prinzip der Einkapselung.

Bin ich richtig darin, diesen Kompromiss zu identifizieren? Wie gehen Sie mit diesem Problem um?

Bitte sehen Sie auch meine Antwort auf meine eigene Frage unten.

117
urig

Es gibt eine andere Sicht auf dieses Problem, die Sie vielleicht interessieren könnte.

Wenn wir IoC/Dependency Injection verwenden, verwenden wir keine OOP Konzepte. Zugegebenermaßen verwenden wir eine OO-Sprache als 'Host', aber die Ideen hinter IoC stammen von komponentenorientiertem Software-Engineering und nicht von OO.

Bei der Komponentensoftware geht es um das Verwalten von Abhängigkeiten - ein Beispiel, das allgemein verwendet wird, ist der Assembly-Mechanismus von .NET. Jede Assembly veröffentlicht die Liste der Assemblys, auf die sie verweist, wodurch das Zusammenstellen (und Validieren) der für eine ausgeführte Anwendung erforderlichen Elemente erheblich vereinfacht wird.

Durch die Anwendung ähnlicher Techniken in unseren OO -Programmen über IoC möchten wir die Konfiguration und Wartung von Programmen vereinfachen. Das Veröffentlichen von Abhängigkeiten (als Konstruktorparameter oder was auch immer) ist ein wesentlicher Bestandteil davon. Encapsulation trifft nicht wirklich zu, da es in der komponenten- und serviceorientierten Welt keinen "Implementierungstyp" gibt, für den Details auslaufen können.

Leider trennen unsere Sprachen derzeit die feinkörnigen, objektorientierten Konzepte nicht von den grobkörnigen komponentenorientierten, daher ist dies eine Unterscheidung, die Sie nur im Kopf behalten müssen :)

59

Es ist eine gute Frage - aber irgendwann muss die Einkapselung in ihrer reinsten Form need verletzt werden, wenn das Objekt jemals seine Abhängigkeit erfüllt. Einige Provider der Abhängigkeit müssen wissen, dass das betreffende Objekt einen Foobenötigt, und der Provider muss die Möglichkeit haben, den Foodem Objekt zur Verfügung zu stellen.

Klassisch wird dieser letzte Fall so behandelt, wie Sie es sagen, durch Konstruktorargumente oder Setter-Methoden. Dies ist jedoch nicht unbedingt der Fall. Ich weiß, dass Sie mit den neuesten Versionen des Spring DI-Frameworks in Java beispielsweise private Felder (z. B. mit @Autowired) kommentieren können, und die Abhängigkeit wird über die Reflektion festgelegt, ohne dass Sie die Abhängigkeit offenlegen müssen durch eine der öffentlichen Methoden/Konstruktoren der Klassen. Dies könnte die Art von Lösung sein, nach der Sie gesucht haben.

Das heißt, ich glaube nicht, dass die Injektion von Konstruktoren auch ein großes Problem darstellt. Ich war immer der Meinung, dass Objekte nach der Konstruktion vollständig gültig sein sollten, sodass alles, was sie zur Wahrnehmung ihrer Rolle (d. H. In einem gültigen Status) benötigen, ohnehin durch den Konstruktor bereitgestellt werden sollte. Wenn Sie über ein Objekt verfügen, für das ein Mitbearbeiter erforderlich ist, scheint es mir gut, dass der Konstruktor diese Anforderung öffentlich bekannt macht und sicherstellt, dass sie erfüllt ist, wenn eine neue Instanz der Klasse erstellt wird.

Im Idealfall interagieren Sie mit Objekten ohnehin über eine Schnittstelle, und je mehr Sie dies tun (und Abhängigkeiten über DI verbunden sind), desto weniger müssen Sie sich mit Konstruktoren beschäftigen. In der idealen Situation befasst sich Ihr Code nicht mit konkreten Klasseninstanzen oder erstellt diese sogar überhaupt. es wird also über DI ein IFoogegeben, ohne sich Gedanken darüber zu machen, was der Konstruktor von FooImplangibt, dass er seinen Job erledigen muss, und in der Tat, ohne sich der Existenz von FooImplname __ bewusst zu sein. Aus dieser Sicht ist die Verkapselung perfekt.

Dies ist natürlich eine Meinung, aber meiner Meinung nach verstößt DI nicht unbedingt gegen die Einkapselung und kann in der Tat helfen, indem es das gesamte notwendige Wissen über Interna an einem Ort zentralisiert. Dies ist nicht nur eine gute Sache an sich, sondern noch besser ist diese Stelle außerhalb Ihrer eigenen Codebase, so dass kein Code, den Sie schreiben, über die Abhängigkeiten der Klassen wissen muss.

29
Andrzej Doyle

Dies macht die Abhängigkeit sichtbar, die injiziert wird, und verstößt gegen das OOP Prinzip der Einkapselung.

Ehrlich gesagt, alles verstößt gegen die Einkapselung. :) Es ist eine Art Ausschreibungsprinzip, das gut behandelt werden muss.

Also, was verstößt gegen die Einkapselung?

Vererbung do .

Msgstr "Da die Vererbung eine Unterklasse mit Details der Implementierung ihres Elternteils konfrontiert, wird oft gesagt, dass die Vererbung die Kapselung bricht". (Viererband 1995: 19)

Aspektorientierte Programmierung tut . Beispielsweise registrieren Sie onMethodCall () - Rückruf. Dies gibt Ihnen eine großartige Möglichkeit, Code in die normale Methodenbewertung einzufügen, ungewöhnliche Nebeneffekte hinzuzufügen usw.

Friend-Deklaration in C++ do .

Klassenerweiterung in Ruby do . Definieren Sie einfach eine String-Methode irgendwo neu, nachdem eine String-Klasse vollständig definiert wurde.

Nun, eine Menge Zeug tut .

Die Einkapselung ist ein guter und wichtiger Grundsatz. Aber nicht der einzige.

switch (principle)
{
      case encapsulation:
           if (there_is_a_reason)
      break!
}
17

Ja, DI verstößt gegen die Kapselung (auch als "Informationsverstecken" bezeichnet).

Das eigentliche Problem besteht jedoch, wenn Entwickler es als Entschuldigung verwenden, um die Prinzipien von KISS (Keep It Short and Simple) und YAGNI (You Ain't Not It) zu verletzen.

Persönlich bevorzuge ich einfache und effektive Lösungen. Meist verwende ich den "neuen" Operator, um zustandsbehaftete Abhängigkeiten zu instanziieren, wann und wo immer sie benötigt werden. Es ist einfach, gut gekapselt, leicht verständlich und einfach zu testen. Also warum nicht?

13
Rogério

Ein gutes Depenance-Injektionscontainer/-system ermöglicht die Konstruktorinjektion. Die abhängigen Objekte werden gekapselt und müssen überhaupt nicht öffentlich sichtbar gemacht werden. Durch die Verwendung eines DP-Systems "kennt" Ihr Code nicht einmal die Details, wie das Objekt erstellt wird, möglicherweise sogar das Objekt, das erstellt wird. In diesem Fall gibt es mehr Verkapselung, da nahezu der gesamte Code nicht nur vor dem Wissen über die gekapselten Objekte geschützt ist, sondern auch nicht an der Objektkonstruktion beteiligt ist.

Nun gehe ich davon aus, dass Sie den Fall vergleichen, in dem das erstellte Objekt seine eigenen gekapselten Objekte erstellt, höchstwahrscheinlich im Konstruktor. Mein Verständnis von DP ist, dass wir diese Verantwortung von dem Objekt nehmen und es an andere weitergeben möchten. Zu diesem Zweck verfügt der "jemand andere", in diesem Fall der DP-Container, über ein intimes Wissen, das die Kapselung "verletzt"; Der Vorteil ist, dass es dieses Wissen selbst aus dem Objekt zieht. Jemand muss es haben. Der Rest Ihrer Bewerbung nicht.

Ich würde es so denken: Das Dependance-Container/-System für die Abhängigkeit verletzt die Kapselung, Ihr Code jedoch nicht. Tatsächlich ist Ihr Code mehr denn je "gekapselt".

5
Bill

Wie Jeff Sternal in einem Kommentar zu der Frage hervorgehoben hat, hängt die Antwort vollständig davon ab, wie Sie encapsulation definieren.

Es scheint zwei Hauptlager zu geben, was Einkapselung bedeutet:

  1. Alles, was mit dem Objekt zusammenhängt, ist eine Methode für ein Objekt. Ein File-Objekt kann also über Methoden für Save, Print, Display, ModifyText usw. verfügen.
  2. Ein Objekt ist seine eigene kleine Welt und hängt nicht vom äußeren Verhalten ab.

Diese beiden Definitionen stehen in direktem Widerspruch zueinander. Wenn ein File-Objekt selbst drucken kann, hängt es stark vom Verhalten des Druckers ab. Wenn dagegen nur weiß über etwas, das für sie drucken kann (eine IFilePrinter oder eine solche Schnittstelle), muss das File-Objekt nichts über das Drucken wissen und damit arbeiten bringt weniger Abhängigkeiten in das Objekt.

Wenn Sie die erste Definition verwenden, unterbricht die Abhängigkeitsinjektion die Kapselung. Aber ehrlich gesagt weiß ich nicht, ob mir die erste Definition gefällt - sie skaliert eindeutig nicht (wenn dies der Fall wäre, wäre MS Word eine große Klasse).

Andererseits ist die Abhängigkeitsinjektion nahezu obligatorisch , wenn Sie die zweite Definition der Kapselung verwenden.

4
kyoryu

Es verstößt nicht gegen die Kapselung. Sie stellen einen Mitarbeiter bereit, aber die Klasse kann entscheiden, wie sie verwendet wird. Solange du folgst sag nicht fragen die Dinge sind in Ordnung. Ich finde die Injektion von Konstrukteuren vorzuziehen, aber Setter können gut sein, solange sie schlau sind. Das heißt, sie enthalten Logik, um die von der Klasse repräsentierten Invarianten zu erhalten.

4
Jason Watkins

Das ist ähnlich wie die Antwort, aber ich möchte laut darüber nachdenken - vielleicht sehen es andere auch so.

  • Classical OO verwendet Konstruktoren, um den öffentlichen Initialisierungsvertrag für Konsumenten der Klasse zu definieren (verbirgt ALLE Implementierungsdetails; auch Kapselung genannt). Durch diesen Vertrag kann sichergestellt werden, dass Sie nach der Instantiierung über ein einsatzbereites Objekt verfügen (d. H. Keine zusätzlichen Initialisierungsschritte, die vom Benutzer gespeichert werden müssen (äh, vergessen)).

  • (Konstruktor) DI bricht die Kapselung unwiderruflich ab, indem er Implementierungsdetail über diese öffentliche Konstruktorschnittstelle blutet. Solange wir den öffentlichen Konstruktor noch als für die Definition des Initialisierungsvertrags für Benutzer verantwortlich betrachten, haben wir einen schrecklichen Verstoß gegen die Einkapselung geschaffen.

Theoretisches Beispiel:

Klasse Foo hat 4 Methoden und benötigt eine Ganzzahl für die Initialisierung, daher sieht der Konstruktor wie Foo (int size) aus und ist für Benutzer der Klasse Foo sofort klar, dass sie ein angeben müssen. size bei der Instantiierung, damit Foo funktioniert.

Angenommen, diese bestimmte Implementierung von Foo kann auch ein IWidget benötigen, um ihre Arbeit zu erledigen. Konstruktorinjektion dieser Abhängigkeit würde einen Konstruktor wie Foo (int size, IWidget-Widget) erstellen

Was mich darüber irritiert, ist jetzt, dass wir einen Konstruktor haben, der Blending Initialisierungsdaten mit Abhängigkeiten enthält - eine Eingabe ist für den Benutzer der Klasse ( size ) von Interesse, die andere ist eine interne Abhängigkeit davon dient nur zur Verwirrung des Benutzers und ist ein Implementierungsdetail ( widget ). 

Der Größenparameter ist NICHT eine Abhängigkeit - er ist einfach ein Initialisierungswert pro Instanz. IoC ist ideal für externe Abhängigkeiten (wie Widget), jedoch nicht für die Initialisierung des internen Zustands.

Was noch schlimmer ist, wenn das Widget nur für 2 der 4 Methoden in dieser Klasse erforderlich ist; Ich kann Instantiierung-Overhead für Widget erleiden, obwohl es möglicherweise nicht verwendet wird!

Wie kann man dies kompromittieren? 

Ein Ansatz besteht darin, ausschließlich zu Schnittstellen zu wechseln, um den Betriebsvertrag zu definieren. und die Verwendung von Konstruktoren durch Benutzer abschaffen. Um konsistent zu sein, müsste auf alle Objekte nur über Schnittstellen zugegriffen werden und nur durch eine Art Resolver (wie ein IOC/DI-Container) instanziiert werden. Nur der Container kann Dinge instanziieren. 

Das kümmert sich um die Widget-Abhängigkeit, aber wie initialisieren wir "size", ohne auf eine separate Initialisierungsmethode auf der Foo-Oberfläche zurückzugreifen? Mit dieser Lösung konnten wir nicht mehr sicherstellen, dass eine Instanz von Foo vollständig initialisiert ist, sobald Sie die Instanz abrufen. Schade, weil mir die Idee und Einfachheit der Konstruktorinjektion wirklich gefällt.

Wie erreiche ich eine garantierte Initialisierung in dieser DI-Welt, wenn die Initialisierung MEHR ist als NUR externe Abhängigkeiten?

4
shawnT

Die reine Kapselung ist ein Ideal, das niemals erreicht werden kann. Wenn alle Abhängigkeiten verborgen wären, müssten Sie überhaupt keine DI haben. Wenn Sie wirklich über private Werte verfügen, die innerhalb des Objekts verinnerlicht werden können, beispielsweise der ganzzahlige Wert der Geschwindigkeit eines Autoobjekts, dann haben Sie keine externe Abhängigkeit und müssen diese Abhängigkeit nicht invertieren oder injizieren. Diese Art von internen Zustandswerten, die rein von privaten Funktionen bearbeitet werden, ist das, was Sie immer kapseln möchten.

Wenn Sie jedoch ein Auto bauen, das eine bestimmte Art von Motorobjekt wünscht, dann haben Sie eine externe Abhängigkeit. Sie können diesen Motor - zum Beispiel den neuen GMOverHeadCamEngine () - innerhalb des Konstruktors des Car-Objekts entweder instanziieren, wobei die Kapselung erhalten bleibt, aber eine weitaus heimtückischere Verknüpfung mit einer konkreten Klasse GMOverHeadCamEngine hergestellt wird, oder Sie können ihn injizieren, sodass Ihr Car-Objekt verwendet werden kann agnostisch (und viel robuster) an beispielsweise einer Schnittstelle IEngine ohne die konkrete Abhängigkeit. Ob Sie einen IOC Container oder ein einfaches DI verwenden, um dies zu erreichen, ist nicht der Punkt - der Punkt ist, dass Sie ein Auto haben, das viele Arten von Motoren verwenden kann, ohne mit einem von ihnen gekoppelt zu sein machen Sie Ihre Codebase flexibler und weniger anfällig für Nebenwirkungen. 

DI ist kein Verstoß gegen die Einkapselung, es ist eine Möglichkeit, die Kopplung zu minimieren, wenn die Einkapselung in praktisch jedem OOP Projekt zwangsläufig unterbrochen wird. Durch das externe Injizieren einer Abhängigkeit in eine Schnittstelle werden Kopplungsnebeneffekte minimiert, und Ihre Klassen bleiben bei der Implementierung agnostisch. 

3
Dave Sims

Ich glaube an die Einfachheit. Das Anwenden von IOC/Dependecy Injection in Domänenklassen führt zu keiner Verbesserung, es sei denn, der Code wird durch die Verwendung externer Xml-Dateien, die die Beziehung beschreiben, schwerer zu behandeln. Viele Technologien wie EJB 1.0/2.0 & struts 1.1 kehren zurück, indem sie den Inhalt der XML-Dateien reduzieren und versuchen, sie in Code als Annoation usw. einzufügen. Wenn Sie also IOC für alle Klassen verwenden, die Sie entwickeln, wird der Code erstellt Unsinn.

IOC bietet Vorteile, wenn das abhängige Objekt zur Kompilierzeit nicht zur Erstellung bereit ist. Dies kann in den meisten Infrastrukturkomponenten der abstrakten Abstraktionsebene geschehen, indem versucht wird, ein gemeinsames Grundgerüst zu schaffen, das für verschiedene Szenarien funktionieren kann. An diesen Stellen ist die Verwendung IOC sinnvoller. Dennoch macht dies den Code nicht einfacher/wartbarer.

Wie alle anderen Technologien verfügt auch dies über PROs & CONs. Meine Sorge ist, wir implementieren neueste Technologien an allen Orten, unabhängig von ihrer besten Kontextnutzung.

Es hängt davon ab, ob die Abhängigkeit wirklich ein Implementierungsdetail ist oder etwas, das der Client auf die eine oder andere Weise wissen möchte/muss. Relevant ist, auf welche Abstraktionsebene die Klasse abzielt. Hier sind einige Beispiele:

Wenn Sie über eine Methode verfügen, die die Zwischenspeicherung unter der Haube verwendet, um Aufrufe zu beschleunigen, sollte das Cache-Objekt ein Singleton oder etwas sein und sollte nicht eingefügt werden. Die Tatsache, dass der Cache überhaupt verwendet wird, ist ein Implementierungsdetail, um das sich die Clients Ihrer Klasse keine Gedanken machen müssen.

Wenn Ihre Klasse Datenströme ausgeben muss, ist es wahrscheinlich sinnvoll, den Ausgabestrom einzuspeisen, damit die Klasse die Ergebnisse problemlos an ein Array, eine Datei oder wo auch immer jemand anderes die Daten senden möchte, ausgeben kann.

Angenommen, Sie haben eine Klasse, die eine Monte-Carlo-Simulation durchführt. Es braucht eine Zufallsquelle. Zum einen ist die Tatsache, dass dies erforderlich ist, ein Implementierungsdetail, bei dem es dem Client wirklich egal ist, wo die Zufälligkeit herkommt. Auf der anderen Seite, da Zufallszahlengeneratoren in der realen Welt Kompromisse zwischen dem Grad der Zufälligkeit, der Geschwindigkeit usw. machen, die der Client möglicherweise steuern möchte, und der Client das Seeding kontrollieren möchte, um ein wiederholbares Verhalten zu erzielen, kann die Injektion sinnvoll sein. In diesem Fall würde ich vorschlagen, eine Möglichkeit zum Erstellen der Klasse ohne Angabe eines Zufallszahlengenerators anzubieten, und einen Singlet-lokalen Singleton als Standard verwenden. Wenn// wann eine feinere Steuerung erforderlich ist, stellen Sie einen anderen Konstruktor bereit, der das Einfügen einer Zufallsquelle ermöglicht.

2
dsimcha

Ich habe mit dem Thema ein wenig weiter gekämpft und bin jetzt der Meinung, dass Dependency Injection (zu diesem Zeitpunkt) die Kapselung in gewissem Maße verletzt. Verstehen Sie mich nicht falsch - ich denke, dass die Verwendung von Abhängigkeitsinjektion in den meisten Fällen einen Kompromiss wert ist.

Der Grund, warum DI gegen die Kapselung verstößt, wird klar, wenn die Komponente, an der Sie arbeiten, an eine "externe" Partei geliefert werden soll (denken Sie daran, eine Bibliothek für einen Kunden zu schreiben). 

Wenn meine Komponente das Einfügen von Unterkomponenten über den Konstruktor (oder öffentliche Eigenschaften) erfordert, gibt es keine Garantie dafür 

"Verhindert, dass Benutzer die internen Daten der Komponente in einen ungültigen oder inkonsistenten Zustand setzen".

Gleichzeitig kann das nicht gesagt werden 

"Benutzer der Komponente (andere Softwarekomponenten) müssen nur wissen, was die Komponente tut, und können sich nicht von den Details ihrer Funktion abhängig machen" .

Beide Zitate stammen aus wikipedia .

Um ein konkretes Beispiel zu geben: Ich muss ein clientseitiges DLL bereitstellen, das die Kommunikation mit einem WCF-Dienst (im Wesentlichen eine entfernte Fassade) vereinfacht und verbirgt. Da es von 3 verschiedenen WCF-Proxy-Klassen abhängt, muss ich beim DI-Ansatz diese über den Konstruktor freigeben. Damit lege ich die Innereien meiner Kommunikationsschicht offen, die ich zu verstecken versuche. 

Im Allgemeinen bin ich alles für DI. In diesem speziellen (extremen) Beispiel erscheint es mir gefährlich. 

2
urig

Die Kapselung ist nur dann kaputt, wenn eine Klasse sowohl die Aufgabe hat, das Objekt zu erstellen (was Kenntnisse der Implementierungsdetails erfordert), als auch die Klasse verwendet (für die diese Details nicht bekannt sind). Ich erkläre warum, aber zuerst eine kurze Autoanalyse:

Als ich mit meinem alten Kombi von 1971 Fuhr, konnte ich das Gaspedal betätigen, und Ging (etwas) schneller. Ich brauchte nicht zu wissen, warum, aber die Jungs, die das Kombi in der Fabrik bauten, wussten genau, warum.

Aber zurück zur Kodierung. Encapsulation "verbirgt ein Implementierungsdetail vor etwas, das diese Implementierung verwendet." Die Kapselung ist eine gute Sache, da sich die Implementierungsdetails ändern können, ohne dass der Benutzer der Klasse dies weiß.

Bei Verwendung der Abhängigkeitseinspritzung wird die Konstruktorinjektion zum Konstruieren vonservicetype-Objekten verwendet (im Gegensatz zu Entity-/Value-Objekten, deren Modellzustand). Alle Member-Variablen im Service-Typ-Objekt stellen Implementierungsdetails dar, die nicht auslaufen dürfen. z.B. Socket-Portnummer, Datenbankanmeldeinformationen, eine andere Klasse zum Aufrufen der Verschlüsselung, ein Cache usw.

Der Konstruktor ist relevant, wenn die Klasse zum ersten Mal erstellt wird. Dies geschieht während der Bauphase, während Ihr DI-Container (oder das Werk) alle Serviceobjekte miteinander verbindet. Der DI-Container kennt nur Implementierungsdetails. Es weiß alles über die Implementierungsdetails, wie die Jungs in der Kombi-Fabrik über Zündkerzen Bescheid wissen.

Zur Laufzeit heißt das erstellte Serviceobjekt "apon", um echte Arbeit zu erledigen. Zu diesem Zeitpunkt kennt der Aufrufer des Objekts nichts von den Implementierungsdetails. 

Das bin ich mit meinem Kombi an den Strand.

Nun zurück zur Kapselung. Wenn sich die Implementierungsdetails ändern, muss die Klasse, die diese Implementierung zur Laufzeit verwendet, nicht geändert werden. Die Kapselung ist nicht kaputt. 

Ich kann mein neues Auto auch zum Strand fahren. Die Kapselung ist nicht kaputt.

Wenn sich die Implementierungsdetails ändern, muss der DI-Container (oder die Factory) geändert werden. Sie haben nie zuvor versucht, die Implementierungsdetails vor der Fabrik zu verbergen.

2
WW.

Vielleicht ist dies eine naive Denkweise, aber was ist der Unterschied zwischen einem Konstruktor, der einen Integer-Parameter verwendet, und einem Konstruktor, der einen Service als Parameter aufnimmt? Bedeutet dies, dass durch das Definieren einer Ganzzahl außerhalb des neuen Objekts und deren Einspeisung in das Objekt die Kapselung unterbrochen wird? Wenn der Dienst nur innerhalb des neuen Objekts verwendet wird, sehe ich nicht, wie dies die Kapselung zerstören würde.

Durch die Verwendung einer Art Autowiring-Funktion (zB Autofac für C #) wird der Code extrem sauber. Durch den Aufbau von Erweiterungsmethoden für den Builder "Autofac" konnte ich eine Menge DI-Konfigurationscode ausschneiden, den ich im Laufe der Zeit beibehalten musste, da die Liste der Abhängigkeiten wuchs.

1
blooware

DI verstößt gegen Encapsulation für NON-Shared Objects - Punkt. Gemeinsame Objekte haben eine Lebensdauer außerhalb des Objekts, das erstellt wird, und müssen daher in das Objekt, das erstellt wird, AGGREGIERT werden. Objekte, die für das zu erstellende Objekt privat sind, sollten mit dem erstellten Objekt COMPOSED werden - wenn das erstellte Objekt zerstört wird, nimmt es das zusammengesetzte Objekt mit. Nehmen wir den menschlichen Körper als Beispiel. Was ist zusammengesetzt und was ist aggregiert? Wenn wir DI verwenden würden, hätte der menschliche Körperkonstruktor Hunderte von Objekten. Viele Organe sind beispielsweise (potentiell) ersetzbar. Aber sie sind immer noch in den Körper integriert. Jeden Tag werden im Körper Blutzellen gebildet (und zerstört), ohne dass äußere Einflüsse (außer Eiweiß) erforderlich sind. So werden Blutzellen intern vom Körper gebildet - neue BloodCell ().

Befürworter von DI argumentieren, dass ein Objekt NIEMALS den neuen Operator verwenden sollte. Dieser "puristische" Ansatz verstößt nicht nur gegen die Einkapselung, sondern auch gegen das Liskov-Substitutionsprinzip für jeden, der das Objekt erstellt.

1
Greg Brand

Ich habe auch mit dieser Vorstellung zu kämpfen. Zunächst war die "Anforderung", den DI-Container (wie Spring) zu verwenden, um ein Objekt zu instantiieren, das sich wie ein Sprung durch den Reifen anfühlt. Aber in Wirklichkeit ist es wirklich kein Reifen - es ist nur eine weitere "veröffentlichte" Methode, Objekte zu erstellen, die ich brauche. Sicher, die Kapselung ist "kaputt", weil jemand außerhalb der Klasse weiß, was er braucht, aber es ist wirklich nicht der Rest des Systems, der das weiß - es ist der DI-Container. Nichts Magisches passiert anders, weil DI 'weiß', dass ein Objekt ein anderes braucht.

Tatsächlich wird es sogar noch besser - durch die Konzentration auf Fabriken und Repositories muss ich gar nicht wissen, dass DI überhaupt involviert ist! Das bringt mir die Kapselung zurück. Wütend!

1
n8wrl

PS. Durch die Bereitstellung von Abhängigkeitsinjektion you muss nicht unbedingt break Encapsulation sein. Beispiel:

obj.inject_dependency(  factory.get_instance_of_unknown_class(x)  );

Der Client-Code kennt die Implementierungsdetails noch nicht.

1

Ich denke, es ist selbstverständlich, dass DI zumindest die Verkapselung erheblich schwächt. Darüber hinaus sind hier noch einige andere Nachteile von DI zu beachten.

  1. Dadurch wird die Wiederverwendung von Code schwieriger. Ein Modul, das ein Client verwenden kann, ohne explizit Abhängigkeiten bereitstellen zu müssen, ist offensichtlich einfacher zu verwenden als ein Modul, in dem der Client irgendwie herausfinden muss, welche Abhängigkeiten diese Komponente hat, und sie dann irgendwie verfügbar machen. Beispielsweise kann eine Komponente, die ursprünglich für die Verwendung in einer Anwendung ASP erstellt wurde, erwarten, dass ihre Abhängigkeiten von einem DI-Container bereitgestellt werden, der Objektinstanzen mit Lebensdauern bereitstellt, die sich auf Client-HTTP-Anforderungen beziehen. Dies ist möglicherweise nicht einfach in einem anderen Client zu reproduzieren, der nicht mit dem gleichen integrierten DI-Container ausgestattet ist wie die ursprüngliche Anwendung ASP.

  2. Es kann Code brüchiger machen. Abhängigkeiten, die von der Schnittstellenspezifikation bereitgestellt werden, können auf unerwartete Weise implementiert werden, was zu einer ganzen Klasse von Laufzeitfehlern führt, die mit einer statisch aufgelösten konkreten Abhängigkeit nicht möglich sind.

  3. Dies kann dazu führen, dass Code weniger flexibel wird, da Sie möglicherweise weniger Auswahlmöglichkeiten haben, wie er funktionieren soll. Nicht jede Klasse muss alle ihre Abhängigkeiten für die gesamte Lebensdauer der besitzenden Instanz besitzen, aber bei vielen DI-Implementierungen haben Sie keine andere Option.

In diesem Sinne denke ich, dass die wichtigste Frage dann lautet: " Muss eine bestimmte Abhängigkeit überhaupt extern spezifiziert werden? ". In der Praxis habe ich es selten für nötig gehalten, eine extern bereitgestellte Abhängigkeit zu machen, um Tests zu unterstützen.

Wenn eine Abhängigkeit wirklich extern bereitgestellt werden muss, deutet dies normalerweise darauf hin, dass die Beziehung zwischen den Objekten eher eine Zusammenarbeit als eine interne Abhängigkeit ist. In diesem Fall ist das richtige Ziel die Einkapselung von each class und nicht die Einkapselung von einer Klasse in der anderen.

Meiner Erfahrung nach besteht das Hauptproblem bei der Verwendung von DI darin, dass Sie entweder mit einem Anwendungs-Framework mit eingebautem DI beginnen oder die Codebase mit DI-Unterstützung erweitern. Aus irgendeinem Grund wird angenommen, dass die DI-Unterstützung die richtige sein muss Weg zu instanziieren alles . Sie haben sich nie die Mühe gemacht, die Frage zu stellen: "Muss diese Abhängigkeit extern spezifiziert werden?". Und schlimmer noch, sie versuchen auch alle anderen zu zwingen, die DI-Unterstützung für alles zu verwenden.

Das Ergebnis davon ist, dass Ihre Codebase unaufhaltsam in einen Zustand übergeht, in dem das Erstellen einer beliebigen Instanz in der Codebase Unmengen von DI-Containerkonfigurationen erfordert, und das Debuggen von Dingen ist doppelt so schwer, da Sie den zusätzlichen Aufwand zu ermitteln versuchen und wo etwas instanziiert wurde.

Meine Antwort auf diese Frage lautet also. Verwenden Sie DI, wo Sie ein tatsächliches Problem identifizieren können, das es für Sie löst, das Sie auf keine andere Weise einfacher lösen können.

1
Neutrino

Es ist wahrscheinlich erwähnenswert, dass Encapsulation etwas von der Perspektive abhängig ist.

public class A { 
    private B b;

    public A() {
        this.b = new B();
    }
}


public class A { 
    private B b;

    public A(B b) {
        this.b = b;
    }
}

Aus der Perspektive, dass jemand an der A-Klasse arbeitet, weiß A im zweiten Beispiel viel weniger über die Natur von this.b

Während ohne DI

new A()

vs

new A(new B())

Die Person, die diesen Code betrachtet, weiß mehr über die Natur von A im zweiten Beispiel.

Mit DI ist zumindest alles durchgesickerte Wissen an einem Ort.

0
Tom B

Ich bin damit einverstanden, dass DI extrem gegen die Kapselung verstoßen kann. Normalerweise deckt DI Abhängigkeiten auf, die nie wirklich gekapselt wurden. Hier ist ein vereinfachtes Beispiel von Miško Heverys Singletons sind pathologische Lügner :

Sie beginnen mit einem CreditCard-Test und schreiben einen einfachen Komponententest.

@Test
public void creditCard_Charge()
{
    CreditCard c = new CreditCard("1234 5678 9012 3456", 5, 2008);
    c.charge(100);
}

Im nächsten Monat erhalten Sie eine Rechnung über 100 US-Dollar. Warum wurden Sie in Rechnung gestellt? Der Unit-Test betraf eine Produktionsdatenbank. Intern ruft CreditCard Database.getInstance() auf. Wenn Sie die CreditCard so umgestalten, dass sie eine DatabaseInterface in ihrem Konstruktor benötigt, wird die Tatsache offengelegt, dass es Abhängigkeiten gibt. Ich würde jedoch behaupten, dass die Abhängigkeit zu Beginn nie gekapselt wurde, da die CreditCard-Klasse nach außen hin sichtbare Nebenwirkungen verursacht. Wenn Sie die CreditCard ohne Refactoring testen möchten, können Sie die Abhängigkeit durchaus beobachten.

@Before
public void setUp()
{
    Database.setInstance(new MockDatabase());
}

@After
public void tearDown()
{
    Database.resetInstance();
}

Ich denke nicht, dass es sich lohnt, sich Sorgen zu machen, ob das Offenlegen der Datenbank als Abhängigkeit die Verkapselung reduziert, weil es ein gutes Design ist. Nicht alle DI-Entscheidungen werden so einfach sein. Keine der anderen Antworten zeigt jedoch ein Gegenbeispiel.

0
Craig P. Motlin

Ich denke, es ist eine Frage des Spielraums. Wenn Sie die Kapselung definieren (nicht wissen lassen), müssen Sie die gekapselte Funktionalität definieren.

  1. Klasse wie sie ist : Was Sie kapseln, ist die einzige Verantwortung der Klasse. Was es zu tun weiß. Zum Beispiel sortieren. Wenn Sie einen Komparator für die Bestellung verwenden, sagen wir, Kunden, das gehört nicht zu den gekapselten Dingen: quicksort.

  2. Konfigurierte Funktionalität : Wenn Sie eine sofort einsatzbereite Funktionalität bereitstellen möchten, stellen Sie keine QuickSort-Klasse, sondern eine mit einem Comparator konfigurierte Instanz der QuickSort-Klasse bereit. In diesem Fall muss der für das Erstellen und Konfigurieren verantwortliche Code vor dem Benutzercode verborgen werden. Und das ist die Kapselung.

Wenn Sie Klassen programmieren, verwenden Sie die Option 1, um einzelne Verantwortlichkeiten in Klassen zu implementieren.

Wenn Sie Anwendungen programmieren, machen Sie etwas, das etwas Nützliches concrete work macht, dann verwenden Sie wiederholt Option 2.

Dies ist die Implementierung der konfigurierten Instanz:

<bean id="clientSorter" class="QuickSort">
   <property name="comparator">
      <bean class="ClientComparator"/>
   </property>
</bean>

So verwenden einige andere Client-Codes es:

<bean id="clientService" class"...">
   <property name="sorter" ref="clientSorter"/>
</bean>

Es ist gekapselt, da bei einer Änderung der Implementierung (Änderung der clientSorter-Bean-Definition) die Verwendung des Clients nicht beeinträchtigt wird. Wenn Sie XML-Dateien mit allen zusammengeschriebenen Dateien verwenden, werden möglicherweise alle Details angezeigt. Aber glauben Sie mir, der Client-Code (ClientService) weiß nicht nichts über seinen Sortierer.

0
helios