我把 300 行 if-else 拆成狀態機,然後後悔了一半
Programming·2026年6月27日·4 分鐘閱讀

我把 300 行 if-else 拆成狀態機,然後後悔了一半

L
Leo Wu

後端工程師,主力 Go,寫過好的抽象也寫過自我感動的抽象,還在學怎麼分辨這兩者。

這篇沒有什麼驚心動魄的線上事故,就是一個關於「過度設計」的自我檢討。我把一段醜到不行的訂單狀態判斷——大概三百行的巢狀 if-else——重構成了一台漂亮的狀態機。同事都說好看。半年後我回頭看,一半覺得值得,一半覺得我當初想太多。這篇講我為什麼重構、為什麼後悔了一半,以及我現在怎麼判斷「這個東西到底該不該上狀態機」。

一開始那段程式碼有多糟

我把 300 行 if-else 拆成狀態機,然後後悔了一半:本文架構

訂單有十幾種狀態:待付款、已付款、備貨中、已出貨、已完成、退款中、已退款、取消……然後每個「事件」(付款成功、使用者取消、倉庫出貨、金流退款回調)都可能讓訂單從一個狀態跳到另一個。

原本的寫法就是一大坨:進來一個事件,先 if 判斷現在是什麼狀態,再 if 判斷這個事件合不合法,合法就改狀態、做副作用。巢狀三四層,三百多行,改一個規則要看老半天,深怕漏掉某個分支。它會動,但沒人敢碰。

我受不了,決定重構成狀態機。

重構成狀態機之後,確實變好的地方

我把它拆成一張明確的「狀態轉移表」:目前狀態 + 事件 → 下一個狀態 + 要做的動作。非法的組合直接查表就知道,不用再靠一層層 if 去擋。

有幾個好處是真的:

  • 非法轉移被擋在門口。以前「已完成的訂單又收到付款成功事件」這種怪事要靠人腦記得去防,現在表裡沒這個組合,直接被拒,很乾淨。
  • 看得懂全貌。整個訂單生命週期攤在一張表上,新人來看轉移表就懂業務流程,不用讀三百行 if。
  • 好測試。每一條合法/非法轉移都能寫成一個獨立的測試案例,覆蓋率很好衝。

到這裡都很美好。

然後我後悔的那一半

問題出在,我不只做了「狀態轉移表」,我還很興奮地把它做成了一個通用的、可設定的狀態機框架——想說以後別的模組(例如退款流程、審核流程)也能複用。

這一步是多的。

  • 抽象的代價超過收益。為了通用,我加了一堆設定、註冊機制、泛型。結果別的模組根本沒有複雜到需要狀態機,硬要套反而更難懂。那個「以後會複用」的未來,半年了沒有來。
  • 除錯變難了。以前 if-else 雖然醜,但你單步 debug 一路跟下去,邏輯是直的。變成通用框架後,實際的轉移邏輯藏在框架的間接呼叫裡,堆疊變深,新人要理解「這個事件到底怎麼被處理的」反而要先搞懂我的框架。
  • 我把簡單問題包裝成了需要文件的問題。原本大家對訂單流程的心智模型很直接,現在多了一層「你要先懂這個狀態機框架怎麼用」。

換句話說,狀態轉移表是對的,通用狀態機框架是我自我感動

我現在的判斷標準

這件事之後,我對「要不要上某個模式/抽象」有了比較務實的準則:

  • 狀態數 × 事件數大到人腦記不住、非法組合會出事,就值得把邏輯變成明確的轉移表。訂單這種十幾個狀態的,值得。
  • 先做「這個具體問題的轉移表」就好,不要順手做「通用框架」。通用化的成本是實實在在的,收益卻是「假設未來會有第二個使用者」——而那個使用者常常不會出現。
  • YAGNI 不是叫你不要抽象,是叫你不要為了還不存在的需求抽象。 等真的出現第二個、第三個相似的需求,那時候再把共通的部分抽出來,你反而更知道該抽什麼。

如果重來我會怎麼做

我會保留那張訂單狀態轉移表——那部分我到現在都覺得對。但我會把它就寫成訂單模組裡一段直白的、專屬於訂單的程式碼,不搞泛型、不搞註冊、不搞「框架」。醜一點的重複,好過早一步的錯誤抽象。

Sandi Metz 有句話我很認同,大意是:重複遠比錯誤的抽象便宜。當年的我,用一個看起來很聰明的抽象,換掉了一段雖然醜但很誠實的重複,結果是聰明反被聰明誤。

小結

重構不是越抽象越好,「好看」也不等於「好維護」。我那次重構,對的部分是把混亂的條件判斷收斂成一張清楚的表,錯的部分是為了想像中的複用做了過度的通用化。現在只要我發現自己在寫「這個以後應該可以複用」的框架,我都會先停一下,問自己:現在到底有幾個使用者?如果答案是一,那我大概又在自我感動了。

#重構#狀態機#軟體設計#過度設計#YAGNI

留言討論

有想法、有不同經驗、或想糾正我?歡迎在下面留言,免註冊,填個暱稱就能留。

相關文章