C++ 中的依赖注入简介
在现代 C++ 开发中,创建灵活且可测试代码的关键原则之一是 依赖注入(DI)。这种设计模式有助于降低系统组件之间的耦合度,使代码更加模块化、可重用,且更容易测试。
在本文中,我们将探讨什么是 C++ 中的依赖注入、它解决了哪些问题、研究在实际中实现它的各种方法,并提供代码示例来帮助你将该方法融入你的项目中。
直接依赖的问题
在讨论解决方案之前,我们先来看一个典型问题。假设我们有一个 UserService 类,它使用 Logger 进行日志记录:
class Logger {public: void log(const std::string& message) { std::cout << "[LOG]: " << message << std::endl; }};
class UserService {private: Logger logger; // 硬依赖public: void createUser(const std::string& name) { logger.log("Creating user: " + name); // ... 用户创建逻辑 }};这里,UserService 自行创建了一个 Logger 实例。这带来了几个问题:
- 测试复杂性:在单元测试中无法用模拟对象替换日志记录器。
- 紧耦合:更改
Logger构造函数需要修改所有使用它的类。 - 违反单一职责原则(SRP):该类不仅负责自身逻辑,还负责创建其依赖项。
什么是依赖注入?
依赖注入 是一种模式,其中对象从外部接收其依赖项,而不是自行创建。核心思想是:"不要请求,而是接收。"
在 C++ 中,主要有三种注入依赖项的方式:
- 构造函数注入 — 通过构造函数。
- 设值方法注入 — 通过设值方法。
- 接口注入 — 通过接口。
1. 构造函数注入
这是最常见且推荐的方法。依赖项通过类的构造函数传入:
#include <memory>#include <iostream>
class ILogger {public: virtual ~ILogger() = default; virtual void log(const std::string& message) = 0;};
class ConsoleLogger : public ILogger {public: void log(const std::string& message) override { std::cout << "[CONSOLE]: " << message << std::endl; }};
class FileLogger : public ILogger {public: void log(const std::string& message) override { // 文件写入逻辑 std::cout << "[FILE]: " << message << std::endl; }};
class UserService {private: std::unique_ptr<ILogger> logger_; // 通过接口的依赖public: // 构造函数注入 explicit UserService(std::unique_ptr<ILogger> logger) : logger_(std::move(logger)) {}
void createUser(const std::string& name) { logger_->log("Creating user: " + name); // ... 逻辑 }};
int main() { auto consoleLogger = std::make_unique<ConsoleLogger>(); UserService service(std::move(consoleLogger)); service.createUser("Alice"); return 0;}优点:
- 依赖项在构造函数签名中清晰可见。
- 对象在创建后始终处于有效状态。
- 易于测试——你可以替