一、问题根源:Swift 的派发机制
- 协议方法的派发规则
-
Protocol requirement(协议声明的方法):
- 如果被类实现 → 动态派发(类似虚方法)
- 如果通过协议类型调用 → 动态派发
-
Protocol extension 默认实现:
- 如果没有被类型实现 → 执行扩展里的默认实现
- 通过具体类型调用时,会静态绑定(静态派发)
- 通过协议类型调用时,会调用默认实现(除非类型重写)
换句话说,protocol + extension 默认实现是 静态绑定 + 动态派发混合,很容易隐藏真实调用者。
例子
protocol Repo {
func fetch() -> String
}
extension Repo {
func fetch() -> String { "Default" }
}
class UserRepo: Repo {
func fetch() -> String { "UserRepo" }
}
let repo: Repo = UserRepo()
repo.fetch() // 输出 "UserRepo" ✅
(UserRepo() as Repo).fetch() // 输出 "UserRepo" ✅
但是:
struct LocalRepo: Repo {}
let repo: Repo = LocalRepo()
repo.fetch() // 输出 "Default" ⚠️
- 默认实现被静态绑定 → Module B 依赖 Module A 协议,但调用的是 Module A 的默认实现
- Module B 实际依赖 Module A 的实现而非接口
二、依赖方向问题
假设:
Module A (定义协议 + default impl)
Module B (依赖协议)
1️⃣ 理想情况(正确的模块边界)
- Module B 只依赖协议
- Module A 提供具体实现
- Module B 调用协议方法 → 通过注入的实例实现
Module B → depends on Repo protocol → inject UserRepo / MockRepo
- 依赖方向:单向
- Module B 不知道 Module A 实现细节
- UI / State / Reducer 可测试可替换
2️⃣ 使用 protocol + default implementation 问题
- Module A 写了协议扩展默认实现
- Module B 依赖协议类型调用
- Module B 可能无意中使用了 Module A 的默认实现
- Module B 不再独立,可被 Module A 的内部逻辑污染 → 反向依赖
Module B → Repo protocol → 调用默认实现 → 依赖 Module A 内部逻辑
-
结果:
- 隐式耦合
- 测试困难(想 mock 需要额外措施)
- 隐藏真实调用者 → 难调试
三、工程直觉
| 使用场景 | 风险 | 建议做法 |
|---|---|---|
| 协议扩展提供默认实现 | 默认实现可能跨模块被依赖 → 破坏依赖方向 | 避免跨模块提供默认实现;只在同模块内部使用 |
| Mock / 测试 | 默认实现会被 unintentionally 调用 | 测试用 mock 明确注入,不依赖默认实现 |
| 高级组合 / Trait | 默认实现混合静态/动态派发 → 不可预测 | 用纯协议 + 注入实现,保持单向依赖 |
四、总结一句话
protocol + default implementation 的危险在于:它让调用者可能隐式依赖提供默认实现的模块,从而破坏单向依赖和模块边界,而不仅仅是语法糖问题。
- 核心点 = 依赖方向被隐藏 → 逻辑跨越模块 → 模块边界失效
- 安全策略 = 协议只声明接口,跨模块不提供默认实现 → 注入具体实现