Cpp best practices - prefer dependency injection to hardwiring resources Page

Item 5: CPP Best Practices - Prefer dependency injection to hardwiring resources



Return to Equivalent Effective Java Item 5, Effective CPP| Effective C++, CPP, CPP Core Guidelines (CG) by Bjarne Stroustrup and Herb Sutter | C++ Core Guidelines (CG) by Bjarne Stroustrup and Herb Sutter, Effective Java

Introduction to Dependency Injection in C++



In C++, dependency injection (DI) is a design pattern that promotes loose coupling between classes by injecting dependencies (such as objects or services) into a class, rather than having the class create or manage these dependencies on its own. This approach contrasts with hardwiring, where resources and dependencies are created or directly managed within the class itself, leading to tightly coupled code that is harder to test, extend, and maintain. By preferring dependency injection over hardwiring resources, you can achieve more modular, testable, and maintainable code.

Advantages of Dependency Injection in C++



Preferring dependency injection over hardwiring resources offers several key advantages:
1. **Improved Testability**: DI allows you to easily replace real implementations with mocks or stubs during testing, making unit tests more isolated and reliable.
2. **Loose Coupling**: DI decouples classes from their dependencies, allowing them to evolve independently. This results in a more flexible and maintainable codebase.
3. **Simplified Configuration Management**: DI frameworks or patterns allow centralized management of dependencies, reducing the complexity of your code and making configuration changes easier.
4. **Better Separation of Concerns**: By separating the creation of dependencies from their usage, you adhere to the single responsibility principle, leading to more focused and maintainable classes.

Example 1: Hardwiring vs. Dependency Injection in a Service Class



### Hardwiring Example

```cpp
class UserService {
private:
DatabaseConnection dbConnection;

public:
UserService() : dbConnection("localhost", "mydb") {} // Hardwiring the dependency

void addUser(const User& user) {
dbConnection.save(user);
}
};
```

In this example, the `UserService` class is responsible for creating its `DatabaseConnection` dependency. This tight coupling makes the `UserService` class harder to test, extend, and maintain.

### Dependency Injection Example

```cpp
class UserService {
private:
DatabaseConnection& dbConnection;

public:
// Injecting the dependency via constructor
UserService(DatabaseConnection& dbConn) : dbConnection(dbConn) {}

void addUser(const User& user) {
dbConnection.save(user);
}
};
```

Here, the `UserService` class receives its `DatabaseConnection` dependency through its constructor. This loose coupling allows for greater flexibility and makes the class easier to test and modify.

Example 2: Constructor Injection vs. Setter Injection



Dependency injection in C++ can be implemented in different ways, with constructor injection and setter injection being the most common methods.

### Constructor Injection (Preferred)

```cpp
class OrderService {
private:
PaymentService& paymentService;

public:
OrderService(PaymentService& paymentSvc) : paymentService(paymentSvc) {}

void processOrder(const Order& order) {
paymentService.processPayment(order);
}
};
```

### Setter Injection

```cpp
class OrderService {
private:
PaymentService* paymentService;

public:
void setPaymentService(PaymentService& paymentSvc) {
paymentService = &paymentSvc;
}

void processOrder(const Order& order) {
if (paymentService) {
paymentService->processPayment(order);
}
}
};
```

Constructor injection is generally preferred over setter injection because it makes dependencies explicit and ensures that the class is never in an invalid state. It also promotes immutability, as the dependencies are typically set only once via the constructor.

Example 3: Using Smart Pointers for Dependency Management



In C++, smart pointers (`std::shared_ptr`, `std::unique_ptr`) can be used to manage dependencies, especially when ownership and lifetime management are important.

### Using `std::shared_ptr` for Dependency Injection

```cpp
class UserService {
private:
std::shared_ptr dbConnection;

public:
// Injecting the dependency using a smart pointer
UserService(std::shared_ptr dbConn) : dbConnection(dbConn) {}

void addUser(const User& user) {
dbConnection->save(user);
}
};
```

In this example, `UserService` receives a `std::shared_ptr` to `DatabaseConnection`, allowing for shared ownership of the dependency. This is particularly useful in complex applications where multiple components might need to share the same resource.

Example 4: Testing with Dependency Injection



One of the main benefits of dependency injection is the ability to test classes more effectively by injecting mock or stub dependencies.

### Testing a Class with Mock Dependencies

```cpp
#include
#include

class MockDatabaseConnection : public DatabaseConnection {
public:
MOCK_METHOD(void, save, (const User&), (override));
};

TEST(UserServiceTest, AddUserCallsSave) {
MockDatabaseConnection mockDbConnection;
UserService userService(mockDbConnection);
User user{"John Doe"};

EXPECT_CALL(mockDbConnection, save(user)).Times(1);

userService.addUser(user);
}
```

In this example, a mock `DatabaseConnection` is injected into the `UserService` for testing purposes. This allows you to test the `UserService` without relying on a real database connection, making your tests faster and more reliable.

Example 5: Dependency Injection in Complex Systems



In large or complex systems, you might use a dependency injection framework or a service locator pattern to manage dependencies more efficiently.

### Using a Simple Service Locator

```cpp
class ServiceLocator {
private:
std::unordered_map> services;

public:
template
void registerService(const std::string& name, std::shared_ptr service) {
services[name] = service;
}

template
std::shared_ptr getService(const std::string& name) {
return std::static_pointer_cast(services.at(name));
}
};

// Usage
int main() {
ServiceLocator locator;
auto dbConn = std::make_shared("localhost", "mydb");
locator.registerService("DB", dbConn);

auto userService = std::make_shared(*locator.getService("DB"));
userService->addUser(User{"John Doe"});

return 0;
}
```

In this example, a simple service locator is used to manage and retrieve dependencies. This approach can be scaled to more complex systems, where dependencies are dynamically registered and resolved at runtime.

When to Prefer Dependency Injection in C++



Dependency injection is particularly useful in the following scenarios:
- **Complex Applications**: In large or complex applications, DI helps manage the interdependencies between classes more effectively.
- **Test-Driven Development (TDD)**: If you follow TDD practices, DI makes it easier to create testable classes by allowing dependencies to be injected as mocks or stubs.
- **Systems Programming**: In systems where resource management and lifetime control are crucial, DI with smart pointers ensures proper ownership and cleanup of dependencies.
- **Modular Architectures**: DI is beneficial in systems designed with modular components, where dependencies need to be loosely coupled and easily interchangeable.

Conclusion



In C++, preferring dependency injection over hardwiring resources is a best practice that leads to more maintainable, testable, and flexible code. By injecting dependencies, you decouple your classes from their dependencies, making it easier to manage and extend your application. This approach aligns well with modern C++ development practices, especially in complex or large-scale systems where flexibility and testability are critical.

Further Reading and References



For more information on dependency injection in C++, consider exploring the following resources:

* https://www.boost.org/doc/libs/release/libs/di/doc/html/index.html
* https://isocpp.org/wiki/faq/dependency-injection
* https://cpppatterns.com/patterns/dependency-injection.html

These resources provide additional insights and best practices for using dependency injection effectively in C++.

Fair Use Sources


Fair Use Sources:
* ddg>CPP Programming
* archive>C Plus Plus for Archive Access for Fair Use Preservation, quoting, paraphrasing, excerpting and/or commenting upon
* The Borg (CBorg)

{{navbar_cplusplus}}

{{navbar_footer}}