Angenommen, ich habe eine Klasse Foobar
, die Klasse verwendet (hängt davon ab) Widget
. In guten Widget
alten Tagen sollte wolud als Feld Foobar
oder als intelligenter Zeiger deklariert werden, wenn polymorphes Verhalten erforderlich ist, und im Konstruktor initialisiert werden:
class Foobar {
Widget widget;
public:
Foobar() : widget(blah blah blah) {}
// or
std::unique_ptr<Widget> widget;
public:
Foobar() : widget(std::make_unique<Widget>(blah blah blah)) {}
(…)
};
Und wir wären fertig. Leider werden uns Java-Kids heute auslachen, wenn sie es sehen, und das zu Recht, wenn es sich paart Foobar
und Widget
zusammen. Die Lösung ist scheinbar einfach: Wenden Sie die Abhängigkeitsinjektion an, um die Konstruktion von Abhängigkeiten aus der Foobar
Klasse herauszunehmen . Aber dann zwingt C ++ uns, über die Inhaberschaft von Abhängigkeiten nachzudenken. Drei Lösungen kommen in den Sinn:
Eindeutiger Zeiger
class Foobar {
std::unique_ptr<Widget> widget;
public:
Foobar(std::unique_ptr<Widget> &&w) : widget(w) {}
(…)
}
Foobar
Ansprüche, die alleiniges Eigentum sind Widget
, gehen auf sie über. Dies hat folgende Vorteile:
- Auswirkungen auf die Leistung sind vernachlässigbar.
- Es ist sicher, da es
Foobar
die Lebensdauer kontrolliertWidget
und so sicherstellt, dassWidget
es nicht plötzlich verschwindet. - Es ist sicher, dass
Widget
es nicht ausläuft und ordnungsgemäß zerstört wird, wenn es nicht mehr benötigt wird.
Dies ist jedoch mit folgenden Kosten verbunden:
- Hiermit wird die Verwendung von
Widget
Instanzen eingeschränkt , z. B. kann kein StapelWidgets
verwendet werden, esWidget
kann kein gemeinsam genutzter Stapel verwendet werden.
Gemeinsamer Zeiger
class Foobar {
std::shared_ptr<Widget> widget;
public:
Foobar(const std::shared_ptr<Widget> &w) : widget(w) {}
(…)
}
Dies ist wahrscheinlich das Äquivalent zu Java und anderen Sprachen mit Speicherbereinigung. Vorteile:
- Universeller, da Abhängigkeiten gemeinsam genutzt werden können.
- Behält die Sicherheit (Punkte 2 und 3) der
unique_ptr
Lösung bei.
Nachteile:
- Verschwendet Ressourcen, wenn keine Freigabe erfolgt.
- Erfordert weiterhin die Zuweisung von Heapspeichern und verbietet die Zuweisung von Stack-Objekten.
Einfacher alter Beobachtungszeiger
class Foobar {
Widget *widget;
public:
Foobar(Widget *w) : widget(w) {}
(…)
}
Platzieren Sie den rohen Zeiger in der Klasse und verlagern Sie die Last des Eigentums auf eine andere Person. Vorteile:
- So einfach wie es nur geht.
- Universal, nimmt gerade irgendwelche an
Widget
.
Nachteile:
- Nicht mehr sicher.
- Führt eine weitere Einheit ein, die für das Eigentum von
Foobar
und verantwortlich istWidget
.
Einige verrückte Template-Metaprogramme
Der einzige Vorteil, den ich mir vorstellen kann, ist, dass ich all die Bücher lesen kann, für die ich keine Zeit gefunden habe, während meine Software erstellt wird;)
Ich neige zur dritten Lösung, da sie am universellsten ist und Foobars
sowieso etwas zu handhaben ist, also ist das Handhaben Widgets
eine einfache Veränderung. Die Verwendung von rohen Zeigern stört mich jedoch, andererseits fühle ich mich als intelligente Zeigerlösung falsch, da sie den Abhängigkeitskonsumenten einschränken, wie diese Abhängigkeit erstellt wird.
Vermisse ich etwas? Oder ist nur Dependency Injection in C ++ nicht trivial? Sollte die Klasse ihre Abhängigkeiten besitzen oder nur beobachten?
quelle
std::unique_ptr
ist es der richtige Weg, wenn Foobar den Gültigkeitsbereich verlässt . Mit können Siestd::move()
den Besitz einer Ressource vom oberen Bereich auf die Klasse übertragen.Foobar
das der einzige Besitzer ist? Im alten Fall ist es einfach. Das Problem bei DI ist jedoch, wie ich es sehe, dass es die Klasse von der Konstruktion ihrer Abhängigkeiten entkoppelt und sie auch vom Eigentum an diesen Abhängigkeiten entkoppelt (da das Eigentum an die Konstruktion gebunden ist). In Umgebungen mit Speicherbereinigung wie Java ist dies kein Problem. In C ++ ist dies.Antworten:
Ich wollte dies als Kommentar schreiben, aber es stellte sich als zu lang heraus.
Ob Sie
std::unique_ptr<Widget>
oder verwendenstd::shared_ptr<Widget>
sollten, liegt an Ihnen und kommt von Ihrer Funktionalität.Nehmen wir an, Sie haben eine
Utilities::Factory
, die für die Erstellung Ihrer Blöcke zuständig ist, wie zFoobar
. Nach dem DI-Prinzip benötigen Sie dieWidget
Instanz, um sie mitFoobar
dem Konstruktor vonUtilities::Factory
'screateWidget(const std::vector<std::string>& params)
einzufügen. Dies bedeutet, dass Sie in einer der Methoden von ' s beispielsweise das Widget erstellen und in dasFoobar
Objekt einfügen .Nun haben Sie eine
Utilities::Factory
Methode, mit der dasWidget
Objekt erstellt wurde. Bedeutet das, die Methode sollte ich für deren Löschung verantwortlich machen? Natürlich nicht. Es ist nur da, um Sie zur Instanz zu machen.Stellen wir uns vor, Sie entwickeln eine Anwendung mit mehreren Fenstern. Jedes Fenster wird mithilfe der
Foobar
Klasse dargestellt, sodass sich das FensterFoobar
wie ein Controller verhält.Der Controller wird wahrscheinlich einige Ihrer
Widget
s nutzen und Sie müssen sich fragen:Wenn ich in meiner Anwendung auf dieses bestimmte Fenster gehe, benötige ich diese Widgets. Werden diese Widgets von anderen Anwendungsfenstern gemeinsam genutzt? In diesem Fall sollte ich sie wahrscheinlich nicht noch einmal erstellen, da sie immer gleich aussehen, weil sie gemeinsam genutzt werden.
std::shared_ptr<Widget>
ist der Weg zu gehen.Sie haben auch ein Anwendungsfenster, in dem es nur ein
Widget
spezifisch an dieses Fenster gebundenes gibt , was bedeutet, dass es nirgendwo anders angezeigt wird. Wenn Sie also das Fenster schließen, benötigen Sie wederWidget
irgendwo in Ihrer Anwendung noch zumindest deren Instanz.Hier
std::unique_ptr<Widget>
kommt es, um seinen Thron zu beanspruchen.Aktualisieren:
Ich stimme @DominicMcDonnell in Bezug auf das Lebenszeitproblem nicht wirklich zu . Durch das Aufrufen
std::move
von wirdstd::unique_ptr
der Besitz vollständig übertragen. Selbst wenn Sieobject A
eine Methode erstellen undobject B
als Abhängigkeit an eine andere übergeben ,object B
ist diese nun für die Ressource verantwortlichobject A
und löscht sie ordnungsgemäß, wennobject B
der Gültigkeitsbereich überschritten wird .quelle
unique_ptr
ist Unit - Tests (DI wird beworben als Prüfung freundlich): Ich testen wollenFoobar
, so dass ich schaffenWidget
, gibt sie anFoobar
, ÜbungFoobar
und dann möchte ich untersuchenWidget
, aber es sei denn ,Foobar
es irgendwie macht, kann ich nicht seit es von behauptet wurdeFoobar
.Foobar
die Ressource im Besitz ist, heißt das nicht, dass sie von niemand anderem verwendet werden sollte. Sie können eineFoobar
Methode wie implementierenWidget* getWidget() const { return this->_widget.get(); }
, die den Rohzeiger zurückgibt, mit dem Sie arbeiten können. Sie können diese Methode dann als Eingabe für Ihre Komponententests verwenden, wenn Sie dieWidget
Klasse testen möchten .Ich würde einen Beobachtungszeiger in Form einer Referenz verwenden. Es gibt eine viel bessere Syntax, wenn Sie es verwenden, und das hat den semantischen Vorteil, dass es keine Eigentümerschaft impliziert, was ein einfacher Zeiger kann.
Das größte Problem bei diesem Ansatz sind die Lebensdauern. Sie müssen sicherstellen, dass die Abhängigkeit vor und nach Ihrer abhängigen Klasse erstellt wird. Das ist kein einfaches Problem. Die Verwendung gemeinsamer Zeiger (als Abhängigkeitsspeicher und in allen Klassen, die davon abhängen, Option 2 oben) kann dieses Problem beheben, führt jedoch auch das Problem der zirkulären Abhängigkeiten ein, das ebenfalls nicht trivial ist und meiner Meinung nach weniger offensichtlich und daher schwieriger ist erkennen, bevor es Probleme verursacht. Deshalb ziehe ich es vor, nicht automatisch zu arbeiten und die Lebensdauer und den Bauauftrag manuell zu verwalten. Ich habe auch Systeme gesehen, die einen Light-Template-Ansatz verwenden, der eine Liste von Objekten in der Reihenfolge ihrer Erstellung erstellt und in umgekehrter Reihenfolge zerstört hat. Das war nicht narrensicher, aber es hat die Dinge viel einfacher gemacht.
Aktualisieren
Die Antwort von David Packer ließ mich ein wenig über die Frage nachdenken. Die ursprüngliche Antwort gilt nach meiner Erfahrung für geteilte Abhängigkeiten. Dies ist einer der Vorteile der Abhängigkeitsinjektion. Sie können mehrere Instanzen verwenden, indem Sie eine Instanz einer Abhängigkeit verwenden. Wenn Ihre Klasse jedoch eine eigene Instanz einer bestimmten Abhängigkeit haben muss,
std::unique_ptr
ist dies die richtige Antwort.quelle
Zuallererst - das ist C ++, nicht Java - und hier laufen viele Dinge anders. Die Java-Leute haben keine Probleme mit dem Eigentum, da es eine automatische Garbage Collection gibt, die diese Probleme für sie löst.
Zweitens: Es gibt keine generelle Antwort auf diese Frage - es kommt auf die Anforderungen an!
Was ist das Problem bei der Kopplung von FooBar und Widget? FooBar möchte ein Widget verwenden, und wenn jede FooBar-Instanz sowieso immer ein eigenes und dasselbe Widget hat, lassen Sie es gekoppelt ...
In C ++ können Sie sogar "seltsame" Dinge tun, die in Java einfach nicht existieren, z. B. einen variablen Template-Konstruktor (naja, es gibt eine ... -Notation in Java, die auch in Konstruktoren verwendet werden kann, aber das ist es nur syntaktischer Zucker, um ein Objektarray zu verbergen, was eigentlich nichts mit echten variadischen Vorlagen zu tun hat!
Mag ich?
Natürlich gibt es Gründe , warum Sie beide Klassen entkoppeln möchten oder müssen - z. B. wenn Sie die Widgets zu einem Zeitpunkt erstellen müssen, zu dem noch keine FooBar-Instanz vorhanden ist, wenn Sie die Widgets wiederverwenden möchten oder müssen, ... oder einfach weil es für das aktuelle Problem passender ist (zB wenn Widget ein GUI-Element ist und FooBar eines sein soll / kann / darf).
Dann kommen wir zum zweiten Punkt zurück: Keine allgemeine Antwort. Sie müssen sich entscheiden, was - für das eigentliche Problem - die geeignetere Lösung ist. Ich mag den Referenzansatz von DominicMcDonnell, aber er kann nur angewendet werden, wenn das Eigentum nicht von FooBar übernommen werden soll (na ja, eigentlich könnten Sie das, aber das impliziert sehr, sehr schmutzigen Code ...). Abgesehen davon schließe ich mich der Antwort von David Packer an (die als Kommentar geschrieben werden sollte - aber trotzdem eine gute Antwort).
quelle
class
es in C ++ nur mit statischen Methoden, auch wenn es sich nur um eine Factory wie in Ihrem Beispiel handelt. Das verstößt gegen die OO-Prinzipien. Verwenden Sie stattdessen Namespaces und gruppieren Sie Ihre Funktionen damit.Ihnen fehlen mindestens zwei weitere Optionen, die Ihnen in C ++ zur Verfügung stehen:
Eine Möglichkeit ist die Verwendung der statischen Abhängigkeitsinjektion, bei der Ihre Abhängigkeiten Vorlagenparameter sind. Dies gibt Ihnen die Möglichkeit, Ihre Abhängigkeiten nach Wert zu halten, während Sie weiterhin die Abhängigkeitsinjektion für die Kompilierungszeit erlauben. STL-Container verwenden diesen Ansatz zum Beispiel für Allokatoren und Vergleichs- und Hash-Funktionen.
Eine andere Möglichkeit besteht darin, polymorphe Objekte mit einer tiefen Kopie nach ihrem Wert zu bestimmen. Die traditionelle Methode hierfür ist die Verwendung einer virtuellen Klonmethode. Eine weitere Option, die immer beliebter wird, ist die Verwendung der Typlöschung, um Werttypen zu erstellen, die sich polymorph verhalten.
Welche Option am besten geeignet ist, hängt von Ihrem Anwendungsfall ab. Es ist schwierig, eine allgemeine Antwort zu geben. Wenn Sie jedoch nur statischen Polymorphismus benötigen, sind Vorlagen der beste Weg, um mit C ++ umzugehen.
quelle
Sie haben eine vierte mögliche Antwort ignoriert, die den ersten von Ihnen veröffentlichten Code (die "guten alten Tage" des Speicherns eines Mitglieds nach Wert) mit der Abhängigkeitsinjektion kombiniert:
Der Client-Code kann dann schreiben:
Die Darstellung der Kopplung zwischen Objekten sollte nicht (ausschließlich) anhand der von Ihnen angegebenen Kriterien erfolgen (dh nicht ausschließlich auf der Grundlage von "Was ist besser für ein sicheres Lebensdauermanagement?").
Sie können auch konzeptionelle Kriterien verwenden ("ein Auto hat vier Räder" im Vergleich zu "ein Auto verwendet vier Räder, die der Fahrer mitbringen muss").
Sie können Kriterien von anderen APIs festlegen lassen (wenn Sie von einer API beispielsweise einen benutzerdefinierten Wrapper oder ein std :: unique_ptr erhalten, sind die Optionen in Ihrem Client-Code ebenfalls eingeschränkt).
quelle