| name | pragmatic-dependency-injection |
| description | 在不提前过度抽象的前提下应用依赖注入与组合根设计。用于审查或重构对象创建边界、构造函数注入、类内部硬编码协作者、业务编排类偷偷创建服务、可测试性入口、C++ 具体类型 DI、composition root、constructor injection、concrete-type DI,以及关于“统一创建 + 依赖注入”“组合根”“控制反转”“依赖倒置”“避免过度抽象”“是否该抽接口/工厂/DI 容器”的问题。 |
务实依赖注入
核心思想
把对象装配放到系统边界,让业务编排类显式接收协作者;先用具体类型依赖注入,等真实变化压力出现后再抽接口。
这不是“所有类都要 DI”,也不是“DI 必须配接口和容器”。优先做最小但有架构收益的一步:把可替换、有生命周期、有外部资源或有测试压力的协作者,从类内部创建改为构造函数注入。
默认策略
先分清两个角色:
- 组合根:创建对象、选择具体实现、配置参数、管理生命周期、连接对象图。常见位置是
main.cpp、CLI 入口、应用启动模块、测试 fixture。
- 业务编排类:接收协作者,表达业务流程,避免在内部偷偷创建可替换服务。
优先采用这种形状:
wxm::ThreadPool pool(threadCount, threadCount * 4, false, 1000);
FileWalker walker;
Hasher hasher;
DuplicateFinder finder(pool, walker, hasher);
避免让编排类同时负责创建协作者:
class DuplicateFinder {
FileWalker fileWalker_;
Hasher hasher_;
};
判断表
| 问题 | 倾向 |
|---|
| 协作者是否访问文件、网络、线程、时间、随机数、数据库、缓存或昂贵资源? | 注入 |
| 协作者的配置、算法或实现选择是否属于外部策略? | 注入 |
| 测试时是否希望换成内存实现、假对象或更可控的实例? | 注入 |
| 这个对象是否只是函数内部的临时数据结构、值对象或算法细节? | 内部创建 |
| 目前是否只有一个真实实现,而且测试不需要假实现? | 注入具体类型,不急着抽接口 |
| 是否已经有多个实现、跨包稳定边界或真实 mock/fake 压力? | 考虑窄接口 |
重构流程
-
识别编排类。
- 找到负责串起流程的类,而不是只做一个底层算法的类。
- 列出它在成员变量或方法内部创建的协作者。
- 标记 I/O、并发、外部资源、可配置算法和昂贵初始化。
-
决定注入边界。
- 把生命周期、配置、实现选择或测试替换权属于外部的对象移到组合根。
- 保留纯局部细节:临时容器、小型转换器、无策略的 helper、不可独立替换的实现细节。
- 不机械注入所有对象,只暴露真正的架构依赖。
-
先注入具体类型。
- 构造函数接收具体协作者,优先用引用或值表达所有权。
- 必需且由外部持有的服务用引用;只读服务用
const 引用;小型不可变配置用值。
- 同步更新组合根和所有调用点,让构建结果反映新的依赖图。
-
延迟接口化。
- 出现多个真实实现、需要内存 fake、依赖太慢或不可控、跨模块边界需要稳定时,再抽接口。
- 接口从当前使用方式中长出来,只包含编排类真正需要的方法。
- 不为了“看起来像架构”新增
IThing、工厂、注册表、服务定位器或 DI 容器。
-
验证。
- 跑被改模块的 focused tests。
- 构造函数签名影响公共调用点时,跑相关 build 或更大范围测试。
- 检查引用生命周期:组合根里的对象必须活得比接收者久。
C++ 基准写法
必需协作者由外部持有时,用这个形状作为起点:
class DuplicateFinder {
public:
DuplicateFinder(wxm::ThreadPool& pool,
const FileWalker& fileWalker,
const Hasher& hasher);
private:
wxm::ThreadPool& pool_;
const FileWalker& fileWalker_;
const Hasher& hasher_;
};
不要把临时对象传给会保存引用的构造函数。若协作者便宜、不可变、值语义清晰,优先按值保存。若协作者可选,显式建模为指针、std::optional 或更合适的类型,不要伪造“可空引用”。
设计护栏
不要把依赖注入当成框架问题。没有容器也可以有清晰的组合根。
不要把依赖倒置等同于立刻抽纯虚接口。具体类型 DI 已经能暴露对象边界、改善生命周期管理,并为后续接口化留下低成本路径。
不要把编排类改成万能参数收集器。若构造函数开始膨胀,优先检查类职责是否过大,或是否需要把一组稳定协作者组合成更高层服务。
不要为了测试把生产设计扭成全 mock。先问测试真正需要控制什么;能用真实轻量依赖时,不必制造假对象。
不要使用全局单例或服务定位器来“省构造参数”。这会把显式依赖重新藏回环境里。
输出要求
分析类请求要说明:
- 哪些对象应该移到组合根;
- 哪些对象应该保留内部创建;
- 现在是否只需要具体类型注入;
- 什么信号会触发未来接口化;
- 当前改动的生命周期风险。
实现类请求要说明:
- 构造函数和成员变量如何变化;
- 组合根、测试 fixture 或调用点如何变化;
- 跑了哪些验证命令;
- 是否刻意没有引入接口、工厂或容器,以及理由。