軟體設計

Strategy 模式 vs State 模式,結構一樣到底差在哪裡?

Strategy 模式從外部選擇演算法,State 模式則由目前狀態主導內部轉換。這兩種具有相同類別結構的行為模式,應透過意圖與變更主體加以區分。

閱讀 4 分鐘
Strategy 模式 vs State 模式,結構一樣到底差在哪裡? 封面圖

學習設計模式時,幾乎一定會遇到一個卡關的地方。

那就是 Strategy 模式和 State 模式。

把 UML(Unified Modeling Language,統一塑模語言)圖並排比較,會發現它們幾乎像複製貼上一樣。放一個介面、建立多個實作,再由 context 持有它們。

你可能會想:「這不就只是名稱不同嗎?」

先說結論。區分兩種模式的關鍵不是結構,而是意圖。Strategy 從外部替換演算法;State 則由內部的狀態自行移動到下一個狀態。

抓住這一句讀下去,就不會再搞混了。

使用狀態物件移除實際條件判斷的實作,會在 Swift State 模式重構中逐步說明。

為什麼結構會如此相似?

兩者都是 GoF(Gang of Four)設計模式書中的行為模式。

兩者使用相同的材料:共用介面、實作該介面的多個類別,以及持有它們的 context。

所以只看程式碼,真的很難分辨。

Strategy 模式寫成程式碼會像這樣。

protocol PaymentStrategy {
    func pay(_ amount: Int)
}

struct CardPayment: PaymentStrategy {
    func pay(_ amount: Int) { print("使用卡片 \(amount)韓元付款") }
}

struct KakaoPay: PaymentStrategy {
    func pay(_ amount: Int) { print("使用 Kakao Pay \(amount)韓元付款") }
}

final class Checkout {
    var strategy: PaymentStrategy
    init(strategy: PaymentStrategy) { self.strategy = strategy }
    func pay(_ amount: Int) { strategy.pay(amount) }
}

let checkout = Checkout(strategy: CardPayment())
checkout.pay(10000)
checkout.strategy = KakaoPay()  // 從外部替換
checkout.pay(5000)
// 輸出:
// 使用卡片 10000韓元付款
// 使用 Kakao Pay 5000韓元付款

這裡要注意的是替換 strategy 的主體,也就是外部的 client 程式碼。

把付款方式從卡片改成 Kakao Pay 的,是開發者,也就是外部的某個角色。

Strategy 本身完全不知道下一個會是什麼,也不在意。


那麼 State 模式有什麼不同?

State 模式由狀態本身決定狀態轉換。

想想紅綠燈。紅燈知道下一個是綠燈,綠燈也知道下一個是黃燈。

也就是說,前往下一個狀態的規則都放在各個狀態裡。

狀態自行前往下一個狀態的流程,這就是 State 的本質
狀態自行前往下一個狀態的流程,這就是 State 的本質
protocol TrafficState {
    func next(_ light: TrafficLight)
}

final class Red: TrafficState {
    func next(_ light: TrafficLight) {
        print("紅 → 綠")
        light.state = Green()  // 狀態自行指定下一個狀態
    }
}

final class Green: TrafficState {
    func next(_ light: TrafficLight) {
        print("綠 → 黃")
        light.state = Yellow()
    }
}

final class Yellow: TrafficState {
    func next(_ light: TrafficLight) {
        print("黃 → 紅")
        light.state = Red()
    }
}

final class TrafficLight {
    var state: TrafficState = Red()
    func change() { state.next(self) }
}

let light = TrafficLight()
light.change()
light.change()
light.change()
// 輸出:
// 紅 → 綠
// 綠 → 黃
// 黃 → 紅

看出差異了嗎?

在 Strategy 模式中,我們像 checkout.strategy = KakaoPay()一樣從外部直接替換。

在 State 模式中,light.state = Green()由狀態類別本身執行。client 只會呼叫 change()。

這正是核心所在。

Strategy 從外部替換,State 則在內部自行轉換。


用三點整理決定性的差異

只用文字說明容易再次混淆,所以整理成表格。

分類 Strategy 模式 State 模式
意圖 替換演算法 根據狀態改變行為
轉換主體 外部 client 狀態物件本身
物件間關係 彼此不知道(獨立) 彼此知道並互相參照
生命週期 通常決定一次後維持 執行期間持續變更

請特別記住第二列和第三列。

Strategy 彼此不知道對方的存在。卡片付款不需要知道 Kakao Pay。

相反地,State 必須互相知道。因為紅燈會直接建立並傳遞綠燈物件。

這種「彼此知道還是不知道」,是從程式碼區分兩種模式最可靠的線索。

把兩段程式碼並排比較,就能看出「誰在變更」
把兩段程式碼並排比較,就能看出「誰在變更」

什麼時候該用,什麼時候該避免?

整理我在實務上的判斷標準如下。

情境 判斷
只有執行相同工作的方法不同時(排序、付款、壓縮) Strategy 模式
物件會依情境採取不同行為,而且情境會改變時 State 模式
轉換規則變成複雜的 if-else 大塊時 用 State 模式處理
簡單到只要傳入一個函式時 兩者都過度設計,使用 closure

也有需要注意的地方。

如果只有 2~3 個狀態且轉換也很簡單,很多時候不必硬套模式,使用 enum 和 switch 就夠了。

模式是管理複雜度的工具,不是替簡單程式碼披上形式的裝飾。

面試時通常會這樣問

Q. Strategy 模式和 State 模式的結構相同,要如何區分?

結構幾乎相同,但意圖不同。Strategy 的目的是從外部替換演算法,State 的目的是依內部狀態改變行為。關鍵差異在於轉換主體:Strategy 由 client 推進,State 則由狀態物件本身前往下一個狀態。

Q. State 模式中狀態彼此參照,這不會造成問題嗎?

狀態之間確實會產生耦合。因此,也可以把轉換邏輯從狀態中移出,放到 context 或獨立的轉換表中,以降低耦合。當狀態變多且轉換互相糾纏時,也可以使用狀態機函式庫。


即使圖看起來一模一樣,也不用害怕。只要問「誰負責轉換?」答案就會立刻出現。

從外部替換就是 Strategy,在內部自行前進就是 State。只要記住這句話,今天這篇文章就達成目的了。下次再帶來另一個容易混淆的模式。

延伸閱讀