一、核心原理
1️⃣ 协议 = “行为契约”
-
协议只关心能力(what can be done),而不关心具体实现(how it’s done)
-
当模块 A 依赖模块 B 时,如果模块 A 只依赖协议,而不是具体类型:
- 解耦:模块 A 不依赖模块 B 的具体实现,模块 B 可以自由变化
- 可替换性:模块 A 可以用任何符合协议的类型替换 B
- 可测试性:模块 A 可以用 mock 或 stub 测试 B 的依赖行为
2️⃣ 对比直接依赖具体类型
class UserManager {
var db: Database
init(db: Database) { self.db = db }
}
UserManager紧耦合Database- 想做单元测试 → 必须依赖真实数据库
- 改动
Database可能破坏UserManager
3️⃣ 改为协议依赖
protocol DatabaseType {
func fetchUser(id: Int) -> String
}
class UserManager {
var db: DatabaseType
init(db: DatabaseType) { self.db = db }
}
UserManager只依赖协议- 可以传入真实数据库或 mock 数据库 → 测试简单
Database实现改动不影响UserManager
二、实际测试示例
struct MockDatabase: DatabaseType {
func fetchUser(id: Int) -> String {
return "MockUser(id)"
}
}
let mockDB = MockDatabase()
let userManager = UserManager(db: mockDB)
print(userManager.db.fetchUser(id: 1)) // 输出: MockUser1 ✅ 可预测
- 模块边界处协议 → 可以直接替换依赖
- 无需启动真实数据库 → 单元测试快速、可靠
三、工程级解耦策略
-
模块边界定义协议
- 每个模块只暴露协议,而非具体类或 struct
- 内部实现可随意修改,外部模块不会受到影响
-
依赖倒置原则(DIP) + POP
- 高层模块依赖抽象(协议),低层模块实现协议
- 避免高层依赖低层具体类型 → 降低耦合
-
使用协议扩展提供默认实现
- 提供可复用的默认行为
- conforming 类型可覆盖 → 保持可扩展性
-
测试用 mock / stub
- 协议允许注入测试替身
- 单元测试完全可控
四、可视化理解
[Module A] ---> depends on ---> [Protocol BType] ---> implemented by ---> [Module B]
^
|
[MockB] for tests
- Module A 无需知道 Module B 的具体类型
- 测试时可以注入 MockB → 解耦 + 可预测
- 生产环境可以注入真实 Module B → 行为一致
五、总结口诀
“模块只依赖协议 → 高层解耦低层实现可替换 → 测试简单又安全。”
✅ 关键点:
- 协议定义能力而非实现
- 模块只依赖协议,不依赖具体类型
- 测试可替换协议实现 → mock / stub