12-28.【SwiftUI】Protocol + Default Implementation 为什么会破坏模块边界?

3 阅读3分钟

在 Swift 中,Protocol + Default Implementation(协议+默认实现)是一把双刃剑:它在提供代码复用便利的同时,往往会悄悄侵蚀模块之间的契约(Contract) ,导致逻辑耦合。

它破坏模块边界的原因主要体现在以下四个维度:

1. 静态分发(Static Dispatch)带来的隐形 Bug

这是最著名的技术陷阱。当你在协议扩展中提供了一个默认实现,但在协议声明中没有定义该方法时,Swift 会使用静态分发

  • 破坏点:如果模块 B 调用了模块 A 的协议方法,但由于静态分发,它可能调用的是协议的“通用默认版本”,而不是类中被“重写”的版本。
  • 后果:模块边界本应保证行为的确定性,但默认实现让“到底执行了哪段代码”变得依赖于变量的类型声明(父类还是子类),而不是对象的实际类型。这打破了模块间的多态一致性

2. 逻辑“潜入”:违反了依赖倒置原则

模块边界的核心在于:高层模块定义协议,低层模块实现协议。

  • 破坏点:当你给协议增加默认实现时,你实际上是在协议层(通常是抽象层)引入了具体逻辑
  • 后果:如果这个默认实现依赖了该模块内部的其他私有组件,那么任何引用该协议的外部模块都会被迫背负这些逻辑。这导致协议不再是一个纯粹的“接口”,而变成了一个带状态或带行为的“伪基类”,形成了隐式耦合

3. “不请自来”的行为(Implicit Conformance)

默认实现允许一个类型只需声明 extension MyType: MyProtocol {} 就能获得一整套行为。

  • 破坏点:这种“自动获得”机制模糊了模块的显式功能边界
  • 后果:在大型项目中,你可能并不希望某个类型拥有某些能力(例如:序列化、日志记录),但由于默认实现的存在,这些能力被无声无息地注入了。这使得代码审查变得困难:你无法通过查看类的定义来确定它的所有真实行为边界。

4. 削弱了编译器强制检查

协议的初衷是强制实现者去思考:“我该如何处理这个逻辑?”

  • 破坏点:默认实现给了开发者“偷懒”的机会。
  • 后果:当模块 A 的需求发生变化,协议增加了一个新方法时,如果有了默认实现,模块 B(实现方)的编译器不会报错
  • 风险:模块 B 可能在完全不知情的情况下,运行着模块 A 提供的一个通用的、可能并不适用于模块 B 业务场景的逻辑。这种逻辑污染是跨模块调试时的噩梦。

防御式编程策略:如何正确使用?

为了保护模块边界,建议遵循以下规则:

  1. 仅用于“辅助功能” :默认实现只应处理那些不涉及核心业务逻辑的通用转换(例如:将 Date 转为 String),而不应涉及业务决策。
  2. 显式声明:确保所有在 extension 中实现的方法,在 protocol 的主定义中都有声明。这能保证动态分发,确保多态行为符合预期。
  3. 使用组合而非注入:如果多个模块需要复用一段逻辑,考虑建立一个专门的 Service 类或结构体,由实现方显式调用,而不是通过协议扩展“硬塞”给所有类型。

总结

真正的模块边界应该是“确定的”且“显式的”。 Default Implementation 引入了“隐式的”且“可能被静态截断”的行为。当你发现自己在写 extension MyProtocol 时,停下来问问自己: “我是在定义一个通用的接口标准,还是在偷懒逃避显式的依赖注入?”