Skip to content

Dependency Injection

Instead of a class creating its own dependencies, those dependencies are provided from outside.

Don't create what you need — ask for it or receive it.

Think: Making your own coffee at work (bad) vs Coffee machine provided by the office (good).

Problem Without DI

cpp
class UserService {
    MySQLDatabase db; // created internally — tightly coupled to MySQL!
public:
    void saveUser(string name) { db.save(name); }
    // what if we want MongoDB? must change UserService!
};

Define Abstraction First

cpp
class IDatabase {
public:
    virtual void save(string data) = 0;
    virtual ~IDatabase() {}
};

class MySQLDatabase : public IDatabase { void save(string d) override {/*...*/} };
class MongoDB : public IDatabase { void save(string d) override {/*...*/} };
class MockDatabase : public IDatabase { void save(string d) override {/*...*/} }; // for testing

Type 1: Constructor Injection (Most Common)

cpp
class UserService {
    IDatabase* db; // depends on abstraction
public:
    UserService(IDatabase* d) : db(d) {} // dependency injected via constructor
    void saveUser(string name) { db->save(name); }
};

MySQLDatabase mysql;
UserService service1(&mysql); service1.saveUser("Alice"); // MySQL

MongoDB mongo;
UserService service2(&mongo); service2.saveUser("Bob"); // MongoDB

MockDatabase mock;
UserService service3(&mock); service3.saveUser("Test"); // Mock (testing)

Best for required dependencies — object can't work without them.

Type 2: Setter Injection

cpp
class UserService {
    IDatabase* db = nullptr;
public:
    void setDatabase(IDatabase* d) { db = d; } // inject via setter
    void saveUser(string name) {
        if (!db) { cout << "No database set!" << endl; return; }
        db->save(name);
    }
};

UserService service;
service.setDatabase(&mysql); service.saveUser("Alice"); // MySQL
service.setDatabase(&mongo); service.saveUser("Bob");    // swap to MongoDB!

Best for optional dependencies — can be changed at runtime.

Type 3: Interface Injection

cpp
class IDatabaseConsumer {
public:
    virtual void setDatabase(IDatabase* db) = 0; // forces injection
};

class UserService : public IDatabaseConsumer {
    IDatabase* db = nullptr;
public:
    void setDatabase(IDatabase* d) override { db = d; }
    void saveUser(string name) { db->save(name); }
};

Less common — used when injection must be enforced by contract.

DI Makes Testing Easy

cpp
class MockDatabase : public IDatabase {
public:
    vector<string> saved;
    void save(string data) override { saved.push_back(data); } // no real DB call
};

void testOrderService() {
    MockDatabase db; MockLogger logger; MockEmailSender emailer;
    OrderService order(&db, &logger, &emailer);
    order.placeOrder("alice@mail.com", "Laptop");
    assert(db.saved[0] == "Laptop"); // verify behavior
    cout << "All tests passed!" << endl;
}

3 Types Comparison

ConstructorSetterInterface
When injectedObject creationAfter creationAfter creation
Required depsBestCan forgetEnforced
Optional depsForcedBestOverhead
Change at runtimeNoYesYes
Most commonYesSometimesRare

Key Points

PointDetail
Core ideaReceive dependencies, don't create them
Depends onAbstractions not concrete classes
Most common typeConstructor injection
Main benefitLoose coupling + easy testing
Relates toDependency Inversion (SOLID D)

One-liner: "Dependency Injection means providing a class its dependencies from outside rather than creating them internally — achieved via constructor, setter, or interface injection — enabling loose coupling, flexibility, and easier testing."