Say I have a class A that creates class B. Class B depends on class C and class C depends on class D. Who should be responsible for creating class D?
Stai saltando dei passi. Si consideri una serie di convenzioni ottimizzate per l'accoppiamento lento e la sicurezza delle eccezioni. Le regole vanno così:
-
R1: se A contiene una B, quindi il costruttore di A riceve una B completamente costruita (cioè non "le dipendenze di costruzione di B"). Allo stesso modo, se la costruzione di B richiede una C, riceverà una dipendenza da C, non da C.
-
R2: se è necessaria una catena completa di oggetti per costruire un oggetto, la costruzione concatenata viene estratta / automatizzata all'interno di un factory (funzione o classe).
Codice (std :: sposta le chiamate omesse per semplicità):
struct D { int dummy; };
struct C { D d; };
struct B { C c; }
struct A { B make_b(C c) {return B{c}; };
In tale sistema, "chi crea D" è irrilevante, perché quando chiami make_b, hai bisogno di una C, non di una D.
Codice cliente:
A a; // factory instance
// construct a B instance:
D d;
C c {d};
B = a.make_b(c);
Qui D viene creato dal codice client. Naturalmente, se questo codice viene ripetuto più volte, sei libero di estrarlo in una funzione (vedi R2 sopra):
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);
}
C'è una tendenza naturale a saltare la definizione di make_b (ignora R1 ) e scrivere il codice direttamente in questo modo:
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 questo caso, hai i seguenti problemi:
-
hai un codice monolitico; Se si arriva a una situazione nel codice client in cui è necessario creare una B da una C esistente, non è possibile utilizzare make_b. Dovrai scrivere un nuovo factory, o la definizione di make_b, e tutto il codice client usando il vecchio make_b.
-
La tua visione delle dipendenze è confusa quando guardi la fonte: ora, osservando la fonte, pensi di aver bisogno di un'istanza di tipo D, quando in realtà potresti aver bisogno di una C
Esempio:
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); };
}
- L'omissione di
struct A { B make_b(C c); }
aumenterà notevolmente l'accoppiamento: ora A ha bisogno di conoscere le definizioni di B e C (invece di solo C). Hai anche restrizioni sul qualsiasi codice client che utilizza A, B, C e D, imposto sul tuo progetto perché hai saltato un passaggio nella definizione di un metodo factory ( R1 ) .
TLDR: In breve, non passare la dipendenza più esterna a una fabbrica, ma i più vicini. Questo rende il tuo codice robusto, facilmente modificabile e rende la domanda che hai posto ("chi crea D") in una domanda irrilevante per l'implementazione di make_b (perché make_b non riceve più D ma una dipendenza più immediata - C - e questo è iniettato come parametro di make_b).