Wenn ich das richtig verstehe, besteht der typische Mechanismus für die Abhängigkeitsinjektion darin, entweder über einen Klassenkonstruktor oder über eine öffentliche Eigenschaft (Mitglied) der Klasse zu injizieren.
Dies legt die injizierte Abhängigkeit offen und verstößt gegen das OOP-Prinzip der Kapselung.
Bin ich richtig darin, diesen Kompromiss zu identifizieren? Wie gehen Sie mit diesem Problem um?
Bitte beachten Sie auch meine Antwort auf meine eigene Frage unten.
Antworten:
Es gibt eine andere Sichtweise auf dieses Problem, die Sie vielleicht interessant finden.
Wenn wir IoC / Abhängigkeitsinjektion verwenden, verwenden wir keine OOP-Konzepte. Zugegeben, wir verwenden eine OO-Sprache als 'Host', aber die Ideen hinter IoC stammen aus dem komponentenorientierten Software-Engineering, nicht aus OO.
Bei der Komponentensoftware dreht sich alles um das Verwalten von Abhängigkeiten. Ein häufig verwendetes Beispiel ist der Assembly-Mechanismus von .NET. Jede Assembly veröffentlicht die Liste der Assemblys, auf die sie verweist. Dies erleichtert das Zusammenführen (und Validieren) der für eine laufende Anwendung erforderlichen Teile erheblich.
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. Die Kapselung trifft nicht wirklich zu, da es in der komponenten- / serviceorientierten Welt keinen 'Implementierungstyp' gibt, aus dem Details austreten können.
Leider trennen unsere Sprachen die feinkörnigen, objektorientierten Konzepte derzeit nicht von den gröberkörnigen komponentenorientierten Konzepten. Dies ist also eine Unterscheidung, die Sie nur im Kopf behalten müssen :)
quelle
Es ist eine gute Frage - aber an einem gewissen Punkt, Verkapselung in seinen reinsten Form Bedürfnissen verletzt werden , wenn das Objekt jemals seine Abhängigkeit erfüllt haben. Einige Anbieter der Abhängigkeit müssen wissen, dass für das betreffende Objekt ein erforderlich ist
Foo
, und der Anbieter muss eine Möglichkeit habenFoo
, das Objekt bereitzustellen.Klassischerweise wird dieser letztere Fall wie gesagt durch Konstruktorargumente oder Setter-Methoden behandelt. 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
) mit Anmerkungen versehen können und die Abhängigkeit durch Reflektion festgelegt wird, ohne dass Sie die Abhängigkeit offenlegen müssen eine der Klassen öffentliche Methoden / Konstruktoren. Dies könnte die Art von Lösung sein, nach der Sie gesucht haben.Trotzdem denke ich auch nicht, dass die Konstruktorinjektion ein großes Problem darstellt. Ich hatte immer das Gefühl, dass Objekte nach der Erstellung vollständig gültig sein sollten, sodass alles, was sie zur Erfüllung ihrer Rolle benötigen (dh in einem gültigen Zustand sind), ohnehin über den Konstruktor bereitgestellt werden sollte. Wenn Sie ein Objekt haben, für dessen Arbeit ein Mitarbeiter erforderlich ist, scheint es mir in Ordnung zu sein, dass der Konstruktor diese Anforderung öffentlich ankündigt und sicherstellt, dass sie erfüllt ist, wenn eine neue Instanz der Klasse erstellt wird.
Idealerweise interagieren Sie beim Umgang mit Objekten ohnehin über eine Schnittstelle mit ihnen. Je mehr Sie dies tun (und Abhängigkeiten über DI verkabelt haben), desto weniger müssen Sie sich selbst mit Konstruktoren befassen. Im Idealfall behandelt Ihr Code keine konkreten Klasseninstanzen oder erstellt sie überhaupt nicht. Es wird also nur
IFoo
durch DI gegeben, ohne sich Gedanken darüber zu machen, was der Konstruktor vonFooImpl
angibt, dass er seine Arbeit erledigen muss, und ohne sich dessen überhaupt bewusst zuFooImpl
sein. Unter diesem Gesichtspunkt ist die Einkapselung perfekt.Dies ist natürlich eine Meinung, aber meiner Meinung nach verstößt DI nicht unbedingt gegen die Kapselung und kann tatsächlich helfen, indem es das gesamte notwendige Wissen über Interna an einem Ort zentralisiert. Dies ist nicht nur eine gute Sache für sich, sondern noch besser, dieser Ort befindet sich außerhalb Ihrer eigenen Codebasis, sodass keiner der von Ihnen geschriebenen Codes über die Abhängigkeiten der Klassen Bescheid wissen muss.
quelle
Ehrlich gesagt verstößt alles gegen die Kapselung. :) Es ist eine Art Ausschreibungsprinzip, das gut behandelt werden muss.
Was verletzt also die Kapselung?
Vererbung tut .
Aspektorientierte Programmierung tut . Wenn Sie beispielsweise den Rückruf onMethodCall () registrieren, haben Sie die Möglichkeit, Code in die normale Methodenbewertung einzufügen und seltsame Nebenwirkungen usw. hinzuzufügen.
Freunddeklaration in C ++ tut .
Klassenerweiterung in Ruby tut . Definieren Sie eine Zeichenfolgenmethode einfach irgendwo neu, nachdem eine Zeichenfolgenklasse vollständig definiert wurde.
Nun, viele Dinge tun es .
Verkapselung ist ein gutes und wichtiges Prinzip. Aber nicht der einzige.
quelle
Ja, DI verletzt die Kapselung (auch als "Verstecken von Informationen" bezeichnet).
Das eigentliche Problem tritt jedoch auf, wenn Entwickler es als Ausrede verwenden, um gegen die Prinzipien KISS (kurz und einfach halten) und YAGNI (Sie werden es nicht brauchen) zu verstoßen.
Persönlich bevorzuge ich einfache und effektive Lösungen. Ich verwende meistens den "neuen" Operator, um zustandsbehaftete Abhängigkeiten zu instanziieren, wann und wo immer sie benötigt werden. Es ist einfach, gut gekapselt, leicht zu verstehen und leicht zu testen. Also warum nicht?
quelle
Ein guter Injektionsinjektionsbehälter / -system ermöglicht eine Konstruktorinjektion. Die abhängigen Objekte werden gekapselt und müssen überhaupt nicht öffentlich zugänglich gemacht werden. Wenn Sie ein DP-System verwenden, "kennt" keiner Ihrer Codes die Details der Konstruktion des Objekts, möglicherweise sogar einschließlich des zu konstruierenden Objekts. In diesem Fall gibt es mehr Kapselung, da fast Ihr gesamter 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 mit dem Fall vergleichen, in dem das erstellte Objekt seine eigenen gekapselten Objekte erstellt, höchstwahrscheinlich in seinem Konstruktor. Mein Verständnis von DP ist, dass wir diese Verantwortung vom Objekt nehmen und es jemand anderem geben wollen. Zu diesem Zweck verfügt die "andere Person", in diesem Fall der DP-Container, über 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 Anwendung tut dies nicht.
Ich würde es so sehen: Der Abhängigkeitsinjektionsbehälter / das System verletzt die Kapselung, Ihr Code jedoch nicht. Tatsächlich ist Ihr Code "gekapselt" wie nie zuvor.
quelle
Wie Jeff Sternal in einem Kommentar zu der Frage betonte, hängt die Antwort vollständig davon ab, wie Sie die Kapselung definieren .
Es scheint zwei Hauptlager zu geben, was Kapselung bedeutet:
File
kann bezwecken MethodenSave
,Print
,Display
,ModifyText
etc.Diese beiden Definitionen stehen in direktem Widerspruch zueinander. Wenn sich ein
File
Objekt selbst drucken kann, hängt dies stark vom Verhalten des Druckers ab. Wenn es jedoch nur etwas weiß , das für es gedruckt werden kann (eineIFilePrinter
oder eine solche Schnittstelle), muss dasFile
Objekt nichts über das Drucken wissen. Wenn Sie also damit arbeiten, werden weniger Abhängigkeiten in das Objekt eingebracht.Wenn Sie die erste Definition verwenden, wird die Kapselung durch die Abhängigkeitsinjektion unterbrochen. Aber ehrlich gesagt weiß ich nicht, ob mir die erste Definition gefällt - sie skaliert eindeutig nicht (wenn ja, wäre MS Word eine große Klasse).
Auf der anderen Seite ist die Abhängigkeitsinjektion fast obligatorisch, wenn Sie die zweite Definition der Kapselung verwenden.
quelle
Es verletzt nicht die Kapselung. Sie stellen einen Mitarbeiter zur Verfügung, aber die Klasse kann entscheiden, wie er verwendet wird. Solange du Tell folgst, frag nicht, ob die Dinge in Ordnung sind. Ich finde die Konstruktorinjektion vorzuziehen, aber Setter können in Ordnung sein, solange sie klug sind. Das heißt, sie enthalten Logik, um die Invarianten beizubehalten, die die Klasse darstellt.
quelle
Dies ähnelt der optimierten Antwort, aber ich möchte laut denken - vielleicht sehen andere die Dinge auch so.
Classical OO verwendet Konstruktoren, um den öffentlichen "Initialisierungs" -Vertrag für Verbraucher der Klasse zu definieren (Ausblenden ALLER Implementierungsdetails; auch bekannt als Kapselung). Dieser Vertrag kann sicherstellen, dass Sie nach der Instanziierung ein gebrauchsfertiges Objekt haben (dh keine zusätzlichen Initialisierungsschritte, an die sich der Benutzer erinnern muss (äh, vergessen)).
(Konstruktor) DI unterbricht unbestreitbar die Kapselung, indem es Implementierungsdetails über diese öffentliche Konstruktorschnittstelle blutet . Solange wir den öffentlichen Konstruktor weiterhin für die Definition des Initialisierungsvertrags für Benutzer verantwortlich machen, haben wir einen schrecklichen Verstoß gegen die Kapselung verursacht.
Theoretisches Beispiel:
Class Foo verfügt über 4 Methoden und benötigt eine Ganzzahl für die Initialisierung. Der Konstruktor sieht also wie Foo (int size) aus, und Benutzern der Klasse Foo ist sofort klar, dass sie bei der Instanziierung eine Größe angeben müssen, damit Foo funktioniert.
Angenommen , diese spezielle Implementierung von Foo benötigt möglicherweise auch ein IWidget , um ihre Aufgabe zu erfüllen . Durch die Konstruktorinjektion dieser Abhängigkeit würden wir einen Konstruktor wie Foo erstellen (int size, IWidget-Widget).
Was darüber ärgert mich jetzt haben wir einen Konstruktor, ist Blending Initialisierungsdaten mit Abhängigkeiten - ein Eingang von Interesse für den Benutzer der Klasse ( Größe ), die andere eine interne Abhängigkeit besteht darin , dass nur die Benutzer zu verwirren dient und ist eine Implementierung Detail ( Widget ).
Der Größenparameter ist KEINE Abhängigkeit - es ist einfach ein Initialisierungswert pro Instanz. IoC eignet sich hervorragend für externe Abhängigkeiten (wie Widgets), jedoch nicht für die Initialisierung des internen Status.
Schlimmer noch, was ist, wenn das Widget nur für 2 der 4 Methoden dieser Klasse erforderlich ist? Ich kann Instanziierungs-Overhead für Widget verursachen, obwohl es möglicherweise nicht verwendet wird!
Wie kann man dies kompromittieren / in Einklang bringen?
Ein Ansatz besteht darin, ausschließlich auf Schnittstellen umzuschalten, 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 und nur über eine Art Resolver (wie einen IOC / DI-Container) instanziiert werden. Nur der Container kann Dinge instanziieren.
Das kümmert sich um die Widget-Abhängigkeit, aber wie initialisieren wir "Größe", ohne auf eine separate Initialisierungsmethode auf der Foo-Oberfläche zurückzugreifen? Mit dieser Lösung haben wir die Möglichkeit verloren, sicherzustellen, dass eine Instanz von Foo zum Zeitpunkt des Abrufs der Instanz vollständig initialisiert ist. Schade, weil mir die Idee und Einfachheit der Konstruktorinjektion sehr gut gefällt .
Wie erreiche ich eine garantierte Initialisierung in dieser DI-Welt, wenn die Initialisierung MEHR als NUR externe Abhängigkeiten ist?
quelle
Reine Einkapselung ist ein Ideal, das niemals erreicht werden kann. Wenn alle Abhängigkeiten ausgeblendet wären, wäre DI überhaupt nicht erforderlich. Stellen Sie sich das so vor: Wenn Sie wirklich private Werte haben, die innerhalb des Objekts verinnerlicht werden können, beispielsweise den ganzzahligen 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 Statuswerten, die nur von privaten Funktionen verarbeitet werden, möchten Sie immer kapseln.
Wenn Sie jedoch ein Auto bauen, das eine bestimmte Art von Motorobjekt möchte, besteht eine externe Abhängigkeit. Sie können diese Engine - zum Beispiel die neue GMOverHeadCamEngine () - entweder intern im Konstruktor des Autoobjekts instanziieren, die Kapselung beibehalten, aber eine viel heimtückischere Kopplung an eine konkrete Klasse GMOverHeadCamEngine herstellen, oder Sie können sie injizieren, damit Ihr Autoobjekt arbeiten kann Agnostisch (und viel robuster) zum Beispiel auf einer Schnittstellen-IEngine ohne die konkrete Abhängigkeit. Ob Sie einen IOC-Container oder einen einfachen 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 an einen von ihnen gekoppelt zu sein, wodurch Ihre Codebasis flexibler und flexibler wird weniger anfällig für Nebenwirkungen.
DI ist keine Verletzung der Kapselung, sondern eine Möglichkeit, die Kopplung zu minimieren, wenn die Kapselung in praktisch jedem OOP-Projekt selbstverständlich unterbrochen wird. Durch das externe Injizieren einer Abhängigkeit in eine Schnittstelle werden die Nebenwirkungen der Kopplung minimiert, und Ihre Klassen können hinsichtlich der Implementierung agnostisch bleiben.
quelle
Es hängt davon ab, ob die Abhängigkeit wirklich ein Implementierungsdetail ist oder etwas, über das der Client auf die eine oder andere Weise Bescheid wissen möchte / muss. Relevant ist, auf welche Abstraktionsebene die Klasse abzielt. Hier sind einige Beispiele:
Wenn Sie eine Methode haben, die das Caching unter der Haube verwendet, um Anrufe zu beschleunigen, sollte das Cache-Objekt ein Singleton oder etwas anderes sein und nicht injiziert werden. Die Tatsache, dass der Cache überhaupt verwendet wird, ist ein Implementierungsdetail, um das sich die Clients Ihrer Klasse nicht kümmern sollten.
Wenn Ihre Klasse Datenströme ausgeben muss, ist es wahrscheinlich sinnvoll, den Ausgabestream einzufügen, damit die Klasse die Ergebnisse problemlos in ein Array, eine Datei oder an einen anderen Ort ausgeben kann, an dem jemand anderes die Daten senden möchte.
Nehmen wir für eine Grauzone an, Sie haben eine Klasse, die eine Monte-Carlo-Simulation durchführt. Es braucht eine Quelle der Zufälligkeit. Einerseits ist die Tatsache, dass dies erforderlich ist, ein Implementierungsdetail, da es dem Kunden wirklich egal ist, woher die Zufälligkeit kommt. Andererseits kann eine Injektion sinnvoll sein, da reale Zufallszahlengeneratoren Kompromisse zwischen dem Grad der Zufälligkeit, der Geschwindigkeit usw. eingehen, den der Client möglicherweise steuern möchte, und der Client möglicherweise das Seeding steuern möchte, um ein wiederholbares Verhalten zu erzielen. In diesem Fall würde ich vorschlagen, eine Möglichkeit zum Erstellen der Klasse ohne Angabe eines Zufallszahlengenerators anzubieten und standardmäßig einen threadlokalen Singleton zu verwenden. Wenn eine feinere Steuerung erforderlich ist, stellen Sie einen anderen Konstruktor bereit, mit dem eine Zufallsquelle injiziert werden kann.
quelle
Ich glaube an Einfachheit. Das Anwenden von IOC / Dependecy Injection in Domänenklassen führt zu keiner Verbesserung, außer dass die Verwaltung des Codes durch externe XML-Dateien, die die Beziehung beschreiben, erheblich erschwert wird. Viele Technologien wie EJB 1.0 / 2.0 und Struts 1.1 kehren sich um, indem sie das in XML eingegebene Material reduzieren und versuchen, es als Annoation usw. in Code einzufügen. Wenn Sie also IOC für alle von Ihnen entwickelten Klassen anwenden, ist der Code nicht sinnvoll.
IOC hat Vorteile, wenn das abhängige Objekt zur Kompilierungszeit nicht zur Erstellung bereit ist. Dies kann in den meisten Komponenten der abstrakten Architektur auf Infrasture-Ebene geschehen, wenn versucht wird, ein gemeinsames Basisframework zu erstellen, das möglicherweise für verschiedene Szenarien funktionieren muss. An diesen Orten ist die Verwendung von IOC sinnvoller. Dies macht den Code jedoch nicht einfacher / wartbarer.
Wie alle anderen Technologien hat auch dies Vor- und Nachteile. Ich mache mir Sorgen, dass wir die neuesten Technologien an allen Orten implementieren, unabhängig von ihrer besten Kontextnutzung.
quelle
Die Kapselung wird nur unterbrochen, wenn eine Klasse sowohl für die Erstellung des Objekts verantwortlich ist (für die Kenntnis der Implementierungsdetails erforderlich ist) als auch die Klasse verwendet (für die keine Kenntnis dieser Details erforderlich ist). Ich werde erklären warum, aber zuerst eine kurze Auto-Anaologie:
Aber zurück zur Codierung. Bei der Kapselung wird "ein Implementierungsdetail vor etwas verborgen, 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ängigkeitsinjektion wird die Konstruktorinjektion zum Erstellen von Service- Typ-Objekten verwendet (im Gegensatz zu Entitäts- / Wertobjekten, die den Status modellieren). Alle Mitgliedsvariablen im Diensttypobjekt stellen Implementierungsdetails dar, die nicht auslaufen sollten. zB Socket-Portnummer, Datenbankanmeldeinformationen, eine andere Klasse, die zum Durchführen der Verschlüsselung aufgerufen werden soll, 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 Ihre Fabrik) alle Serviceobjekte miteinander verdrahtet. Der DI-Container kennt nur Implementierungsdetails. Es weiß alles über Implementierungsdetails, wie die Leute in der Kombi-Fabrik über Zündkerzen Bescheid wissen.
Zur Laufzeit wird das erstellte Serviceobjekt als apon bezeichnet, um echte Arbeit zu leisten. Zu diesem Zeitpunkt kennt der Aufrufer des Objekts nichts von den Implementierungsdetails.
Nun zurück zur Kapselung. Wenn sich die Implementierungsdetails ändern, muss sich die Klasse, die diese Implementierung zur Laufzeit verwendet, nicht ändern. Die Kapselung ist nicht gebrochen.
Wenn sich die Implementierungsdetails ändern, muss sich der DI-Container (oder die Factory) ändern. Sie haben nie versucht, Implementierungsdetails vor der Fabrik zu verbergen.
quelle
Nachdem ich mich ein wenig weiter mit dem Problem auseinandergesetzt habe, bin ich jetzt der Meinung, dass Dependency Injection (zu diesem Zeitpunkt) die Kapselung bis zu einem gewissen Grad verletzt. Verstehen Sie mich aber nicht falsch - ich denke, dass die Verwendung der Abhängigkeitsinjektion in den meisten Fällen den Kompromiss wert ist.
Der Grund, warum DI die Kapselung verletzt, 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 für meine Komponente Unterkomponenten über den Konstruktor (oder öffentliche Eigenschaften) eingefügt werden müssen, gibt es keine Garantie dafür
Gleichzeitig kann das nicht gesagt werden
Beide Zitate stammen aus Wikipedia .
Um ein konkretes Beispiel zu nennen: Ich muss eine clientseitige DLL bereitstellen, die die Kommunikation mit einem WCF-Dienst (im Wesentlichen einer Remote-Fassade) vereinfacht und verbirgt. Da es von 3 verschiedenen WCF-Proxy-Klassen abhängt, bin ich gezwungen, diese über den Konstruktor verfügbar zu machen, wenn ich den DI-Ansatz verwende. Damit lege ich die Interna meiner Kommunikationsschicht frei, die ich zu verbergen versuche.
Generell bin ich alles für DI. In diesem speziellen (extremen) Beispiel erscheint es mir gefährlich.
quelle
DI verletzt die Kapselung für NICHT gemeinsam genutzte Objekte - Punkt. Freigegebene Objekte haben eine Lebensdauer außerhalb des zu erstellenden Objekts und müssen daher in das zu erstellende Objekt AGGREGIERT werden. Objekte, die für das zu erstellende Objekt privat sind, sollten in das erstellte Objekt zusammengesetzt werden. Wenn das erstellte Objekt zerstört wird, nimmt es das zusammengesetzte Objekt mit. Nehmen wir als Beispiel den menschlichen Körper. Was ist zusammengesetzt und was ist aggregiert. Wenn wir DI verwenden würden, hätte der Konstruktor des menschlichen Körpers Hunderte von Objekten. Viele der Organe sind beispielsweise (möglicherweise) austauschbar. Aber sie sind immer noch im Körper zusammengesetzt. Blutzellen werden jeden Tag im Körper gebildet (und zerstört), ohne dass äußere Einflüsse (außer Protein) erforderlich sind. So werden Blutzellen intern vom Körper erzeugt - neue Blutzelle ().
Befürworter von DI argumentieren, dass ein Objekt NIEMALS den neuen Operator verwenden sollte. Dieser "puristische" Ansatz verstößt nicht nur gegen die Kapselung, sondern auch gegen das Liskov-Substitutionsprinzip für jeden, der das Objekt erstellt.
quelle
Ich hatte auch mit dieser Vorstellung zu kämpfen. Zunächst fühlte sich die "Anforderung", den DI-Container (wie Spring) zum Instanziieren eines Objekts zu verwenden, an, als würde man durch Reifen springen. Aber in Wirklichkeit ist es wirklich kein Reifen - es ist nur eine andere "veröffentlichte" Art, Objekte zu erstellen, die ich brauche. Sicher, die Kapselung ist "kaputt", weil jemand "außerhalb der Klasse" weiß, was sie 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 - wenn ich mich auf Fabriken und Repositories konzentriere, muss ich nicht einmal wissen, dass DI überhaupt involviert ist! Das bringt für mich den Deckel wieder auf die Kapselung. Wütend!
quelle
PS. Durch die Bereitstellung von Dependency Injection Sie nicht unbedingt brechen Encapsulation . Beispiel:
Der Client-Code kennt die Implementierungsdetails noch nicht.
quelle
Vielleicht ist dies eine naive Art, darüber nachzudenken, aber was ist der Unterschied zwischen einem Konstruktor, der einen ganzzahligen Parameter aufnimmt, und einem Konstruktor, der einen Dienst als Parameter aufnimmt? Bedeutet dies, dass das Definieren einer Ganzzahl außerhalb des neuen Objekts und das Einspeisen in das Objekt die Kapselung unterbricht? Wenn der Dienst nur innerhalb des neuen Objekts verwendet wird, sehe ich nicht, wie dies die Kapselung unterbrechen würde.
Durch die Verwendung einer automatischen Verdrahtungsfunktion (z. B. Autofac für C #) wird der Code extrem sauber. Durch das Erstellen von Erweiterungsmethoden für den Autofac-Builder konnte ich eine Menge DI-Konfigurationscode ausschneiden, den ich im Laufe der Zeit hätte pflegen müssen, als die Liste der Abhängigkeiten wuchs.
quelle
Ich denke, es ist selbstverständlich, dass DI zumindest die Einkapselung signifikant schwächt. Darüber hinaus sind hier einige andere Nachteile von DI zu berücksichtigen.
Es macht die Wiederverwendung von Code schwieriger. Ein Modul, das ein Client verwenden kann, ohne explizit Abhängigkeiten angeben zu müssen, ist offensichtlich einfacher zu verwenden als eines, bei dem der Client die Abhängigkeiten dieser Komponente irgendwie herausfinden und sie dann irgendwie verfügbar machen muss. Beispielsweise kann für eine Komponente, die ursprünglich für die Verwendung in einer ASP-Anwendung erstellt wurde, erwartet werden, dass ihre Abhängigkeiten von einem DI-Container bereitgestellt werden, der Objektinstanzen eine Lebensdauer im Zusammenhang mit Client-http-Anforderungen bietet. Dies ist möglicherweise nicht einfach auf einem anderen Client zu reproduzieren, der nicht über denselben integrierten DI-Container wie die ursprüngliche ASP-Anwendung verfügt.
Dies kann den Code anfälliger machen. Abhängigkeiten, die durch die 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.
Dies kann den Code weniger flexibel machen, 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 haben, aber bei vielen DI-Implementierungen haben Sie keine andere Option.
Vor diesem Hintergrund 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 notwendig gehalten, eine extern bereitgestellte Abhängigkeit zu erstellen, um das Testen 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 geeignete Ziel die Kapselung jeder Klasse und nicht die Kapselung einer Klasse innerhalb der anderen .
Nach meiner Erfahrung besteht das Hauptproblem bei der Verwendung von DI darin, dass unabhängig davon, ob Sie mit einem Anwendungsframework mit integriertem DI beginnen oder Ihrer Codebasis DI-Unterstützung hinzufügen, aus irgendeinem Grund davon ausgegangen wird, dass dies korrekt sein muss, da Sie DI-Unterstützung haben Weg, um alles zu instanziieren . Sie stellen sich einfach nie die Frage "Muss diese Abhängigkeit extern spezifiziert werden?". Schlimmer noch, sie versuchen auch, alle anderen zu zwingen, die DI-Unterstützung auch für alles zu nutzen.
Das Ergebnis ist, dass sich Ihre Codebasis unaufhaltsam in einen Zustand verwandelt, in dem das Erstellen einer Instanz von irgendetwas in Ihrer Codebasis Unmengen von stumpfen DI-Containerkonfigurationen erfordert und das Debuggen von Dingen doppelt so schwierig ist, da Sie die zusätzliche Arbeitslast haben, um herauszufinden, wie und wo etwas instanziiert wurde.
Meine Antwort auf die Frage lautet also: Verwenden Sie DI, um ein tatsächliches Problem zu identifizieren, das es für Sie löst und das Sie auf keine andere Weise einfacher lösen können.
quelle
Ich bin damit einverstanden, dass DI im Extremfall die Kapselung verletzen kann. Normalerweise legt DI Abhängigkeiten offen, die nie wirklich gekapselt wurden. Hier ist ein vereinfachtes Beispiel aus Miško Heverys Singletons: Pathologische Lügner :
Sie beginnen mit einem CreditCard-Test und schreiben einen einfachen Unit-Test.
Nächsten Monat erhalten Sie eine Rechnung über 100 US-Dollar. Warum wurdest du angeklagt? Der Komponententest betraf eine Produktionsdatenbank. Intern ruft CreditCard an
Database.getInstance()
. Wenn Sie CreditCard so umgestalten, dass es einenDatabaseInterface
Konstruktor enthält, wird die Tatsache sichtbar, dass eine Abhängigkeit besteht. Ich würde jedoch argumentieren, dass die Abhängigkeit von Anfang an nie gekapselt wurde, da die CreditCard-Klasse extern sichtbare Nebenwirkungen verursacht. Wenn Sie CreditCard ohne Refactoring testen möchten, können Sie die Abhängigkeit durchaus beobachten.Ich denke nicht, dass es sich lohnt, sich Sorgen zu machen, ob das Offenlegen der Datenbank als Abhängigkeit die Kapselung verringert, da es ein gutes Design ist. Nicht alle DI-Entscheidungen werden so einfach sein. Keine der anderen Antworten zeigt jedoch ein Gegenbeispiel.
quelle
CreditCard
Objekt von einer WebAPI zu PayPal wechseln, ohne dass sich etwas außerhalb der Klasse ändern muss?Ich denke, es ist eine Frage des Umfangs. Wenn Sie die Kapselung definieren (ohne zu wissen, wie), müssen Sie die gekapselte Funktionalität definieren.
Klasse wie sie ist : Was Sie einkapseln, ist die einzige Verantwortung der Klasse. Was es zu tun weiß. Zum Beispiel Sortieren. Wenn Sie einen Komparator für die Bestellung einfügen, sagen wir, Kunden, ist das nicht Teil der gekapselten Sache: Quicksort.
Konfigurierte Funktionalität : Wenn Sie eine sofort einsatzbereite Funktionalität bereitstellen möchten, stellen Sie keine QuickSort-Klasse bereit, sondern eine mit einem Comparator konfigurierte Instanz der QuickSort-Klasse. In diesem Fall muss der Code, der für das Erstellen und Konfigurieren verantwortlich ist, vor dem Benutzercode verborgen sein. Und das ist die Kapselung.
Wenn Sie Klassen programmieren, verwenden Sie Option 1, indem Sie einzelne Verantwortlichkeiten in Klassen implementieren.
Wenn Sie Anwendungen programmieren und etwas tun, das nützliche konkrete Arbeit leistet, verwenden Sie wiederholt Option 2.
Dies ist die Implementierung der konfigurierten Instanz:
So verwendet es ein anderer Client-Code:
Es ist gekapselt, da
clientSorter
es die Client-Verwendung nicht beeinträchtigt, wenn Sie die Implementierung ändern (Sie ändern die Bean-Definition). Wenn Sie XML-Dateien verwenden, bei denen alle zusammen geschrieben sind, sehen Sie möglicherweise alle Details. Aber glauben Sie mir, der Client-Code (ClientService
) weiß nichts über seinen Sortierer.quelle
Es ist wahrscheinlich erwähnenswert, dass dies
Encapsulation
etwas perspektivabhängig ist.Aus der Perspektive von jemandem, der an der
A
Klasse arbeitet,A
weiß das zweite Beispiel viel weniger über die Natur vonthis.b
Während ohne DI
vs.
Die Person, die diesen Code betrachtet, weiß mehr über die Art des Codes
A
im zweiten Beispiel.Mit DI befindet sich zumindest das gesamte durchgesickerte Wissen an einem Ort.
quelle