你是否曾為了變更一個畫面狀態,而修改十個if陳述式?
如果載入、成功與失敗狀態全部用if isLoading和else if hasError處理,程式碼很快就會難以維護。
今天就以實際經驗為主,介紹如何透過Swift 狀態模式,將 if 陳述式地獄整理成狀態物件。
先說重點,狀態模式就是將各狀態拆分為獨立物件(或 enum),並把狀態轉換邏輯放進該物件中。如此一來,條件分支不會四處散落,而是集中在同一處。
它與結構幾乎相同的策略模式,差別在於由誰主導變更。策略模式 vs. 狀態模式可以先區分外部選擇與內部狀態轉換。
本文你將學到
- 為什麼會出現 if 陳述式地獄
- 在 Swift 中將狀態拆成物件的兩種方法
- 何時該使用 enum 或 protocol
- 可直接應用於實務的重構順序
為什麼會出現 if 陳述式地獄?
一開始只有兩三個狀態時,if看起來就足夠了。但隨著需求增加,狀態會膨脹到 5 個、6 個:載入、成功、失敗、空畫面、重試中……諸如此類。
問題在於這些狀態組合會分散在多個方法中。按鈕啟用邏輯會重複出現if,畫面更新會出現if,網路回呼也會出現if。因此新增一個狀態時,就得找遍並修改所有if區塊;只要漏掉一個,立刻就會變成錯誤。
狀態散落時,錯誤會藏起來;狀態集中後,錯誤就會浮現。
狀態模式正是用來阻止這種「散落」的結構。
在 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)
Q. 只有 3 個狀態,也需要使用模式嗎?
不一定。不過,只要狀態散落在多個方法中,不論數量多少,都建議用 enum 集中管理。
Q. SwiftUI 也能使用嗎?
可以。將狀態維持為@Published var state: ViewState形式,並在視圖中用switch分支處理,就非常適合。
Q. enum 的關聯值變多後,不會變得凌亂嗎?
關聯值超過 3 個時,建議用獨立的 struct 整理,再以case loaded(Result)這樣的形式保存。
一開始可能不習慣,但掌握將狀態集中到物件中的感覺後,你會不想再回到以前的程式碼。先從一個小畫面開始,用 enum 整理吧。你會比想像中更快離開 if 陳述式地獄。加油!

