在開發一個購物系統的時候,其訂單可能有多種狀態,如:待付款、付款完成、取消、過期、失敗,等等。
如果考慮這筆訂單又有貨運的狀態:待發貨、已出貨、配送中、退貨中,諸如此類多樣且複雜的狀態,應該如何管理怎麼好?
我們今天就從購物訂單,來聊聊全端開發常用到的「狀態 State」管理這件事!
在資料庫當中,購物的訂單可以簡單給一個狀態 state 的欄位來管理:
{ "id": 20240929001, "items": ["蘋果x1", "橘子x2"], "state": "待付款" }
每當狀態改變的時候,我們就直接修改這個欄位即可,例如買家已經付款成功了,便可以讀取此訂單並做修改 order.state = "付款完成"
。
然而,通常我們在改變訂單狀態之前,會做許多條件的判斷。像是要取消一筆訂單,一定要在買家付款之前才能做:
function updateOrderState(action, order) {
if (action == "cancel") {
if (order.state != "付款完成") {
order.state = "取消";
} else {
throw new Error("已付款的訂單無法取消");
}
}
}
若是訂單的狀態不多,可能寫幾個條件尚可以把狀態的轉換給處理好,但是一旦狀態多了起來,就會讓條件判斷 if else 變得非常複雜,難以管理。
function updateOrderState(action, order) {
if (action == "cancel") {
if (order.state == "待付款" || order.state == "失敗") {
order.state = "取消";
} else if (order.state == "付款完成") {
throw new Error("已付款的訂單無法取消");
} else if (order.state == "過期") {
order.state = "取消";
}
} else if (action == "pay") {
if (order.state == "待付款") {
order.state = "付款完成";
} else if (order.state == "過期" || order.state == "取消" || order.state == "失敗") {
throw new Error("無法對已過期、已取消或失敗的訂單進行付款");
}
}
// ...
}
上面這段程式碼只實作部分的狀態轉換,並且只有「待付款」「付款完成」「取消」「過期」「失敗」這五種狀態,如果要開發更複雜的訂單管理系統,就算把這些 Code 寫得乾淨點,能拆小 Function 盡量拆,也會顯得難以管理,並且牽一髮而動全身。
這時候我們就需要 Finite State Machine 的幫助了。
所謂 Finite State Machine,取首字母得到簡稱 FSM,中文翻譯為的有限狀態機,是一種表達狀態之間轉換的數學模型。
直接來看個訂單狀態的 FSM:
*訂單狀態的 FSM 範例
我們可以看到圖中有幾種元素:圓圈表達「狀態」、箭頭上的文字表達「轉換」。最基本的 FSM 可以表達出狀態,以及從什麼狀態透過什麼轉換能到達另一個狀態。
對應到上面的 function updateOrderState(action, order)
,這邊的 action 就是可以對照一個「轉換」,而訂單的狀態則是我們事先定義清楚的。
例如在 JavaScript 中可以使用 javascript-state-machine
這個 Library,寫出以下的 FSM:
import StateMachine from 'javascript-state-machine';
const fsm = new StateMachine({
init: '待付款',
transitions: [
{ name: 'pay', from: '待付款', to: '付款完成' },
{ name: 'cancel', from: ['待付款', '過期', '失敗'], to: '取消' },
{ name: 'expire', from: '待付款', to: '過期' },
{ name: 'fail', from: '待付款', to: '失敗' },
{ name: 'recover', from: ['過期', '失敗'], to: '待付款' }
],
});
透過定義 Transitions,將每個 Transition 從什麼狀態(from)轉換到另一個狀態(to),就能把將我們前面寫的一堆 if else 簡化。
接著我們便能呼叫 transition 來改變現在的訂單狀態,例如當訂單為「待付款」時,呼叫 fsm.pay()
就能將 fsm.state
改變成「付款完成」;但是當訂單為「付款完成」時如果呼叫 fsm.cancel()
就無法轉變狀態,因為我們並沒有定義這個 Transition。
除了訂單這種商業邏輯上可以用 FSM,前端的許多元件也常常適用 FSM 的輔助。
例如我們要實作一個按鈕,可以對此 Hover、Click、Disable,此按鈕的狀態就可能會有 Init、Hovered、Clicked、Disabled。
*按鈕的 FSM 範例
從原始的按鈕狀態,如果要點擊就能先 hover,然後再 click;但是每個狀態下都可能轉換成 Disabled,這樣稍嫌複雜的狀態轉換,同樣可以畫出 FSM 的轉換圖,並且依此實作。
只要是透過 FSM 的機制實作,如果我們覺得在 Init 的狀態下可以不被 hover 就直接 click,那也只要圖上的箭頭調整一下即可,但是用 if else 的方式來管理狀態,則會在要調整狀態的時候,難以 Debug 以及全局思考的情形。