依赖注入(Dependency Injection, DI)的核心本意是解耦,但在实际操作中,它最容易演变成**“过度设计”或“隐藏的混乱”**。
如果 DI 让你的代码变得难以追踪、或者让单元测试本身变得比业务逻辑还复杂,那么它就已经被滥用了。
1. DI 最容易滥用的三个地方
- 过度抽象(Interface-itis) :为每一个简单的类都定义一个协议(Protocol)。如果你只有一个实现且短期内不会变,这种注入只是增加了跳转代码的物理负担。
- 服务定位器伪装(Service Locator Path) :在构造函数中注入一个巨大的
DependencyContainer或Resolver。这本质上是“全局变量”的变体,因为它隐藏了对象真正的依赖需求。 - 深层传递(Prop Drilling) :为了让第 10 层视图拿到一个
Service,被迫在中间 9 层视图的构造函数里全部添加这个参数,即使它们根本不用它。
2. 构造注入 (Initializer Injection) 的坑
这是最推荐的方式,但也并非完美。
-
构造函数爆炸:当一个对象需要 7、8 个依赖时,构造函数会变得极其臃肿。
- 防御点:这通常是**违反单一职责原则(SRP)**的信号。说明这个类干了太多事,应该拆分,而不是继续增加注入项。
-
样板代码冗余:每次新增依赖都要修改
init、私有属性和赋值语句。- 避坑:在 Swift 中可以利用
struct的自动初始化,或者利用类似 Needle 或 Factory 这种编译时安全的 DI 框架来减少手工劳动。
- 避坑:在 Swift 中可以利用
3. 属性注入 (Property Injection) 的坑
通常表现为 @Inject 宏或在 init 之后手动赋值。
- 状态不确定性:对象在被初始化后、依赖被注入前,处于一个非法状态。如果忘记赋值,调用时会直接 Crash(如果是强拆箱)或无响应。
- 不可变性破坏:你被迫将属性声明为
var。这在并发环境下会带来数据竞争的风险。 - 依赖隐藏:查看
init时看不出依赖,必须通读代码才能知道它需要什么。
4. 环境注入 (Environment Injection) 的坑
这是 SwiftUI 的特色(@EnvironmentObject, @Environment),也是最容易引发生产事故的地方。
-
隐式依赖导致的预览崩溃:
- 坑:如果你在视图里用了
@EnvironmentObject,但在 Preview 或父视图中忘了注入.environmentObject(),App 会直接 Runtime Crash。
- 坑:如果你在视图里用了
-
重绘范围失控:
- 坑:环境对象是全局可见的。如果你把一个高频更新的对象存入环境,所有读取该环境的视图(无论是否用到改变的字段)都会被迫重新渲染。
-
测试性变差:
- 坑:在单元测试中,你必须构建一个完整的
EnvironmentValues结构或视图层级才能测试逻辑。
- 坑:在单元测试中,你必须构建一个完整的
5. 决策矩阵:我该选哪种?
| 注入方式 | 推荐度 | 适用场景 | 核心风险 |
|---|---|---|---|
| 构造注入 | ⭐⭐⭐⭐⭐ | 绝大多数业务逻辑、ViewModel | 构造函数过长 |
| 属性注入 | ⭐⭐ | 循环依赖、UIKit 视图控制器加载 | 忘记注入导致的空指针 |
| 环境注入 | ⭐⭐⭐ | 全局配置、主题、跨多层级的共享状态 | 运行时崩溃、过度渲染 |
总结:防御式 DI 准则
- 显式优于隐式:优先使用构造注入,让依赖关系在编译期就清清楚楚。
- 避免“上帝容器” :不要注入容器,只注入具体的协议。
- 环境注入要兜底:在使用
@Environment时,尽量提供 Default Value,防止运行时因为缺少注入而崩溃。