Skip to Content
Course content

143: Dependency Injection in C++

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to show you a piece of code that looks perfectly fine at first glance. It compiles, it runs, and it does exactly what it's supposed to do in production. But the moment you try to write a unit test for it, you'll realize it's actually a nightmare.

#include <iostream>
#include <string>
#include <memory>

class EmailService {
public:
    void sendEmail(const std::string& msg) {
        // In a real app, this connects to an SMTP server and sends a real email
        std::cout < < "Sending real email: " < < msg < < std::endl;
    }
};

class UserNotificationManager {
    std::unique_ptr<EmailService> service;
public:
    UserNotificationManager() {
        // The manager creates its own dependency
        service = std::make_unique<EmailService>();
    }

    void notifyUser(const std::string& message) {
        service->sendEmail("User Notification: " + message);
    }
};

int main() {
    UserNotificationManager manager;
    manager.notifyUser("Your password was changed.");
    return 0;
}

The Hardcoded Dependency Wall

Here is the problem: UserNotificationManager is "tightly coupled" to EmailService. Because the manager instantiates the service inside its own constructor, there is no way to replace that service with anything else.

Imagine you're writing a unit test to ensure notifyUser formats the message correctly. If you run that test 1,000 times a day in a CI/CD pipeline, you're going to send 1,000 actual emails to your test account (or worse, a real customer). You can't just "turn off" the email sending for tests because the manager owns the creation of the service. You're stuck.

Decoupling via Interfaces and Constructor Injection

To fix this, we need to stop the manager from deciding which service it uses. Instead, we'll define an interface—a base class with pure virtual functions—and "inject" the specific implementation from the outside. This is the core of Dependency Injection (DI).

#include <iostream>
#include <string>
#include <memory>

// 1. Define the interface
class IMessageService {
public:
    virtual ~IMessageService() = default;
    virtual void sendMessage(const std::string& msg) = 0;
};

// 2. Real implementation for production
class EmailService : public IMessageService {
public:
    void sendMessage(const std::string& msg) override {
        std::cout < < "Sending real SMTP email: " < < msg < < std::endl;
    }
};

// 3. Mock implementation for testing
class MockMessageService : public IMessageService {
public:
    bool wasMessageSent = false;
    std::string lastMessage;

    void sendMessage(const std::string& msg) override {
        wasMessageSent = true;
        lastMessage = msg;
        std::cout < < "[TEST] Mock captured message: " < < msg < < std::endl;
    }
};

class UserNotificationManager {
    std::unique_ptr<IMessageService> service;
public:
    // Dependency Injection happens here: we pass the dependency in
    UserNotificationManager(std::unique_ptr<IMessageService> svc) 
        : service(std::move(svc)) {}

    void notifyUser(const std::string& message) {
        service->sendMessage("User Notification: " + message);
    }
};

int main() {
    // Production mode: Inject the real service
    auto prodManager = UserNotificationManager(std::make_unique<EmailService>());
    prodManager.notifyUser("Welcome to the app!");

    // Test mode: Inject the mock service
    auto testManager = UserNotificationManager(std::make_unique<MockMessageService>());
    testManager.notifyUser("Test alert!");
    
    return 0;
}

Why this actually solves the problem

By moving the new (or make_unique) call outside of the UserNotificationManager, we've shifted the responsibility of configuration. The manager no longer cares how a message is sent; it only cares that it has something that satisfies the IMessageService contract.

I've found that beginners often ask, "Isn't this just more code for the same result?" Yes, it's more boilerplate. But the trade-off is immense. You can now swap EmailService for SmsService, SlackService, or a MockService without touching a single line of code inside your business logic. Your code becomes a set of Lego bricks rather than a single, solid block of concrete.

  • Constructor Injection: What we did above. Best for dependencies that the class absolutely needs to function.
  • Setter Injection: Providing a setService() method. Useful for optional dependencies that can be changed at runtime.
  • Interface Injection: The dependency provides a method that the client uses to inject itself. (Rarely used in modern C++, as constructor injection is cleaner).



📋 Practical Task

Refactoring a Payment Processor for Mock Testing

You are working on an e-commerce system. Currently, the OrderProcessor class is tightly coupled to a CreditCardGateway class that hits a real API. This makes testing the "Out of Stock" or "Invalid Address" logic impossible without actually charging a credit card.

Your Task:

  1. Create an abstract base class (interface) named IPaymentGateway with a virtual method bool processPayment(double amount).
  2. Refactor the CreditCardGateway to inherit from IPaymentGateway.
  3. Create a MockPaymentGateway that allows you to manually set whether the payment should succeed or fail (using a boolean flag).
  4. Modify OrderProcessor to use Constructor Injection to receive a std::unique_ptr<IPaymentGateway>.
  5. In main(), demonstrate that you can trigger a "Payment Failed" scenario using the mock gateway without ever touching the real credit card logic.

Starter Code Structure:

class CreditCardGateway {
public:
    bool processPayment(double amount) { 
        /* Real API Call */ 
        return true; 
    }
};

class OrderProcessor {
    CreditCardGateway gateway;
public:
    void completeOrder(double total) {
        if (gateway.processPayment(total)) {
            std::cout < < "Order successful!";
        } else {
            std::cout < < "Payment failed!";
        }
    }
};
Rating
0 0

There are no comments for now.

to be the first to leave a comment.