软件设计

Swift Visitor 模式:需要双重分派的时机

进行 iOS 开发时,总会遇到需要不断为图形或节点等多种类型的对象添加操作的情况。

4 分钟阅读
Swift Visitor 模式:需要双重分派的时机 封面图

进行 iOS 开发时,总会遇到需要不断为图形或节点等多种类型的对象添加操作的情况。

你通常会用 if let、as?逐个检查类型;每增加一种类型,就要再次修改分支逻辑。

本文通过示例整理 Swift Visitor 模式,以及其核心双重分派究竟何时有必要。

Swift 选择方法时,只会查看接收者(接收消息的对象)的动态类型。

当“是哪种图形”和“是哪种操作”两个维度同时变化时,单次分派就不够了。双重分派通过两次调用,在运行时确定两个类型。


Swift 的方法分派为什么只靠一次还不够?

在 Swift 中调用 shape.draw() 时,会执行符合 shape实际类型的 draw()。

这就是单次分派:只根据接收者类型 ****选择方法。

问题会在操作有多个时出现。

图形包括圆形、矩形和三角形;操作包括绘制、计算面积和导出 JSON。两个维度相乘后,组合会迅速增加。

所以经常会写出这样的代码。

// 每增加一种图形操作,代码就会不断变长 if-else
func export(_ shape: Shape) -> String {
    if let c = shape as? Circle { return drawCircle(c) }
    if let r = shape as? Rect   { return drawRect(r) }
    return "" // 每次新增图形,都要再次修改这里
}

如果新增一个图形,就得打开这个函数,再插入一个分支。

有五个操作,就相当于有五个这样的函数。

当 if-else 堆积到这种程度时,我会把它视为一个信号。
当 if-else 堆积到这种程度时,我会把它视为一个信号。

什么时候需要双重分派?

我的判断标准很简单。

当方法的行为同时依赖两个类型时,就是需要双重分派的时候。

前面的例子正是如此:要执行的代码同时取决于图形类型和操作类型。

Swift 的默认分派只查看接收者类型,因此另一个维度只能通过 as?这类类型检查手动处理。

这种手动分支就是代码坏味道。

双重分派连第二个类型的决定也交给语言的重载机制,开发者无需再手动检查类型。

反过来说,如果只有一个操作,或者类型不会继续增加,就没必要使用这个模式。


如何实现 Swift Visitor 模式?

关键是把调用拆成两次,所以才叫双重分派。

protocol Shape { func accept(_ v: Visitor) -> String }
struct Circle: Shape { func accept(_ v: Visitor) -> String { v.visit(self) } }
struct Rect: Shape   { func accept(_ v: Visitor) -> String { v.visit(self) } }

protocol Visitor {
    func visit(_ c: Circle) -> String   // 图形类型在这里确定
    func visit(_ r: Rect) -> String      // 操作通过重载确定
}

我们来跟着流程看一遍。

第一次调用 shape.accept(v)会确定 图形的实际类型。运行时会选择调用 Circle的 accept,还是 Rect的版本。

第二次调用 v.visit(self)时,self的静态类型已经确定为 Circle或Rect。因此可以正确选择重载的 visit。

两次调用衔接起来,就能准确确定两个类型。

两次调用交汇的地方:双重分派
两次调用交汇的地方:双重分派

想添加新操作时,只需创建一个采用 Visitor 的新结构体即可。现有图形代码完全不用修改。


Visitor 模式要不要用?优缺点对比

没有完美的模式。下面看看它的取舍。

分类 类型分支(as? / switch) Visitor 模式
添加操作 修改各处的分支 添加一个新的 Visitor
添加类型 添加一行分支 需要修改所有 Visitor
代码可读性 分支增多后变复杂 按操作清晰分离
初始编写成本 低 相对较高

如表所示,Visitor 模式在操作经常增加、类型保持稳定时最能发挥作用。

相反,如果类型(图形)持续增加,反而会更费事,因为所有 Visitor 都要修改。

accept 再 visit,只需两次调用
accept 再 visit,只需两次调用

常见问题(Q&A)

Q. Swift 有泛型,但仍然需要 Visitor 吗?

泛型在编译时确定类型时很强大。但当实际类型在运行时混杂在[Shape]数组中时,双重分派仍然有用。

Q. 用 enum 和 switch 也可以吧?

可以。如果类型固定,enum + switch 反而更简洁。操作不断增加时,Visitor 更合适。


遇到同时依赖两个类型的行为时,就是该想起双重分派和 Swift Visitor 模式的时候。

如果现在有 as?分支不断增加的代码,可以参考今天的示例尝试重构,结构会更易维护 🙂

延伸阅读