C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
143: Dependency Injection in C++
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:
- Create an abstract base class (interface) named
IPaymentGatewaywith a virtual methodbool processPayment(double amount). - Refactor the
CreditCardGatewayto inherit fromIPaymentGateway. - Create a
MockPaymentGatewaythat allows you to manually set whether the payment should succeed or fail (using a boolean flag). - Modify
OrderProcessorto use Constructor Injection to receive astd::unique_ptr<IPaymentGateway>. - 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!";
}
}
};There are no comments for now.