C ++: Sollte die Klasse ihre Abhängigkeiten besitzen oder beachten?

16

Angenommen, ich habe eine Klasse Foobar, die Klasse verwendet (hängt davon ab) Widget. In guten Widgetalten Tagen sollte wolud als Feld Foobaroder 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 Foobarund Widgetzusammen. Die Lösung ist scheinbar einfach: Wenden Sie die Abhängigkeitsinjektion an, um die Konstruktion von Abhängigkeiten aus der FoobarKlasse 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) {}
    (…)
}

FoobarAnsprüche, die alleiniges Eigentum sind Widget, gehen auf sie über. Dies hat folgende Vorteile:

  1. Auswirkungen auf die Leistung sind vernachlässigbar.
  2. Es ist sicher, da es Foobardie Lebensdauer kontrolliert Widgetund so sicherstellt, dass Widgetes nicht plötzlich verschwindet.
  3. Es ist sicher, dass Widgetes nicht ausläuft und ordnungsgemäß zerstört wird, wenn es nicht mehr benötigt wird.

Dies ist jedoch mit folgenden Kosten verbunden:

  1. Hiermit wird die Verwendung von WidgetInstanzen eingeschränkt , z. B. kann kein Stapel Widgetsverwendet werden, es Widgetkann 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:

  1. Universeller, da Abhängigkeiten gemeinsam genutzt werden können.
  2. Behält die Sicherheit (Punkte 2 und 3) der unique_ptrLösung bei.

Nachteile:

  1. Verschwendet Ressourcen, wenn keine Freigabe erfolgt.
  2. 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:

  1. So einfach wie es nur geht.
  2. Universal, nimmt gerade irgendwelche an Widget.

Nachteile:

  1. Nicht mehr sicher.
  2. Führt eine weitere Einheit ein, die für das Eigentum von Foobarund verantwortlich ist Widget.

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 Foobarssowieso etwas zu handhaben ist, also ist das Handhaben Widgetseine 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?

el.pescado
quelle
1
Wenn Sie von Anfang an unter C ++ 11 kompilieren können und / oder niemals, ist es nicht mehr empfehlenswert, einen einfachen Zeiger als Mittel zum Umgang mit Ressourcen in einer Klasse zu haben. Wenn die Foobar-Klasse der einzige Besitzer der Ressource ist und die Ressource freigegeben werden soll, std::unique_ptrist es der richtige Weg, wenn Foobar den Gültigkeitsbereich verlässt . Mit können Sie std::move()den Besitz einer Ressource vom oberen Bereich auf die Klasse übertragen.
Andy
1
Woher weiß ich, dass Foobardas 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.
el.pescado
1
@ el.pescado Es gibt auch das gleiche Problem in Java, Speicher ist nicht die einzige Ressource. Es gibt auch Dateien und Verbindungen zum Beispiel. Die übliche Möglichkeit, dies in Java zu lösen, besteht darin, einen Container zu haben, der den Lebenszyklus aller darin enthaltenen Objekte verwaltet. Daher müssen Sie sich in den Objekten, die in einem DI-Container enthalten sind, nicht darum kümmern. Dies kann auch für c ++ - Anwendungen funktionieren, wenn der DI-Container weiß, wann er in welche Lebenszyklusphase wechseln muss.
SpaceTrucker
3
Natürlich könnten Sie es auf die altmodische Art und Weise tun, ohne all die Komplexität und den Code, der funktioniert und viel einfacher zu warten ist. (Beachten Sie, dass die Java-Jungs zwar lachen, sich aber ständig um die
Umgestaltung bemühen
3
Außerdem ist es kein Grund, andere Leute über dich lachen zu lassen, wie sie zu sein. Hören Sie sich ihre Argumente an (außer "weil wir dazu aufgefordert wurden") und implementieren Sie DI, wenn dies einen konkreten Nutzen für Ihr Projekt bringt. Stellen Sie sich in diesem Fall das Muster als eine sehr allgemeine Regel vor, die Sie projektspezifisch anwenden müssen - alle drei Ansätze (und alle anderen potenziellen Ansätze, an die Sie noch nicht gedacht haben) sind gültig Sie bringen einen Gesamtnutzen (dh sie haben mehr Vor- als Nachteile).

Antworten:

2

Ich wollte dies als Kommentar schreiben, aber es stellte sich als zu lang heraus.

Woher weiß ich, dass Foobardas 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 Besitz dieser Abhängigkeiten entkoppelt (da der Besitz an die Konstruktion gebunden ist). In Umgebungen mit Speicherbereinigung wie Java ist dies kein Problem. In C ++ ist dies.

Ob Sie std::unique_ptr<Widget>oder verwenden std::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 z Foobar. Nach dem DI-Prinzip benötigen Sie die WidgetInstanz, um sie mit Foobardem Konstruktor von Utilities::Factory's createWidget(const std::vector<std::string>& params)einzufügen. Dies bedeutet, dass Sie in einer der Methoden von ' s beispielsweise das Widget erstellen und in das FoobarObjekt einfügen .

Nun haben Sie eine Utilities::FactoryMethode, mit der das WidgetObjekt 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 FoobarKlasse dargestellt, sodass sich das Fenster Foobarwie ein Controller verhält.

Der Controller wird wahrscheinlich einige Ihrer Widgets 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 Widgetspezifisch an dieses Fenster gebundenes gibt , was bedeutet, dass es nirgendwo anders angezeigt wird. Wenn Sie also das Fenster schließen, benötigen Sie weder Widgetirgendwo 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::movevon wird std::unique_ptrder Besitz vollständig übertragen. Selbst wenn Sie object Aeine Methode erstellen und object Bals Abhängigkeit an eine andere übergeben , object Bist diese nun für die Ressource verantwortlich object Aund löscht sie ordnungsgemäß, wenn object Bder Gültigkeitsbereich überschritten wird .

Andy
quelle
Die allgemeine Vorstellung von Eigentum und intelligenten Zeigern ist mir klar. Es scheint mir einfach nicht nett zu sein, mit der Idee von DI zu spielen. In der Abhängigkeitsinjektion geht es für mich darum, Klassen von ihren Abhängigkeiten zu entkoppeln. In diesem Fall beeinflusst die Klasse jedoch weiterhin, wie ihre Abhängigkeiten erstellt werden.
el.pescado
Zum Beispiel haben das Problem , das ich mit unique_ptrist Unit - Tests (DI wird beworben als Prüfung freundlich): Ich testen wollen Foobar, so dass ich schaffen Widget, gibt sie an Foobar, Übung Foobarund dann möchte ich untersuchen Widget, aber es sei denn , Foobares irgendwie macht, kann ich nicht seit es von behauptet wurde Foobar.
el.pescado
@ el.pescado Du mischst zwei Dinge zusammen. Sie mischen Zugänglichkeit und Eigentum. Nur weil Foobardie Ressource im Besitz ist, heißt das nicht, dass sie von niemand anderem verwendet werden sollte. Sie können eine FoobarMethode wie implementieren Widget* 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 die WidgetKlasse testen möchten .
Andy
Guter Punkt, das macht Sinn.
el.pescado
1
Wenn Sie einen shared_ptr in einen unique_ptr konvertiert haben, wie möchten Sie unique_ness garantieren?
Aconcagua,
2

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_ptrist dies die richtige Antwort.

Dominique McDonnell
quelle
1

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!

namespace WidgetFactory
{
    Widget* create(int, int)
    {
        return 0;
    }
    Widget* create(int, bool, long)
    {
        return 0;
    }
}
class FooBar
{
public:
    template < typename ...Arguments >
    FooBar(Arguments... arguments)
        : mWidget(WidgetFactory::create(arguments...))
    {
    }
    ~FooBar()
    {
        delete mWidget;
    }
private:
    Widget* mWidget;
};

FooBar foobar1(10, 12);
FooBar foobar2(51, true, 54L);

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).

Aconcagua
quelle
1
Verwenden Sie classes 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.
Andy
@ DavidPacker Hmm, klar, Sie haben Recht - ich ging stillschweigend davon aus, dass die Klasse einige private Interna hat, aber das ist weder sichtbar noch anderswo erwähnt ...
Aconcagua
1

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.

mattnewport
quelle
0

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:

class Foobar {
    Widget widget;
public:
    Foobar(Widget w) // pass w by value
     : widget{ std::move(w) } {}
};

Der Client-Code kann dann schreiben:

Widget w;
Foobar f1{ w }; // default: copy w into f1
Foobar f2{ std::move(w) }; // move w into f2

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).

utnapistim
quelle
1
Dies schließt ein polymorphes Verhalten aus, das die Wertschöpfung stark einschränkt.
el.pescado
Wahr. Ich habe nur darüber nachgedacht, es zu erwähnen, da es die Form ist, die ich am häufigsten verwende (ich verwende Vererbung in meiner Codierung nur für polymorphes Verhalten - keine Wiederverwendung von Code, daher habe ich nicht viele Fälle, in denen polymorphes Verhalten erforderlich ist).
utnapistim