訂單在草稿→已付款→出貨→完成間流轉,行為各不同,怎麼設計?

Medium★★★★Studied

FromDesign Patterns (GoF)大話設計模式

Concepts practiced🔁 狀態 (State)🔁 策略 (Strategy)📏 開放封閉原則 (OCP)

Convention: read the requirements & constraints first, sketch your own design, then expand the reference to compare.

📚 From the Books

訂單在草稿→已付款→出貨→完成間流轉,行為各不同,怎麼設計?ProblemMedium設計情境

需求:一張訂單會在「草稿 → 已付款 → 出貨中 → 完成 / 取消」之間流轉。每個狀態能做的操作不同(草稿能改、已付款不能改能退款、完成後只能查),而且每個動作都要檢查「現在這個狀態允不允許」。用一堆 if (status == ...) 很快就失控。怎麼設計?

🎯 Scenario

// 反例:每個操作都散落一組狀態檢查,狀態一多就爆炸
class Order(var status: String) {
    fun pay() {
        if (status != "draft") error("can't pay in $status")
        status = "paid"
    }
    fun ship() {
        if (status != "paid") error("can't ship in $status")   // 同樣的分支到處複製
        status = "shipped"
    }
    fun cancel() {
        if (status == "shipped" || status == "done") error("too late")
        status = "cancelled"
    }
}

需求關鍵字:物件行為隨『內部狀態』改變、狀態間有明確轉移規則、且是狀態自己決定下一步。這是 State 的主場。

🧠 Intuition

TIP

先別看下面。注意這裡切換的不是「演算法」而是「狀態」,而且是狀態自己決定能不能轉、轉去哪。把「每個狀態允許什麼、之後變成什麼」封裝進各自的狀態物件,Order 只把操作委派給當前狀態。

⚖️ Reference Design

主解法 — State:一狀態一物件,轉移封裝在狀態內
interface OrderState {
    fun pay(o: Order): OrderState = error("can't pay in ${this::class.simpleName}")
    fun ship(o: Order): OrderState = error("can't ship in ${this::class.simpleName}")
    fun cancel(o: Order): OrderState = error("can't cancel now")
}

object Draft : OrderState {
    override fun pay(o: Order) = Paid            // 狀態自己決定下一個狀態
    override fun cancel(o: Order) = Cancelled
}
object Paid : OrderState {
    override fun ship(o: Order) = Shipped
    override fun cancel(o: Order) = Refunding
}
object Shipped : OrderState { /* 只允許收貨 → Done */ }

class Order(var state: OrderState = Draft) {
    fun pay()  { state = state.pay(this) }       // Order 只委派,不含分支
    fun ship() { state = state.ship(this) }
}
  • 每個狀態把「允許哪些操作、之後轉到哪」封裝在自己身上;Order 不再有 if (status == ...)
  • 加一個新狀態 = 新增一個 OrderState 實作,不必回頭改所有操作(OCP)。
  • 介面用預設實作統一「此狀態不允許此操作」的行為,非法轉移集中一處。
State vs Strategy —— 結構同形,差在誰決定切換
  • 同構:兩者都是「介面 + 多實作 + 委派」。
  • 差別Strategy外部選定一個演算法、存活期通常不自換;State 是狀態物件自己決定下一個狀態、行為隨內部狀態流轉。
  • 若你的「狀態」其實只是「外部挑的一種計算方式、彼此不會互相轉移」——那要的是 Strategy,不是 State。
邊界與誤用
  • 狀態超多、轉移是資料 → 若轉移表龐大且規則化,考慮用狀態轉移表(map 驅動)而非一堆類別;State 適合「每狀態行為差異大」的情況。
  • 只是把 switch 搬家 → 若選狀態的地方還是一個巨大 when(status) 在 new 狀態物件,味道只是搬到工廠;用註冊表/查表對應。(見重構大 switch
  • 狀態物件亂存業務資料 → 狀態物件最好無狀態(可做 object 單例),把資料留在 context(Order)上。

🔑 Takeaways

✍️ My Notes

No notes yet — jot your takeaways or Q&A here.

📖 Further Reading

🔗 Dive Deeper