软件设计

Swift 桥接模式(Bridge Pattern):分离抽象,防止子类爆炸

使用 Swift 开发应用时,总会遇到类不断增加的时刻。

4 分钟阅读
Swift 桥接模式(Bridge Pattern):分离抽象,防止子类爆炸 封面图

使用 Swift 开发应用时,总会遇到类不断增加的时刻。

明明只添加了一个按钮,却要分别支持浅色模式、深色模式、iOS 和 iPadOS;组合越多,类的数量就越容易爆炸。

先说结论。Swift 桥接模式将“做什么(抽象)”和“怎么做(实现)”分离到不同的类层次中,让两个维度通过相加而不是相乘来管理

本文将依次说明子类爆炸为何发生、桥接模式如何避免,以及实际的 Swift 代码。


为什么会发生子类爆炸?

当两个相互独立的变化维度只能通过继承来表达时,就会发生子类爆炸。

来看一个例子。

假设有一个 Remote 类,可以用它控制电视和收音机。

如果还需要“基础遥控器”和“高级遥控器”两种类型呢?

组合会变成这样。

  • 基础遥控器 × 电视
  • 基础遥控器 × 收音机
  • 高级遥控器 × 电视
  • 高级遥控器 × 收音机

这就已经有 4 个了。

设备增加“音箱”后变成 6 个,遥控器增加“语音遥控器”后则变成 9 个。

当有两个维度时,只使用继承会让类的数量按乘法(m×n)增长,而不是按加法(m+n)增长。

这就是子类爆炸。维度越多,维护负担增长得越快。


Swift 桥接模式如何避免这个问题?

核心只有一个。

将两个变化维度分别拆成独立的层次,再通过引用(组合)连接起来。

  • 抽象层:遥控器类型(基础/高级)
  • 实现层:设备类型(电视/收音机)

遥控器不继承设备,而是将设备接口作为属性持有。

首先将实现层定义为协议,只包含设备必须提供的最小操作。

// 实现层:设备必须提供的最小功能
protocol Device {
    var volume: Int { get set }
    func enable()
    func disable()
}

现在,作为抽象层的遥控器只引用 Device。这是注入,而不是继承。

// 抽象层:持有设备并委托给它
class RemoteControl {
    let device: Device          // 桥接连接点
    init(device: Device) { self.device = device }
    func togglePower() { /* device.enable/disable 调用 */ }
}

这个 device 属性就是连接两个层次的桥(Bridge)。

遥控器不继承设备,只持有它
遥控器不继承设备,只持有它

实际扩展后会有什么不同?

这是我亲自扩展代码时感受到的变化。

添加高级遥控器时,只需继承 RemoteControl,在遥控器层次中新增一个类即可。

// 扩展抽象:设备代码一行都不用改
class AdvancedRemote: RemoteControl {
    func mute() {
        var d = device
        d.volume = 0        // 委托给实现层
    }
}

反过来,添加新设备(例如音箱)时,只需遵循 Device 协议。遥控器代码可以原样复用。

两个维度完全独立,因此扩展一侧时不必重新创建另一侧的组合。

用表格比较,差异会很明显。

分类 仅使用继承 桥接模式
类的增长方式 乘法(m×n) 加法(m+n)
3 种遥控器 × 4 种设备 12 个 7 个
添加新设备时 按遥控器类型数量增加 只增加 1 个
运行时替换 困难 通过注入灵活替换

以 3 种遥控器和 4 种设备为例,类的数量从 12 个减少到 7 个。维度越多,差距越明显。

原本按乘法增长的类,通过一座桥就变成按加法增长
原本按乘法增长的类,通过一座桥就变成按加法增长

什么时候该用,什么时候该避免?

桥接模式并不是所有场景的答案。到处滥用反而容易让代码变复杂。

适合使用的情况:

  1. 存在两个或更多相互独立变化的维度
  2. 希望在运行时替换实现(例如真实 API ↔ 模拟对象)
  3. 希望清晰地拆分平台相关和主题相关的实现

可以不使用的情况:

  • 只有一个维度,或组合今后也固定为 2~3 个
  • 结构很简单,却提前拆分层次而造成过度设计

Swift 中协议和依赖注入本来就很自然,因此你可能只是叫不出桥接模式的名字,实际上早就在使用它。


常见问题(Q&A)

问:它和适配器模式有什么不同?

适配器用于事后连接已经创建但彼此不兼容的代码;桥接模式则是在设计阶段就将两个维度分开。

问:一定要使用 protocol 吗?

是的。在 Swift 中,将实现层定义为 protocol 最自然。使用协议而不是抽象类后,值类型(struct)也可以作为实现。

问:SwiftUI 中也能使用吗?

可以。如果视图通过注入接收数据源协议,只负责渲染,这就是桥接结构。

如果发现有两个维度,像这样拆开会轻松很多
如果发现有两个维度,像这样拆开会轻松很多

如果你一直因子类不断增加而感到烦恼,今天只需记住一件事。

当你觉得“这个类有两个变化原因”时,就是拆分两个维度的时机。

先亲手改写一个小例子,你会比想象中更快熟悉。希望你的代码变得更轻量!

延伸阅读