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?
You're jumping steps. Consider a set of conventions optimized for loose coupling and exception safety. The rules go like this:
R1: if A contains a B, then the constructor of A receives a fully constructed B (i.e. not "B's construction dependencies"). Similarly, if B's construction requires a C, it will receive a C, not C's dependencies.
R2: if a full chain of objects are required to construct an object, the chained construction is extracted/automated within a factory (function or class).
Code (std::move calls omitted for simplicity):
struct D { int dummy; }; struct C { D d; }; struct B { C c; } struct A { B make_b(C c) {return B{c}; };
In such a system, "who creates D" is irrelevant, because when you call make_b, you need a C, not a D.
Client code:
A a; // factory instance // construct a B instance: D d; C c {d}; B = a.make_b(c);
Here, D is created by the client code. Naturally, if this code is repeated more than once, you are free to extract it into a function (see R2 above):
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); }
There is a natural tendency to skip the definition of make_b (ignore R1), and write the code directly like this:
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 this case, you have the following problems:
you have monolithic code; If you come to a situation in client code where you need to make a B from an existent C, you cannot use make_b. You will either need to write a new factory, or the definition of make_b, and all the client code using the old make_b.
Your view of dependencies is muddled when you look at the source: Now, by looking at the source you get to think that you need a D instance, when in fact you may just need a C.
Example:
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); }; }
- The omission of
struct A { B make_b(C c); } will greatly increase coupling: now A needs to know the definitions of both B and C (instead of just C). You also have restrictions on any client code using A, B, C and D, imposed on your project because you skipped a step in the definition of a factory method (R1).
TLDR: In short, do not pass the outermost dependency to a factory, but the closest ones. This makes your code robust, easily alterable, and renders the question you posed ("who creates D") into an irrelevant question for the implementation of make_b (because make_b no longer receives a D but a more immediate dependency - C - and this is injected as a parameter of make_b).