引言:为什么依赖注入在 C++ 中很重要?
在现代 C++ 开发中,保持类之间的松散耦合是代码灵活性和可测试性的关键。Dependency Injection (DI) 或依赖注入是一种模式,对象从外部获取其依赖项,而不是自己创建它们。这直接实现了 SOLID 中的依赖反转原则(Dependency Inversion Principle)。
没有 DI,类通常与具体实现紧密耦合:
// 糟糕:类自己创建依赖项class ReportGenerator { Database db_; // 与特定数据库紧密耦合public: ReportGenerator() : db_("localhost") {} void generate() { db_.query("SELECT ..."); }};使用 DI,我们获得了灵活性,能够替换实现(例如,用于单元测试),并简化了代码维护。在本文中,我们将探讨在 C++ 中实现 DI 的三种实用方法:通过构造函数、通过接口以及使用简单的容器。
1. 通过构造函数注入(Constructor Injection)
这是最常见且推荐的方法。依赖项作为参数传递给类的构造函数。在 C++ 中,使用智能指针(std::unique_ptr、std::shared_ptr)或引用很方便。
示例:日志记录器和服务
#include <iostream>#include <memory>#include <string>
// 1. 抽象(接口)class ILogger {public: virtual ~ILogger() = default; virtual void log(const std::string& msg) = 0;};
// 2. 具体实现class ConsoleLogger : public ILogger {public: void log(const std::string& msg) override { std::cout << "[LOG]: " << msg << std::endl; }};
// 3. 依赖于 ILogger 的类class UserService { std::shared_ptr<ILogger> logger_; // 依赖项public: // 通过构造函数注入 explicit UserService(std::shared_ptr<ILogger> logger) : logger_(std::move(logger)) {}
void createUser(const std::string& name) { // 业务逻辑... logger_->log("User created: " + name); }};
int main() { auto logger = std::make_shared<ConsoleLogger>(); UserService service(logger); service.createUser("Alice"); return 0;}优点:
- 依赖项在构造函数签名中清晰可见。
- 对象始终处于有效状态(创建后)。
- 易于为测试替换实现(例如,
MockLogger)。
缺点:当依赖项数量较多时,构造函数可能变得臃肿。在这种情况下,容器会有所帮助(参见第 3 节)。
2. 通过接口和多态注入
这是对前一种方法的扩展。我们为所有依赖项定义纯虚接口。客户端代码仅与抽象交互,具体实现则在对象组装阶段(组合根)注入。
示例:存储库和业务逻辑
#include <vector>#include <memory>
// 存储库接口class IUserRepository {public: virtual ~IUserRepository() = default; virtual void add(const std::string& user) = 0; virtual std::vector<std::string> getAll() const = 0;};
// 数据库实现class DatabaseUserRepo : public IUserRepository {public: void add(const std::string& user) override { // SQL 查询 } std::vector<std::string> getAll() const override { return {"Alice", "Bob"}; }};
// 业务逻辑,不了解数据库class UserManager { std::uniq