软件设计

用 Swift 状态模式逃离 if 语句地狱

状态模式将各状态的行为和转换集中到对象或 enum 中,减少分散的条件判断。本文介绍重构 Swift 界面状态的步骤与策略模式般的结构如何根据设计意图进行区分。

4 分钟阅读
用 Swift 状态模式逃离 if 语句地狱 封面图

你是否曾为了修改一个界面状态,而打开 if 并逐个改十个 if 语句?

如果加载、成功和失败状态全部通过 if isLoadingelse if hasError 处理,代码很快就会变得难以维护。

今天我会结合实际经验,介绍如何使用 Swift 状态模式,把 if 语句地狱整理成状态对象。

先说结论:状态模式就是将每个状态拆分成独立对象(或 enum),并把状态转换逻辑放进对象内部。这样条件分支就不会散落各处,而是集中在一个地方。

它与结构几乎相同的策略模式,区别在于由谁主导变更。策略模式 vs. 状态模式可以先区分外部选择和内部状态转换。

你将学到什么

  1. 为什么会出现 if 语句地狱
  2. 在 Swift 中将状态拆分为对象的两种方法
  3. 何时使用 enum,何时使用 protocol
  4. 可直接用于实际项目的重构顺序

为什么会出现 if 语句地狱?

一开始只有两三个状态时,if看起来已经够用了。但随着需求增加,状态会膨胀到 5 个、6 个:加载、成功、失败、空页面、重试中……诸如此类。

问题在于,这些状态组合会分散在多个方法中。按钮启用逻辑中会重复if,界面更新中会重复if,网络回调中也会重复if。因此新增一个状态时,必须找到并修改所有if代码块;漏掉任何一个都会立刻变成 bug。

状态分散时,bug 会隐藏;状态集中后,bug 就会暴露。

状态模式正是用来阻止这种“分散”的结构。


在 Swift 中将状态拆分为对象的方法(enum 方式)

最轻量的起点是enum。可以将状态定义为值,并通过关联值保存所需数据。下面是使用 enum 表示界面状态的示例。

enum ViewState {
    case loading
    case loaded(items: [String]) // 成功时附带数据
    case failed(message: String) // 附带失败原因
    case empty
}

现在界面更新只需执行switch一次。借助关联值,不会再出现“明明成功但数据为 nil”这种模糊状态。

switch state {
case .loading:      showSpinner()
case .loaded(let items): render(items)
case .failed(let msg):   showError(msg)
case .empty:        showEmptyView()
}

我发现大多数界面状态用 enum 方式就足够了。switch会在编译时捕获遗漏的 case,因此新增状态时也更放心。

用一个 switch 集中处理界面状态后,代码更容易维护了
用一个 switch 集中处理界面状态后,代码更容易维护了

enum vs. protocol,什么时候该用哪个?

如果各状态的“行为”差异很大,且转换规则复杂,protocol 方式更合适。将每个状态创建为一种类型,并让它自行返回下一个状态。两种方式的区别如下。

分类 enum 方式 protocol 方式
适用场景 状态以数据为中心 每个状态的行为不同
添加状态 添加 case 添加类型
转换逻辑 在外部使用 switch 封装在状态对象内部
学习曲线 略高

protocol 方式的关键在于,由每个对象负责状态转换。

protocol PlayerState {
    func play() -> PlayerState  // 返回下一个状态
    func pause() -> PlayerState
}

struct PlayingState: PlayerState {
    func play() -> PlayerState { self }        // 如果已经在播放则保持不变
    func pause() -> PlayerState { PausedState() } // 转换为暂停状态
}

这样一来,“播放时按下 pause 会怎样?”这类规则只存在于该状态内部。分支消失后,每个状态只需要了解自己的规则。

由状态自行决定下一个状态的 protocol 方式转换
由状态自行决定下一个状态的 protocol 方式转换

实际重构,请按这个顺序进行

试图一次性全部修改,反而会让人害怕。我按照下面的顺序逐步迁移。

  1. 把分散的if实际表示的状态列表写在纸上
  2. 用一个 enum 定义这些状态
  3. 先将界面更新代码替换为switch
  4. 用 enum 彻底消除不可能的状态组合,例如同时处于加载和错误状态
  5. 只将转换规则复杂的部分提升为 protocol

重点是从 enum 开始,只在需要时提升为 protocol。一开始就过度使用 protocol 设计,反而会让代码变得沉重。

先像这样画出状态,再迁移到 enum,会容易很多
先像这样画出状态,再迁移到 enum,会容易很多

常见问题(Q&A)

问:只有 3 个状态,也需要使用模式吗?

不一定。不过,如果状态分散在多个方法中,无论数量多少,都建议用 enum 集中管理。

问:SwiftUI 也可以使用吗?

可以。将状态保持为@Published var state: ViewState形式,并在视图中通过switch进行分支,非常适合这种场景。

问:enum 的关联值变多后,不会变得杂乱吗?

关联值超过 3 个时,建议用单独的 struct 将它们整理起来,并以case loaded(Result)这样的形式保存。


刚开始可能不太习惯,但掌握将状态集中到对象中的方法后,你会不想再回到旧代码。先从一个小界面开始,用 enum 整理起来吧。你会比想象中更快摆脱 if 语句地狱。加油!

延伸阅读