依赖倒置原则(DIP)下:实际案例——数据访问层的依赖注入,IoC容器与DIP
上一讲我们聊了DIP的理论,说白了就是“高层模块不要依赖低层模块,两者都依赖抽象”。但理论归理论,落地才是真功夫。今天我就拿数据访问层这个最常见的场景,带大家看看DIP到底怎么用。
一个真实的数据访问层痛点
我记得几年前接手过一个遗留系统。那代码写得,啧啧……业务逻辑层直接new了一个MySQL连接对象。后来公司要切换到PostgreSQL,你猜怎么着?几乎每个业务类都得改。这就是典型的“高层依赖低层”的后果。
为什么会这样?因为业务层和数据库实现绑死了。我们来看看最原始的写法:
class OrderService {
private:
MySQLDatabase db; // 直接依赖具体实现
public:
void createOrder(Order& order) {
db.connect("localhost", "root", "pass");
db.execute("INSERT INTO orders ...");
}
};
这段代码的问题很明显——OrderService 直接依赖了 MySQLDatabase。哪天你想换成 PostgreSQLDatabase,就得改 OrderService 的代码。这违反了开闭原则,也违反了依赖倒置原则。
引入抽象接口
按照DIP的思路,我们应该让高层和低层都依赖抽象。先定义一个数据库访问接口:
class IDatabase {
public:
virtual ~IDatabase() = default;
virtual void connect(const std::string& host,
const std::string& user,
const std::string& pass) = 0;
virtual void execute(const std::string& sql) = 0;
virtual std::vector<Record> query(const std::string& sql) = 0;
};
然后让具体数据库实现这个接口:
class MySQLDatabase : public IDatabase {
public:
void connect(const std::string& host,
const std::string& user,
const std::string& pass) override {
// MySQL连接逻辑
}
void execute(const std::string& sql) override { /* ... */ }
std::vector<Record> query(const std::string& sql) override { /* ... */ }
};
class PostgreSQLDatabase : public IDatabase {
// 类似实现
};
现在业务层只依赖接口:
class OrderService {
private:
IDatabase* db; // 依赖抽象,不是具体实现
public:
explicit OrderService(IDatabase* database) : db(database) {}
void createOrder(Order& order) {
db->connect("localhost", "root", "pass");
db->execute("INSERT INTO orders ...");
}
};
嗯,这里要注意——构造函数接收的是 IDatabase*,而不是具体类型。这就是依赖注入的雏形。
依赖注入的三种方式
我在项目中用过三种依赖注入方式,各有适用场景:
| 注入方式 | 实现方法 | 适用场景 |
|---|---|---|
| 构造函数注入 | 通过构造函数参数传入依赖 | 依赖在对象生命周期内不变 |
| Setter注入 | 通过setter方法设置依赖 | 依赖可选或可替换 |
| 接口注入 | 通过接口方法传入依赖 | 需要动态切换依赖 |
我个人最常用的是构造函数注入。为什么?因为依赖一旦确定就不该变,构造函数注入能保证对象创建后依赖就绪,不会出现空指针问题。
// 构造函数注入
class OrderService {
private:
std::unique_ptr<IDatabase> db_;
public:
explicit OrderService(std::unique_ptr<IDatabase> db)
: db_(std::move(db)) {}
};
// Setter注入
class OrderService {
private:
IDatabase* db_ = nullptr;
public:
void setDatabase(IDatabase* db) { db_ = db; }
};
IoC容器登场
依赖注入解决了“依赖什么”,但没解决“谁来创建依赖”。如果每个业务类都手动new数据库对象,那还是耦合。这时候就需要IoC容器了。
IoC容器说白了就是一个“对象工厂+生命周期管理器”。它负责创建对象、管理依赖关系、控制生命周期。我曾在项目中写过一个轻量级容器,核心逻辑其实不复杂:
class IoCContainer {
private:
// 存储类型信息到工厂函数的映射
std::unordered_map<std::string, std::function<void*()>> factories_;
public:
template<typename T>
void registerType(const std::string& key, std::function<T*()> factory) {
factories_[key] = [factory]() -> void* {
return static_cast<void*>(factory());
};
}
template<typename T>
T* resolve(const std::string& key) {
auto it = factories_.find(key);
if (it != factories_.end()) {
return static_cast<T*>(it->second());
}
return nullptr;
}
};
使用起来就像这样:
IoCContainer container;
// 注册数据库实现
container.registerType<IDatabase>("mysql", []() -> IDatabase* {
return new MySQLDatabase("localhost", 3306);
});
container.registerType<IDatabase>("postgres", []() -> IDatabase* {
return new PostgreSQLDatabase("localhost", 5432);
});
// 注册业务服务
container.registerType<OrderService>("order", [&container]() -> OrderService* {
auto db = container.resolve<IDatabase>("mysql");
return new OrderService(db);
});
// 使用时
auto service = container.resolve<OrderService>("order");
service->createOrder(myOrder);
核心要点:IoC容器把“创建对象”和“使用对象”分开了。业务层只关心“我要用IDatabase”,不关心“IDatabase是怎么创建的”。这就是控制反转——创建对象的控制权从业务层转移到了容器。
DIP + IoC = 灵活的数据访问层
把DIP和IoC容器结合起来,数据访问层就变得非常灵活。我曾经在一个项目中,数据库从MySQL切换到TiDB,只改了一行配置代码——连重新编译都不需要。
来看一个完整的架构图:
我的经验:刚开始用IoC容器时,容易陷入“万物皆容器”的误区。其实不是所有对象都需要容器管理。值对象、简单工具类直接new就行。我一般只把那些“可能变化”的依赖交给容器——比如数据库、缓存、消息队列这些基础设施。
避坑指南
我曾经在一个项目中踩过几个坑,分享出来给大家参考:
- 过度抽象:不要为了DIP而DIP。如果某个依赖永远不会变(比如std::string),没必要抽象成接口。
- 生命周期管理:IoC容器创建的对象,谁负责销毁?我建议用智能指针配合容器管理,避免内存泄漏。
- 性能开销:每次从容器解析对象都有查找和创建开销。高频调用的对象可以考虑单例模式或对象池。
- 循环依赖:A依赖B,B依赖A,容器会死循环。设计时尽量避免,或者用Setter注入打破循环。
特别注意:IoC容器不是银弹。小项目用容器反而增加复杂度。我一般建议:项目超过10个业务类,或者有3个以上数据源时,才考虑引入容器。
总结一下
DIP在数据访问层的落地,说白了就是三步:
- 定义抽象接口(IDatabase)
- 让高层依赖接口,不依赖具体实现
- 用IoC容器管理依赖的创建和注入
这样做的好处很明显:数据库切换只需改容器配置,业务代码纹丝不动。测试时也可以轻松注入Mock对象,单元测试好写多了。
嗯,DIP这部分就聊到这儿。记住一句话:面向接口编程,而不是面向实现编程。这是C++设计中最值钱的经验之一。
公众号:蓝海资料掘金营,微信deep3321