软件设计

Swift 适配器模式(Adapter Pattern):用协议封装旧版 API

适配器模式会将旧版 API 的接口转换为新代码所期待的协议。本文整理了如何在 Swift 中建立转换边界、简化替换与测试,以及它与其他封装模式的区别。

4 分钟阅读
Swift 适配器模式(Adapter Pattern):用协议封装旧版 API 封面图

你是否也遇到过这样的旧版 API 代码:删不掉,原样使用又很难受?

把旧网络模块接入新页面时,这是任何人都会遇到的障碍。

先说结论,最简洁的方案是用协议(interface)封装旧版 API,并在两者之间放置适配器。

适配器模式是在两个不兼容的接口之间插入一个“转换器”。

如果你容易把它与同样封装对象的 Facade、Proxy、Decorator 混淆,可以在 4 种封装模式对比 先按目的区分它们。

今天就按照我亲自经历的流程,整理如何在 Swift 中应用它。


本文你将学到三件事

先为时间紧张的读者总结重点。

  1. 先用协议定义新代码所需要的形式
  2. 创建一个将旧版 API 转换为该协议的适配器类型
  3. 让页面和视图模型只依赖协议,而不是旧版 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))
            }
        }
    }
}

在这里,将回调转换为 asyncwithCheckedThrowingContinuation 发挥了关键作用。

第 3 步:让页面只依赖协议。

视图模型不需要了解 LegacyUserAPI,只要了解 UserRepository 就够了。

这样一来,旧版代码只会出现在一个地方:适配器。

旧版 API 隐藏在适配器后面,页面只看到协议
旧版 API 隐藏在适配器后面,页面只看到协议
页面代码只需要了解这个简洁的协议
页面代码只需要了解这个简洁的协议

使用适配器后有什么不同?(与直接调用比较)

下面用表格比较直接调用和适配器方式。

项目 直接调用旧版 API 用适配器封装
替换 API 时的修改范围 整个页面 一个适配器
单元测试 困难 用 mock 轻松完成
新代码可读性
初始工作量 稍有增加

初始工作量确实会稍微增加。

但我完全不觉得这笔成本不值得。

测试时只要注入一个实现了 UserRepository 的伪对象,就能在没有网络的情况下验证页面逻辑。

即使真实 API 尚未完成,也可以先开始开发。

记住这三栏结构,就等于完成了一半
记住这三栏结构,就等于完成了一半

常见问题(Q&A)

Q. 适配器应该做成 struct 还是 class?

如果没有状态,使用 struct 就足够了。

如果需要持续持有旧版对象,或需要共享引用,就使用 class

Q. 适配器模式和 Facade 模式有什么区别?

适配器的目的是“让接口相互兼容”。

Facade 的目的是“将多个复杂部分以一个简单接口呈现”,因此方向略有不同。

Q. 协议名称应该怎么命名?

不要沿用旧版名称,应从新代码的角度按照所需角色命名。

例如使用 UserRepository,而不是 LegacyUserAPI


不要强行删除旧版代码,先用协议和适配器将它安静地隔离起来。

只要把旧代码限制在一个地方,接下来的重构就会轻松很多。也可以先尝试封装项目中最混乱的那个 API。

延伸阅读