你是否也遇到过这样的旧版 API 代码:删不掉,原样使用又很难受?
把旧网络模块接入新页面时,这是任何人都会遇到的障碍。
先说结论,最简洁的方案是用协议(interface)封装旧版 API,并在两者之间放置适配器。
适配器模式是在两个不兼容的接口之间插入一个“转换器”。
如果你容易把它与同样封装对象的 Facade、Proxy、Decorator 混淆,可以在 4 种封装模式对比 先按目的区分它们。
今天就按照我亲自经历的流程,整理如何在 Swift 中应用它。
本文你将学到三件事
先为时间紧张的读者总结重点。
- 先用协议定义新代码所需要的形式
- 创建一个将旧版 API 转换为该协议的适配器类型
- 让页面和视图模型只依赖协议,而不是旧版 API
只要遵守这三点,之后整体替换 API 时需要修改的范围就会大幅缩小。
改成这种结构后,我发现编写测试代码方便多了。
为什么需要 Swift 适配器模式?
旧版 API 通常不是我们想要的样子。
它可能基于回调、参数混乱,或者返回类型含糊不清。
如果新代码被迫迎合旧 API,那么 API 消失的那一天,整个代码库都会受到影响。
所以我们要在中间放置适配器。
假设旧模块是下面这样。
// 难以下手的旧版代码 API (基于回调)
class LegacyUserAPI {
func fetch(id: Int,
done: @escaping (NSDictionary?) -> Void) {
// 旧的网络调用...
}
}
如果把 NSDictionary 原样带到页面层,之后更换这个 API 时,连页面代码也必须全部拆掉重写。
一看就知道不想直接接入。
如何用协议封装旧版 API(3 个步骤)
实际的封装过程比想象中简单。
第 1 步:用协议定义想要的形式。
先写下新代码希望能够“这样调用”的形式。
// 新代码需要的简洁接口
protocol UserRepository {
func user(id: Int) async throws -> User
}
我将回调替换为 async/await,并将 NSDictionary 替换为 User 类型。
第 2 步:让适配器将旧版 API 调整为符合该协议。
把所有复杂的转换都封装在适配器内部。
// 将旧版 API 转换为新协议的适配器
struct LegacyUserAdapter: UserRepository {
let legacy = LegacyUserAPI()
func user(id: Int) async throws -> User {
try await withCheckedThrowingContinuation { cont in
legacy.fetch(id: id) { dict in
cont.resume(returning: User(dict))
}
}
}
}
在这里,将回调转换为 async 的 withCheckedThrowingContinuation 发挥了关键作用。
第 3 步:让页面只依赖协议。
视图模型不需要了解 LegacyUserAPI,只要了解 UserRepository 就够了。
这样一来,旧版代码只会出现在一个地方:适配器。
使用适配器后有什么不同?(与直接调用比较)
下面用表格比较直接调用和适配器方式。
| 项目 | 直接调用旧版 API | 用适配器封装 |
|---|---|---|
| 替换 API 时的修改范围 | 整个页面 | 一个适配器 |
| 单元测试 | 困难 | 用 mock 轻松完成 |
| 新代码可读性 | 低 | 高 |
| 初始工作量 | 少 | 稍有增加 |
初始工作量确实会稍微增加。
但我完全不觉得这笔成本不值得。
测试时只要注入一个实现了 UserRepository 的伪对象,就能在没有网络的情况下验证页面逻辑。
即使真实 API 尚未完成,也可以先开始开发。
常见问题(Q&A)
Q. 适配器应该做成 struct 还是 class?
如果没有状态,使用 struct 就足够了。
如果需要持续持有旧版对象,或需要共享引用,就使用 class。
Q. 适配器模式和 Facade 模式有什么区别?
适配器的目的是“让接口相互兼容”。
Facade 的目的是“将多个复杂部分以一个简单接口呈现”,因此方向略有不同。
Q. 协议名称应该怎么命名?
不要沿用旧版名称,应从新代码的角度按照所需角色命名。
例如使用 UserRepository,而不是 LegacyUserAPI。
不要强行删除旧版代码,先用协议和适配器将它安静地隔离起来。
只要把旧代码限制在一个地方,接下来的重构就会轻松很多。也可以先尝试封装项目中最混乱的那个 API。

