你是否曾为了修改一个界面状态,而打开 if 并逐个改十个 if 语句?
如果加载、成功和失败状态全部通过 if isLoading 与 else if hasError 处理,代码很快就会变得难以维护。
今天我会结合实际经验,介绍如何使用 Swift 状态模式,把 if 语句地狱整理成状态对象。
先说结论:状态模式就是将每个状态拆分成独立对象(或 enum),并把状态转换逻辑放进对象内部。这样条件分支就不会散落各处,而是集中在一个地方。
它与结构几乎相同的策略模式,区别在于由谁主导变更。策略模式 vs. 状态模式可以先区分外部选择和内部状态转换。
你将学到什么
- 为什么会出现 if 语句地狱
- 在 Swift 中将状态拆分为对象的两种方法
- 何时使用 enum,何时使用 protocol
- 可直接用于实际项目的重构顺序
为什么会出现 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,因此新增状态时也更放心。
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 会怎样?”这类规则只存在于该状态内部。分支消失后,每个状态只需要了解自己的规则。
实际重构,请按这个顺序进行
试图一次性全部修改,反而会让人害怕。我按照下面的顺序逐步迁移。
- 把分散的
if实际表示的状态列表写在纸上 - 用一个 enum 定义这些状态
- 先将界面更新代码替换为
switch - 用 enum 彻底消除不可能的状态组合,例如同时处于加载和错误状态
- 只将转换规则复杂的部分提升为 protocol
重点是从 enum 开始,只在需要时提升为 protocol。一开始就过度使用 protocol 设计,反而会让代码变得沉重。
常见问题(Q&A)
问:只有 3 个状态,也需要使用模式吗?
不一定。不过,如果状态分散在多个方法中,无论数量多少,都建议用 enum 集中管理。
问:SwiftUI 也可以使用吗?
可以。将状态保持为@Published var state: ViewState形式,并在视图中通过switch进行分支,非常适合这种场景。
问:enum 的关联值变多后,不会变得杂乱吗?
关联值超过 3 个时,建议用单独的 struct 将它们整理起来,并以case loaded(Result)这样的形式保存。
刚开始可能不太习惯,但掌握将状态集中到对象中的方法后,你会不想再回到旧代码。先从一个小界面开始,用 enum 整理起来吧。你会比想象中更快摆脱 if 语句地狱。加油!

