Introduction to Dependency Injection in C++
When you first start learning C++, you write simple programs where classes create their dependencies directly. For example, a Car class creates an Engine object itself. This works for small projects, but as the code grows, this approach leads to serious problems: the code becomes tightly coupled, difficult to test, and hard to modify.
Dependency Injection (DI) is a design pattern where an object receives its dependencies from the outside, rather than creating them itself. This is one of the key principles of SOLID, specifically the Dependency Inversion Principle.
In this article, we will break down what Dependency Injection in C++ is, why it is needed, and how to apply it in practice. You will see how this pattern makes code flexible, testable, and maintainable.
Why It's Bad Without DI: The Problem of Tight Coupling
Imagine you are writing a smart home management system. There is a SmartLight class that controls a light bulb. In a naive implementation, it creates a Bulb object itself:
#include <iostream>#include <string>
class Bulb {public: void turnOn() { std::cout << "Bulb turned on"; } void turnOff() { std::cout << "Bulb turned off"; }};
class SmartLight {private: Bulb bulb; // Tight dependencypublic: void activate() { bulb.turnOn(); } void deactivate() { bulb.turnOff(); }};
int main() { SmartLight light; light.activate(); return 0;}What are the problems here?
- Tight coupling:
SmartLightis inseparably tied toBulb. If you want to use a different bulb (e.g., a smart LED or Phillips Hue), you have to change the class code. - Testing difficulty: You cannot replace the real bulb with a mock in unit tests.
- SRP violation: The
SmartLightclass does two things: manages the bulb and creates it.
This is a classic example of tight coupling, which Dependency Injection aims to eliminate.
What is Dependency Injection in C++: Core Concepts
Dependency Injection (DI) is a technique where an object receives its dependencies (usually via a constructor, setter, or interface) from an external source, rather than creating them itself. The main idea is: "Don't ask for what you need — it will be given to you."
In C++, DI is implemented through:
- Interfaces (abstract classes): Dependencies are declared via abstract types, not concrete classes.
- Constructor with parameters: Dependencies are passed through the constructor (the most common method).
- Setters: Dependencies are set after the object is created.
- Dependency containers: Special libraries that automatically create and inject dependencies (e.g.,
boost.di).
Let's rewrite the smart light example using DI:
#include <iostream>#include <memory>
// Abstract interface for a bulbclass IBulb {public: virtual void turnOn() = 0; virtual void turnOff() = 0; virtual ~IBulb() = default;};
// Concrete implementation: regular bulbclass Bulb : public IBulb {public: void turnOn() override { std::cout << "Bulb turned on"; } void turnOff() override { std::cout << "Bulb turned off"; }};
// Another implementation: LED bulbclass LedBulb : public IBulb {public: void turnOn() override { std::cout << "LED bulb turned on"; } void turnOff() override {