依赖注入——同一实例多处调用时怎么共享
虚函数解决了「怎么定义接口」,但还有一个问题:如果三个地方都需要用同一个 AuthService 实例,这个实例从哪来?怎么传过去?这就是依赖注入要解决的问题。依赖注入不是一种设计模式,而是「共享同一实例」的四种机制的总称。
一、问题:同一实例需要多处调用
假设有一个日志系统,多个模块都需要用它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class LogSystem { public: void info(const char* msg); void error(const char* msg); };
class AuthService { LogSystem& log_; public: AuthService(LogSystem& log) : log_(log) {} void login() { log_.info("login"); } };
class NetworkClient { LogSystem& log_; public: NetworkClient(LogSystem& log) : log_(log) {} void send() { log_.info("sent"); } };
|
AuthService 和 NetworkClient 都需要 LogSystem。问题是:**LogSystem 的实例从哪来?**
二、四种共享机制
机制 1:全局变量
最简单的做法——把 LogSystem 做成全局变量:
1 2 3 4 5
| LogSystem global_log;
class AuthService { void login() { global_log.info("login"); } };
|
优点:简单直接。
缺点:
- 任何地方都能修改它,没有边界
- 无法替换(测试时不能用 MockLogSystem)
- 隐藏了依赖关系(看不出 AuthService 依赖 LogSystem)
全局变量 = 最弱的共享机制。能用,但没有约束。
机制 2:构造函数传参
把实例通过构造函数传进去:
1 2 3 4 5 6 7 8 9
| class AuthService { LogSystem& log_; public: AuthService(LogSystem& log) : log_(log) {} };
LogSystem log; AuthService auth(log); NetworkClient net(log);
|
优点:依赖显式(看构造函数就知道依赖什么)。
缺点:创建链条长——如果 AuthService 还依赖 HttpClient,HttpClient 还依赖 EventLoop,创建顺序就很复杂。
构造函数传参 = 最显式的共享机制。依赖清晰,但创建链条可能很长。
机制 3:组装者集中创建
由一个「组装者」(main 或 Composition Root)统一创建所有实例,再传给需要的地方:
1 2 3 4 5 6 7 8 9 10 11 12
| int main() { LogSystem log; HttpClient http; AuthService auth(log, http); NetworkClient net(log, http); LoginViewModel vm(auth); LoginView view(vm);
view.show(); }
|
优点:所有依赖关系一目了然;可以轻松替换任何一个实现。
缺点:main 知道太多——它知道所有组件的创建方式和依赖关系。
组装者集中创建 = 依赖注入的标准形式(Composition Root)。
机制 4:子系统内部组装
不让 main 知道太多——每个子系统自己负责内部组件的创建:
1 2 3 4 5 6 7 8 9 10 11
| class AuthService { LogSystem log_; HttpClient http_; LoginParser parser_; public: AuthService() = default; LoginResult login(...) { } };
AuthService auth;
|
优点:外部简单,不需要知道内部组成。
缺点:内部组件无法替换(测试时不能注入 MockLogSystem)。
子系统内部组装 = 最简单的共享机制。外部简单,但牺牲了可替换性。
三、四种机制的对比
1 2 3 4 5 6
| 全局变量 构造函数传参 组装者集中 内部组装 依赖显式? ❌ 隐藏 ✅ 显式 ✅ 显式 ❌ 隐藏 可替换? ❌ ✅ ✅ ❌ 外部复杂度 低 高 高 低 内部复杂度 低 低 低 高 适合场景 快速原型 小型项目 中大型项目 子系统封装
|
四、选择依据
1 2 3 4 5 6 7 8 9 10 11 12 13
| 问自己三个问题:
1. 这个实例会被多处调用吗? → 不会 → 不需要注入,直接创建 → 会 → 继续
2. 需要替换实现吗(测试/多平台)? → 不需要 → 子系统内部组装 → 需要 → 继续
3. 组件多吗(超过 5 个)? → 不多 → 构造函数传参 → 多 → 组装者集中创建
|
五、依赖注入和虚函数的关系
两条线交叉的地方:
1 2 3 4 5 6 7
| 虚函数定义了:怎么定义接口(ISearcher) 依赖注入解决了:怎么传递实例(ISearcher&)
两者配合: 1. 定义接口:class ISearcher { virtual find() = 0; } 2. 创建实现:FileSearcher fs("file.bin"); 3. 注入实例:do_search(fs, pattern); // 通过参数传入
|
1 2 3
| 没有虚函数:接口固定,不需要注入(直接用具体类) 有虚函数但没注入:接口可替换,但实例传递困难 有虚函数 + 有注入:接口可替换 + 实例可共享 = 完整的可扩展架构
|
六、框架化组件被调用 → 依赖注入必然出现
核心结论(已用代码验证):框架化组件一旦被其他组件调用,就必然出现依赖注入。
为什么?框架化组件的三个特征凑在一起:
1 2 3 4 5 6 7 8 9
| 框架化组件(LogSystem / EventLoop / NetworkClient): → 有生命周期(start/stop) → 有状态(缓冲、事件列表、连接表) → 全程序通常只有一份实例(共享)
被调用的场景: ProcessService 要 log.info(...) NetworkService 也要 log.info(...) → 两个组件都要「同一个 LogSystem 实例」
|
实例从哪来?三个选项:
1 2 3
| ❌ 全局变量:太弱(不可替换、依赖隐藏) ❌ 每个组件内部创建:生命周期被复制、状态不共享 ✅ 依赖注入:main 创建一份,构造函数传进去
|
所以链条是:
1 2 3 4
| 框架化 + 被多处调用 → 必须共享同一实例 → 实例必须从外部传入 → 依赖注入
|
依赖注入的本质:解决「框架化组件的唯一实例怎么被多个调用方共享」。
对照:接口化组件不需要注入:
1 2 3 4 5 6
| 接口化组件(file_read、StringUtil::trim): → 无状态 → 即调即用 → 没有「实例」概念
调用:直接调用,没有实例可传 → 不需要注入
|
七、总结
1 2 3 4 5 6 7 8 9 10 11 12
| 依赖注入解决的问题:同一实例多处调用时怎么共享
四种机制: 1. 全局变量 → 最简单,最弱 2. 构造函数传参 → 最显式 3. 组装者集中 → 中大型项目标配 4. 子系统内部 → 最简单但不可替换
和虚函数的关系: 虚函数 = 怎么定义接口 依赖注入 = 怎么传递实例 两者配合 = 完整的可扩展架构
|
两条线到这里讲完:接口形态演化决定了「用函数还是用类」,依赖注入决定了「实例怎么共享」。它们共同构成了组件对外暴露能力的完整机制。