Object-oriented – Injecting dependencies (DI) in c++ applications

cdependency-injectiondesignobject-oriented

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?

  1. It could be class A. However, in a large system, class A might end up creating and assembling a very large number of objects.

  2. A separate builder class that will create D, C and B. A will use this builder class.

  3. 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?

Best Answer

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

Related Topic