在 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 业务场景的逻辑。这种逻辑污染是跨模块调试时的噩梦。
防御式编程策略:如何正确使用?
为了保护模块边界,建议遵循以下规则:
- 仅用于“辅助功能” :默认实现只应处理那些不涉及核心业务逻辑的通用转换(例如:将
Date转为String),而不应涉及业务决策。 - 显式声明:确保所有在
extension中实现的方法,在protocol的主定义中都有声明。这能保证动态分发,确保多态行为符合预期。 - 使用组合而非注入:如果多个模块需要复用一段逻辑,考虑建立一个专门的
Service类或结构体,由实现方显式调用,而不是通过协议扩展“硬塞”给所有类型。
总结
真正的模块边界应该是“确定的”且“显式的”。 Default Implementation 引入了“隐式的”且“可能被静态截断”的行为。当你发现自己在写 extension MyProtocol 时,停下来问问自己: “我是在定义一个通用的接口标准,还是在偷懒逃避显式的依赖注入?”