軟體設計

用 Swift 狀態模式逃離 if 陳述式地獄

狀態模式會將各狀態的行為與轉換集中到物件或 enum 中,減少散落各處的條件判斷。本文說明重構 Swift 畫面狀態的順序與策略模式般的結構如何依設計意圖區分。

閱讀 4 分鐘
用 Swift 狀態模式逃離 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區塊;只要漏掉一個,立刻就會變成錯誤。

狀態散落時,錯誤會藏起來;狀態集中後,錯誤就會浮現。

狀態模式正是用來阻止這種「散落」的結構。


在 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)

Q. 只有 3 個狀態,也需要使用模式嗎?

不一定。不過,只要狀態散落在多個方法中,不論數量多少,都建議用 enum 集中管理。

Q. SwiftUI 也能使用嗎?

可以。將狀態維持為@Published var state: ViewState形式,並在視圖中用switch分支處理,就非常適合。

Q. enum 的關聯值變多後,不會變得凌亂嗎?

關聯值超過 3 個時,建議用獨立的 struct 整理,再以case loaded(Result)這樣的形式保存。


一開始可能不習慣,但掌握將狀態集中到物件中的感覺後,你會不想再回到以前的程式碼。先從一個小畫面開始,用 enum 整理吧。你會比想像中更快離開 if 陳述式地獄。加油!

延伸閱讀