Injizieren von Abhängigkeiten (DI) in C ++ - Anwendungen

8

Ich spiele mit Abhängigkeitsinjektion, bin mir aber nicht sicher, ob ich es richtig mache. Insbesondere bin ich mir nicht sicher, wie Klassen mit injizierten Abhängigkeiten richtig erstellt werden sollen.

Angenommen, ich habe eine Klasse A, die Klasse B erstellt. Klasse B hängt von Klasse C ab und Klasse C hängt von Klasse D ab. Wer sollte für die Erstellung von Klasse D verantwortlich sein?

  1. Es könnte Klasse A sein. In einem großen System kann Klasse A jedoch eine sehr große Anzahl von Objekten erstellen und zusammensetzen.

  2. Eine separate Builder-Klasse, die D, C und B erstellt. A verwendet diese Builder-Klasse.

  3. Eine andere Option.

Außerdem habe ich viel über DI-Container gelesen. Es scheint jedoch, dass es keine wesentlichen Frameworks für C ++ gibt. Wenn ich das richtig verstehe, kann DI auch ohne Container gut durchgeführt werden. Hab ich recht?

Erik Sapir
quelle
... vorausgesetzt natürlich, dass Sie tatsächlich einen DI-Container benötigen. Die meisten Anwendungen tun dies nicht.
Robert Harvey
Woher weiß ich, ob ich einen Container brauche?
Erik Sapir

Antworten:

6

Angenommen, ich habe eine Klasse A, die Klasse B erstellt. Klasse B hängt von Klasse C ab und Klasse C hängt von Klasse D ab. Wer sollte für die Erstellung von Klasse D verantwortlich sein?

Du springst Schritte. Betrachten Sie eine Reihe von Konventionen, die für lose Kopplung und Ausnahmesicherheit optimiert sind. Die Regeln lauten wie folgt:

  • R1: Wenn A ein B enthält, erhält der Konstruktor von A ein vollständig konstruiertes B (dh nicht "Konstruktionsabhängigkeiten von B"). Wenn die Konstruktion von B ein C erfordert, erhält es in ähnlicher Weise ein C und nicht die Abhängigkeiten von C.

  • R2: Wenn eine vollständige Kette von Objekten erforderlich ist, um ein Objekt zu erstellen, wird die verkettete Konstruktion innerhalb einer Fabrik (Funktion oder Klasse) extrahiert / automatisiert.

Code (std :: move-Aufrufe sind der Einfachheit halber weggelassen):

struct D { int dummy; };
struct C { D d; };
struct B { C c; }

struct A { B make_b(C c) {return B{c}; };

In einem solchen System ist "wer D erstellt" irrelevant, denn wenn Sie make_b aufrufen, benötigen Sie ein C, kein D.

Kundencode:

A a; // factory instance

// construct a B instance:
D d;
C c {d};
B = a.make_b(c);

Hier wird D durch den Client-Code erstellt. Wenn dieser Code mehrmals wiederholt wird, können Sie ihn natürlich in eine Funktion extrahieren (siehe R2 oben):

B make_b_from_d(D& d) // you should probably inject A instance here as well
{
    C c {d};
    A a;
    return a.make_b(c);
}

Es gibt eine natürliche Tendenz, die Definition von make_b zu überspringen ( R1 ignorieren ) und den Code direkt so zu schreiben:

struct D { int dummy; };
struct C { D d; };
struct B { C c; }

struct A { B make_b(D d) { C c; return B{c}; }; // make B from D directly

In diesem Fall haben Sie folgende Probleme:

  • Sie haben monolithischen Code; Wenn Sie im Clientcode zu einer Situation kommen, in der Sie aus einem vorhandenen C ein B erstellen müssen, können Sie make_b nicht verwenden. Sie müssen entweder eine neue Factory oder die Definition von make_b und den gesamten Client-Code mit dem alten make_b schreiben.

  • Ihre Sicht auf Abhängigkeiten ist durcheinander, wenn Sie sich die Quelle ansehen: Wenn Sie sich nun die Quelle ansehen, denken Sie, dass Sie eine D-Instanz benötigen, obwohl Sie möglicherweise nur eine C benötigen.

Beispiel:

void sub_optimal_solution(C& existent_c) {
    // you cannot create a B here using existent_C, because your A::make_b
    // takes a D parameter; B's construction doesn't actually need a D
    // but you cannot see that at all if you just have:
    // struct A { B make_b(D d); };
}
  • Das Weglassen von struct A { B make_b(C c); }wird die Kopplung erheblich erhöhen: Jetzt muss A die Definitionen von B und C kennen (anstatt nur C). Sie haben auch Einschränkungen für jeden Clientcode, der A, B, C und D verwendet und Ihrem Projekt auferlegt wurde, da Sie einen Schritt bei der Definition einer Factory-Methode ( R1 ) übersprungen haben .

TLDR: Kurz gesagt, geben Sie die äußerste Abhängigkeit nicht an eine Fabrik weiter, sondern an die nächstgelegenen. Dies macht Ihren Code robust, leicht änderbar und macht die von Ihnen gestellte Frage ("Wer erstellt D") zu einer irrelevanten Frage für die Implementierung von make_b (da make_b kein D mehr erhält, sondern eine unmittelbarere Abhängigkeit - C - und das ist injiziert als Parameter von make_b).

utnapistim
quelle
1
Die Frage, ob D erstellt werden soll, bleibt weiterhin bestehen. Ich versuche immer noch zu verstehen, welcher Teil des Systems für die Konstruktion von D
Erik Sapir am
0

Wer sollte für die Erstellung der Klasse D verantwortlich sein?

Jedes Mal, wenn eine solche Frage auftaucht, weist sie auf eine zu injizierende Abhängigkeit hin . Die ganze Idee besteht darin, die Verantwortung außerhalb des Objekts zu "delegieren", bei dem es anscheinend Probleme gibt, herauszufinden, wie man damit umgeht.

Verwenden Sie Ihr Beispiel, da Klasse A nicht über ausreichende "Kenntnisse" zu verfügen scheint, um herauszufinden, wie D erstellt wird, invertieren Sie das Steuerelement und legen dies als eine für Klasse A erforderliche Abhängigkeit offen, indem Sie eine Factory benötigen , die weiß, wie man erstellt Instanzen der Klasse D.

Mücke
quelle
Es macht Sinn, aber am Ende komme ich zur ersten Klasse im System (Main), die keine Abhängigkeiten bekommen kann. Es scheint, dass diese Klasse eine Reihe von Fabriken / Objekten initialisieren müsste
Erik Sapir
Die Frage, wie mit der Verantwortung für die Schaffung der Klasse D umzugehen ist, scheint gelöst zu sein, nicht wahr? Was die "Verkabelung" der Dinge in der ersten Klasse des Systems (Main) betrifft, wäre dies eine andere Frage . Erwägen Sie, sie separat zu veröffentlichen.
Mücke
Eigentlich ist das nicht gerade eine andere Frage - die Frage war, wer für die Durchführung aller Initialisierungen (neue Operationen) verantwortlich ist. Ich bin nicht sicher, ob es weitere Fragen verdient. Auf jeden Fall würde ich wirklich gerne hier eine Antwort auf diese Frage
Erik Sapir
1
Ich habe eine zusätzliche Frage gestellt: programmers.stackexchange.com/questions/233828/…
Erik Sapir
1
"Software-Frameworks, Rückrufe, Scheduler, Ereignisschleifen und Abhängigkeitsinjektion sind Beispiele für Entwurfsmuster, die der Umkehrung des Steuerungsprinzips folgen ..." ( Wikipedia )
Mücke