I am playing with dependency injection, but i am not sure I am doing it right.
Especially, I am not sure what should be the correct way to build classes with injected dependencies.
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?
-
It could be class A. However, in a large system, class A might end up creating and assembling a very large number of objects.
-
A separate builder class that will create D, C and B. A will use this builder class.
-
Some other option.
In addition, I read a lot about DI containers. However, it seems that there are no major frameworks for C++. Also, if I understand correctly, DI can be performed well even without containers. Am I correct?
8
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).
1
There is a DI framework for C++ (still under development AFAIK): Boost.DI.
There are some useful comments about the framework on reddit.
Who should be responsible for creating class D?
Every time the question like that pops up, it indicates a dependency to inject. The whole idea is to “delegate” responsibility outside of the object that seems to be having a problem figuring how to handle it.
Using your example, since class A doesn’t seem to possess sufficient “knowledge” to figure how to create D, you invert the control and expose this as a dependency needed for class A, by making it require a factory that knows how to create instances of class D.
7