4-21.【协议导向编程】为什么在模块边界处使用协议可以增强测试和解耦?

3 阅读2分钟

一、核心原理

1️⃣ 协议 = “行为契约”

  • 协议只关心能力(what can be done),而不关心具体实现(how it’s done)

  • 当模块 A 依赖模块 B 时,如果模块 A 只依赖协议,而不是具体类型:

    1. 解耦:模块 A 不依赖模块 B 的具体实现,模块 B 可以自由变化
    2. 可替换性:模块 A 可以用任何符合协议的类型替换 B
    3. 可测试性:模块 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 ✅ 可预测
  • 模块边界处协议 → 可以直接替换依赖
  • 无需启动真实数据库 → 单元测试快速、可靠

三、工程级解耦策略

  1. 模块边界定义协议

    • 每个模块只暴露协议,而非具体类或 struct
    • 内部实现可随意修改,外部模块不会受到影响
  2. 依赖倒置原则(DIP) + POP

    • 高层模块依赖抽象(协议),低层模块实现协议
    • 避免高层依赖低层具体类型 → 降低耦合
  3. 使用协议扩展提供默认实现

    • 提供可复用的默认行为
    • conforming 类型可覆盖 → 保持可扩展性
  4. 测试用 mock / stub

    • 协议允许注入测试替身
    • 单元测试完全可控

四、可视化理解

[Module A] ---> depends on ---> [Protocol BType] ---> implemented by ---> [Module B]
                           ^
                           |
                    [MockB] for tests
  • Module A 无需知道 Module B 的具体类型
  • 测试时可以注入 MockB → 解耦 + 可预测
  • 生产环境可以注入真实 Module B → 行为一致

五、总结口诀

“模块只依赖协议 → 高层解耦低层实现可替换 → 测试简单又安全。”

✅ 关键点:

  • 协议定义能力而非实现
  • 模块只依赖协议,不依赖具体类型
  • 测试可替换协议实现 → mock / stub