Abhängigkeitsspritze ; Bewährte Methoden zur Reduzierung des Code für das Boilerplate

25

Ich habe eine einfache Frage, und ich bin mir nicht einmal sicher, ob sie eine Antwort hat, aber versuchen wir es. Ich programmiere in C ++ und verwende Dependancy Injection, um den globalen Status zu vermeiden. Das funktioniert ganz gut und ich stoße nicht sehr oft auf unerwartetes / undefiniertes Verhalten.

Mir ist jedoch klar, dass ich mit dem Wachstum meines Projekts eine Menge Code schreibe, den ich als Boilerplate betrachte. Schlimmer noch: Die Tatsache, dass es mehr Boilerplate-Code als tatsächlichen Code gibt, macht es manchmal schwer zu verstehen.

Nichts ist besser als ein gutes Beispiel.

Ich habe eine Klasse namens TimeFactory, die Time-Objekte erstellt.

Weitere Informationen (nicht sicher, ob dies relevant ist): Zeitobjekte sind recht komplex, da die Zeit unterschiedliche Formate haben kann und die Konvertierung zwischen ihnen weder linear noch unkompliziert ist. Jede "Zeit" enthält einen Synchronizer, um Konvertierungen zu verarbeiten. Um sicherzustellen, dass sie denselben, ordnungsgemäß initialisierten Synchronizer haben, verwende ich eine TimeFactory. Die TimeFactory hat nur eine Instanz und ist anwendungsweit, sodass sie sich für Singleton qualifizieren würde. Da sie jedoch veränderlich ist, möchte ich sie nicht als Singleton definieren

In meiner App müssen viele Klassen Zeitobjekte erstellen. Manchmal sind diese Klassen tief verschachtelt.

Angenommen, ich habe eine Klasse A, die Instanzen von Klasse B usw. bis Klasse D enthält. Klasse D muss Zeitobjekte erstellen.

In meiner naiven Implementierung gebe ich die TimeFactory an den Konstruktor der Klasse A weiter, der sie an den Konstruktor der Klasse B weitergibt, und so weiter, bis Klasse D.

Stellen Sie sich vor, ich habe ein paar Klassen wie TimeFactory und ein paar Klassenhierarchien wie die oben genannte: Ich verliere die Flexibilität und Lesbarkeit, die ich für die Verwendung von Dependancy Injection empfinde.

Ich frage mich, ob meine App keinen größeren Designfehler aufweist. Oder ist dies ein notwendiges Übel bei der Verwendung von Dependancy Injection?

Was denkst du ?

Dinaiz
quelle
5
Ich habe diese Frage Programmierern anstelle von Stackoverflow gestellt, da sich Programmierer meines Wissens eher auf designbezogene Aufgaben und Stackoverflow auf Fragen beziehen, bei denen Sie sich vor Ihren Compiler gestellt fühlen. Entschuldigung im Voraus, wenn ich falsch liege.
Dinaiz
2
aspektorientierte Programmierung, ist auch gut für diese Aufgaben - en.wikipedia.org/wiki/Aspect-oriented_programming
EL Yusubov
Ist die Klasse A eine Fabrik für die Klassen B, C und D? Wenn nicht, warum werden dann Instanzen davon erstellt? Wenn nur Klasse D Zeitobjekte benötigt, warum würden Sie dann eine andere Klasse mit TimeFactory injizieren? Braucht die Klasse D wirklich TimeFactory, oder könnte nur eine Instanz von Time in sie injiziert werden?
Simoraman
D muss Zeitobjekte erstellen, mit Informationen, die nur D haben soll. Ich muss über den Rest Ihres Kommentars nachdenken. Möglicherweise liegt ein Problem bei "Wer erstellt wen
?
"who's Creating Who" wird durch IoC-Container gelöst, siehe meine Antwort für Details. "who's Creating Who" ist eine der besten Fragen, die Sie bei der Verwendung von Dependency Injection stellen können.
GameDeveloper

Antworten:

23

In meiner App müssen viele Klassen Zeitobjekte erstellen

Ihre TimeKlasse scheint ein sehr grundlegender Datentyp zu sein, der zur "allgemeinen Infrastruktur" Ihrer Anwendung gehören sollte. DI funktioniert für solche Klassen nicht gut. Überlegen Sie, was es bedeutet, wenn eine Klasse wie stringin jeden Teil des Codes, der Zeichenfolgen verwendet, eingefügt werden muss und Sie eine stringFactoryals einzige Möglichkeit zum Erstellen neuer Zeichenfolgen verwenden müssen - die Lesbarkeit Ihres Programms würde sich um eine Größenordnung von verringern Größe.

Also mein Vorschlag: benutze DI nicht für allgemeine Datentypen wie Time. Schreiben Sie Komponententests für dieTime Klasse selbst, und verwenden Sie sie dann überall in Ihrem Programm, genau wie die stringKlasse oder eine vectorKlasse oder eine andere Klasse der Standardbibliothek. Verwenden Sie DI für Komponenten, die wirklich voneinander entkoppelt werden sollen.

Doc Brown
quelle
Du hast es ganz richtig verstanden. "Die Lesbarkeit Ihres Programms würde um eine Größenordnung abnehmen." : genau das ist ja passiert. Vorausgesetzt, ich möchte noch eine Fabrik nutzen, ist es dann nicht SO schlimm, sie zu einem Singleton zu machen?
Dinaiz
3
@Dinaiz: Die Idee ist, eine enge Kopplung zwischen Timeund den anderen Teilen Ihres Programms zu akzeptieren . So könnte man auch eine dichte Ankopplung hinnehmen TimeFactory. Was ich jedoch vermeiden würde, ist ein einzelnes globales TimeFactoryObjekt mit einem Status (z. B. eine Gebietsschemainformation oder ähnliches), das böse Nebenwirkungen für Ihr Programm verursachen und allgemeine Tests und die Wiederverwendung sehr erschweren könnte. Machen Sie es entweder zustandslos oder verwenden Sie es nicht als Singleton.
Doc Brown
Eigentlich muss es einen Status haben. Wenn Sie also "Verwenden Sie es nicht als Singleton" sagen, meinen Sie "Verwenden Sie weiterhin die Abhängigkeitsinjektion, dh übergeben Sie die Instanz an jedes Objekt, das sie benötigt", oder?
Dinaiz
1
@Dinaiz: Ehrlich gesagt, das hängt davon ab, in welchem ​​Bundesstaat, in welcher Art und in welcher Anzahl Teile Ihres Programms verwendet werden Timeund in welchem TimeFactoryGrad Sie "evolvability" für zukünftige Erweiterungen benötigen TimeFactory.
Doc Brown
OK, ich werde versuchen, Dependancy Inject beizubehalten, habe aber ein clevereres Design, das dann nicht so viel Boilerplate erfordert!
Vielen
4

Was meinen Sie mit "Ich verliere all die Flexibilität und Lesbarkeit, die ich mit der Abhängigkeitsinjektion erhalten soll" - DI handelt nicht von Lesbarkeit. Es geht darum, die Abhängigkeit zwischen Objekten zu entkoppeln.

Es klingt, als hätten Sie Klasse A, die Klasse B erstellt, Klasse B, die Klasse C erstellt, und Klasse C, die Klasse D erstellt.

Was Sie haben sollten, ist Klasse B in Klasse A injiziert. Klasse C in Klasse B injiziert. Klasse D in Klasse C injiziert.

C0deAttack
quelle
DI kann über Lesbarkeit sein. Wenn eine globale Variable "magisch" in der Mitte einer Methode erscheint, hilft es nicht, den Code zu verstehen (aus Wartungssicht). Woher kommt diese Variable? Wem gehört es ? Wer initialisiert das? Und so weiter ... Für Ihren zweiten Punkt haben Sie wahrscheinlich Recht, aber ich denke nicht, dass dies in meinem Fall zutrifft. Vielen Dank, dass Sie sich die Zeit genommen haben, um zu antworten
Dinaiz
DI erhöht die Lesbarkeit hauptsächlich, weil "Neu / Löschen" auf magische Weise aus Ihrem Code entfernt wird. Wenn Sie sich nicht mit dynamischem Zuweisungscode auseinandersetzen müssen, ist dieser leichter zu lesen.
GameDeveloper
Vergessen Sie auch zu sagen, dass dies den Code ein bisschen stärker macht, da Sie keine Annahmen mehr darüber machen können, wie eine Abhängigkeit erstellt wird, sondern "nur noch brauchen". Und das macht die Klasse leichter zu pflegen .. siehe meine Antwort
GameDeveloper
3

Ich bin mir nicht sicher, warum Sie Ihre Zeitfabrik nicht zu einem Singleton machen wollen. Wenn Ihre gesamte App nur eine Instanz davon enthält, handelt es sich de facto um einen Singleton.

Abgesehen davon ist es sehr gefährlich, ein veränderbares Objekt gemeinsam zu nutzen, es sei denn, es wird ordnungsgemäß durch Synchronisierungsblöcke geschützt. In diesem Fall gibt es keinen Grund, kein Singleton zu sein.

Wenn Sie eine Abhängigkeitsinjektion durchführen möchten, sollten Sie sich das Spring-Framework oder andere Frameworks für die Abhängigkeitsinjektion ansehen, mit denen Sie mithilfe einer Annotation automatisch Parameter zuweisen können

molyss
quelle
Der Frühling ist für Java und ich benutze C ++. Aber warum haben dich 2 Leute abgewertet? Es gibt Punkte in Ihrem Beitrag, mit denen ich nicht einverstanden bin, aber immer noch ...
Dinaiz
Ich gehe davon aus, dass die Ablehnung darauf zurückzuführen ist, dass die Frage an sich nicht beantwortet wurde, vielleicht ebenso wie die Erwähnung von Spring. Der Vorschlag, einen Singleton zu verwenden, war jedoch in meinen Augen richtig und kann die Frage nicht beantworten - schlägt jedoch eine gültige Alternative vor und wirft ein interessantes Designproblem auf. Aus diesem Grund habe ich +1.
Fergus In London
1

Dies zielt darauf ab, eine ergänzende Antwort auf Doc Brown zu sein und auch auf unbeantwortete Kommentare von Dinaiz zu antworten, die sich immer noch auf die Frage beziehen.

Was Sie wahrscheinlich brauchen, ist ein Framework für DI. Komplexe Hierarchien zu haben, bedeutet nicht unbedingt ein schlechtes Design. Wenn Sie jedoch eine TimeFactory-Bottom-up-Methode (von A nach D) verwenden müssen, anstatt direkt in D zu injizieren, liegt wahrscheinlich ein Fehler in der Art und Weise vor, wie Sie die Abhängigkeitsinjektion durchführen.

Ein Singleton? Nein Danke. Wenn Sie nur eine Instanz benötigen, um sie für den gesamten Anwendungskontext freizugeben (Wenn Sie einen IoC-Container für DI wie Infector ++ verwenden, müssen Sie TimeFactory nur als einzelne Instanz binden), sehen Sie sich das folgende Beispiel an (C ++ 11 übrigens, aber C ++. Möglicherweise verschieben zu C ++ 11 erhalten Sie kostenlos die Leak-Free-Anwendung):

Infector::Container ioc; //your app's context

ioc.bindSingleAsNothing<TimeFactory>(); //declare TimeFactory to be shared
ioc.wire<TimeFactory>(); //wire its constructor 

// if you want to be sure TimeFactory is created at startup just request it
// (else it will be created lazily only when needed)
auto myTimeFactory = ioc.buildSingle<TimeFactory>();

Der Vorteil eines IoC-Containers besteht nun darin, dass Sie die Zeitfactory nicht an D übergeben müssen. Wenn Ihre Klasse "D" eine Zeitfactory benötigt, geben Sie einfach die Zeitfactory als Konstruktorparameter für die Klasse D ein.

ioc.bindAsNothing<A>(); //declare class A
ioc.bindAsNothing<B>(); //declare class B
ioc.bindAsNothing<D>(); //declare class D

//constructors setup
ioc.wire<D, TimeFactory>(); //time factory injected to class D
ioc.wire<B, D>(); //class D injected to class B
ioc.wire<A, B>(); //class B injected to class A

Wie Sie sehen, spritzen Sie TimeFactory nur einmal. Wie benutze ich "A"? Ganz einfach, jede Klasse wird eingespritzt, in der Hauptleitung gebaut oder mit einer Fabrik verbunden.

auto myA1 = ioc.build<A>(); //A is not "single" so many different istances
auto myA2 = ioc.build<A>(); //can live at same time

Jedes Mal, wenn Sie Klasse A erstellen, werden automatisch alle Abhängigkeiten (Lazy Istantiation) und TimeFactory (D) eingefügt. Wenn Sie also nur 1 Methode aufrufen, haben Sie Ihre gesamte Hierarchie bereit (und auch komplexe Hierarchien werden auf diese Weise gelöst) Entfernen VIELES Kesselplattencodes): Sie müssen nicht "new / delete" aufrufen, und das ist sehr wichtig, da Sie die Anwendungslogik vom Klebercode trennen können.

D kann Zeitobjekte mit Informationen erstellen, die nur D haben kann

Das ist ganz einfach, Ihre TimeFactory verfügt über eine "create" -Methode, verwenden Sie dann einfach eine andere Signatur "create (params)" und fertig. Parameter, die keine Abhängigkeiten darstellen, werden häufig auf diese Weise aufgelöst. Dadurch entfällt auch die Pflicht zum Injizieren von Elementen wie "Strings" oder "Integer", da nur eine zusätzliche Kesselplatte hinzugefügt wird.

Wer schafft wen? Der IoC-Container erstellt Bestände und Fabriken, die Fabriken erstellen den Rest (Fabriken können verschiedene Objekte mit beliebigen Parametern erstellen, sodass Sie keinen Status für Fabriken benötigen). Sie können die Fabriken weiterhin als Wrapper für den IoC-Container verwenden: Im Allgemeinen ist das Injizieren in den IoC-Container sehr schlecht und entspricht der Verwendung eines Service-Locators. Einige Leute haben das Problem gelöst, indem sie den IoC-Container mit einer Fabrik umhüllt haben (dies ist nicht unbedingt erforderlich, hat aber den Vorteil, dass die Hierarchie durch den Container gelöst wird und alle Ihre Fabriken noch einfacher zu warten sind).

//factory method
std::unique_ptr<myType> create(params){
    auto istance = ioc->build<myType>(); //this code's agnostic to "myType" hierarchy
    istance->setParams(params); //the customization you needed
    return std::move(istance); 
}

Missbrauchen Sie auch nicht die Abhängigkeitsinjektion. Einfache Typen können nur Klassenmitglieder oder Variablen mit lokalem Gültigkeitsbereich sein. Dies scheint offensichtlich zu sein, aber ich habe gesehen, wie Leute "std :: vector" injizierten, nur weil es ein DI-Framework gab, das dies erlaubte. Denken Sie immer an Demeters Gesetz: "Injizieren Sie nur, was Sie wirklich brauchen, um zu injizieren"

Spielentwickler
quelle
Wow, ich habe deine Antwort vorher nicht gesehen, sorry! Sehr informativ, Sir! Upvoted
Dinaiz
0

Müssen Ihre Klassen A, B und C auch Zeitinstanzen erstellen oder nur Klasse D? Wenn es nur Klasse D ist, sollten A und B nichts über TimeFactory wissen. Erstellen Sie eine Instanz von TimeFactory in der C-Klasse und übergeben Sie sie an die Klasse D. Beachten Sie, dass mit "Instanz erstellen" nicht unbedingt gemeint ist, dass die C-Klasse für die Instanziierung der TimeFactory verantwortlich sein muss. Es kann DClassFactory von Klasse B empfangen, und DClassFactory weiß, wie eine Zeitinstanz erstellt wird.

Eine Technik, die ich auch oft verwende, wenn ich kein DI-Framework habe, besteht darin, zwei Konstruktoren bereitzustellen, einen, der eine Factory akzeptiert, und einen anderen, der eine Standard-Factory erstellt. Der zweite hat normalerweise einen geschützten / Paketzugriff und wird hauptsächlich für Komponententests verwendet.

rmaruszewski
quelle
-1

Ich habe ein weiteres C ++ - Framework für das Einfügen von Abhängigkeiten implementiert, das kürzlich für boost vorgeschlagen wurde - https://github.com/krzysztof-jusiak/di - Bibliothek ist ohne Makros (kostenlos), nur Header, C ++ 03 / C ++ 11 / C ++ 14-Bibliothek, die typensichere, makrofreie Konstruktorabhängigkeitsinjektion zur Verfügung stellt.

krzychoo
quelle
3
Die Beantwortung alter Fragen zu P.SE ist eine schlechte Möglichkeit, für Ihr Projekt zu werben. Bedenken Sie, dass es Zehntausende von Projekten gibt, die für einige Fragen relevant sein können. Wenn alle ihre Autoren dies hier posten würden, würde die Antwortqualität der Website sinken.