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