情境:有一個變數A的初始值是 undefined,當經過 http request 後,若是失敗則需要重試,若是重試3次失敗則通知重試失敗錯誤訊息,須 request 成功後才賦予變數 A 的值為 0 或 1。
問題是,變數 A 有三種可能性,應該將其當作狀態嗎?
一個簡單的狀態機範例
先透過 xstate.js 來描述這樣的狀態機,我們可以建立一個狀態機,其中包括初始狀態、重試狀態、成功狀態和失敗狀態。以下是一個簡單的範例:

fetch 狀態機 (Generated by https://stately.ai/viz)
import { createMachine, interpret } from 'xstate';
// 定義狀態機
const fetchMachine = createMachine({
id: 'fetch',
initial: 'idle',
context: {
retries: 0,
A: undefined,
},
states: {
idle: {
on: {
FETCH: 'loading'
}
},
loading: {
invoke: {
src: 'fetchData',
onDone: {
target: 'success',
actions: 'setData'
},
onError: {
target: 'retry',
actions: 'incrementRetries'
}
}
},
retry: {
always: [
{
target: 'failure',
cond: 'maxRetriesReached'
},
{ target: 'loading' }
]
},
success: {
type: 'final'
},
failure: {
type: 'final'
}
}
}, {
actions: {
incrementRetries: (context) => context.retries++,
setData: (context, event) => context.A = event.data
},
guards: {
maxRetriesReached: (context) => context.retries >= 3
}
});
// 模擬 fetchData 函數
const fetchData = () => {
return new Promise((resolve, reject) => {
// 模擬 HTTP request
setTimeout(() => {
// 假設 50% 的機率成功,50% 的機率失敗
Math.random() > 0.5 ? resolve(Math.random() > 0.5 ? 1 : 0) : reject('Failed');
}, 1000);
});
};
// 創建服務
const fetchService = interpret(fetchMachine.withConfig({
services: {
fetchData: fetchData
}
})).onTransition(state => {
console.log(state.value, state.context);
});
// 啟動服務
fetchService.start();
// 發送 FETCH 事件開始請求
fetchService.send('FETCH');
這段代碼建立了一個狀態機,其邏輯如下:
- 初始狀態是
idle
,接收到FETCH
事件後進入loading
狀態。 - 在
loading
狀態中,執行fetchData
請求,請求成功則進入success
狀態,並將變數A
設置為 0 或 1;請求失敗則進入retry
狀態,並增加重試次數。 - 在
retry
狀態中,如果重試次數達到 3 次,則進入failure
狀態,否則返回loading
狀態重新嘗試。 success
和failure
狀態是終結狀態,表示狀態機的運行結束。
如何辨別是狀態還是數據?
從上述例子可以看出變數 A 其實並不適合作為狀態機中的狀態。
在設計狀態機時,辨別是狀態還是數據是一個關鍵問題。以下是一些指導原則,可以幫助你做出這個區分:
狀態的特徵
- 行為驅動: 狀態反映系統在特定時間點上的行為或狀態。不同的狀態導致系統有不同的行為方式。例如,“加載中”、“重試中”、“成功”和“失敗”這些狀態會導致系統採取不同的行為。
- 有限且明確: 狀態的數量通常是有限的,並且在設計時可以明確列出來。每個狀態都有明確的開始和結束條件。
- 可觀察和區分: 每個狀態都是系統中的一個獨立的狀態,應該是可以觀察和區分的。你應該能夠清楚地描述在某個狀態下系統的行為特徵。
數據的特徵
- 信息驅動: 數據是系統運行所需的具體信息或變量。數據用來記錄狀態機運行過程中的細節和參數,比如計數器、標識符、返回的數據等。
- 可變性和靈活性: 數據可以是變化的,可能有無限多個值。數據的值通常是動態改變的,並且它們的變化並不會改變狀態機的結構,只是影響其內部邏輯。
- 共享和獨立: 數據可以在多個狀態之間共享和使用,而狀態則不能在其他狀態中直接引用。數據是運行時期的屬性,而狀態是行為時期的屬性。
在這個例子中:
- 狀態:
idle
: 系統閒置等待請求。loading
: 系統正在發送請求。retry
: 系統正在處理重試邏輯。success
: 請求成功。failure
: 請求失敗。
- 數據:
retries
: 記錄重試的次數,是一個可變的計數器。A
: 請求返回的數據,是請求結果的具體信息。
補充:一些狀態機設計原則
設計良好的狀態機能夠使系統的行為更加清晰和易於管理。以下是一些設計原則,可以幫助你明確識別和定義狀態:
1. 單一責任原則 (Single Responsibility Principle)
每個狀態應該有明確的單一責任,即它應該只負責一個特定的行為或任務。如果某個狀態需要負責多個不同的行為,考慮將其拆分成多個獨立的狀態。
2. 明確的狀態轉移 (Clear State Transitions)
每個狀態之間的轉移應該是明確的和有意圖的。應避免不必要的狀態轉移,並確保每個轉移都有合理的觸發條件。
3. 有限狀態原則 (Finite State Principle)
狀態機應該只包含有限的狀態。過多的狀態會增加系統的複雜性,難以管理和維護。應盡量保持狀態數量在合理範圍內。
4. 原子狀態 (Atomic States)
狀態應該是原子的,即在任何時刻系統應該只處於一個明確的狀態中。避免狀態重疊或多重狀態,這會導致系統行為不確定。
5. 行為一致性 (Behavior Consistency)
在每個狀態中,系統的行為應該是一致的。這意味著在特定狀態下系統應該總是執行相同的操作,並對相同的事件做出一致的反應。
6. 狀態與數據分離 (Separation of State and Data)
狀態應該描述系統的行為,而數據(如 context
)應該描述狀態機運行時的數據。這有助於保持狀態定義的清晰和簡潔,並使數據管理更加靈活。
7. 可觀察性 (Observability)
確保每個狀態和狀態轉移都是可觀察的,即可以在運行時監控和記錄狀態機的行為。這對於調試和監控系統運行非常重要。
8. 可測試性 (Testability)
狀態應該是可測試的。設計狀態機時應考慮如何對每個狀態和狀態轉移進行單元測試,以確保系統行為符合預期。
9. 簡單性 (Simplicity)
保持狀態機的設計簡單。避免過於複雜的狀態結構和不必要的狀態轉移。簡單的狀態機更容易理解和維護。
10. 具體案例分析 (Case Analysis)
通過具體的業務需求和使用案例來分析系統需要哪些狀態。對每個狀態進行詳細分析,確保它們能夠覆蓋所有業務場景。
例子:HTTP Request 狀態機
以下是上述原則在 HTTP Request 狀態機中的應用示例:
- 單一責任:
loading
狀態負責處理 HTTP 請求。retry
狀態負責處理重試邏輯。success
狀態表示請求成功。failure
狀態表示請求失敗。
- 明確的狀態轉移:
idle
->loading
(接收到FETCH
事件)loading
->success
(請求成功)loading
->retry
(請求失敗)retry
->loading
(重試次數未達到限制)retry
->failure
(重試次數達到限制)
- 有限狀態:
- 狀態數量控制在合理範圍內 (
idle
,loading
,retry
,success
,failure
)
- 狀態數量控制在合理範圍內 (
- 原子狀態:
- 在任何時刻,系統只會處於上述五個狀態之一。
- 行為一致性:
- 在
loading
狀態中,系統總是嘗試發送 HTTP 請求。
- 在
- 狀態與數據分離:
- 重試次數和請求結果數據保存在
context
中,而不是狀態中。
- 重試次數和請求結果數據保存在
- 可觀察性:
- 每次狀態轉移時,記錄當前狀態和上下文數據。
- 可測試性:
- 為每個狀態和轉移條件編寫單元測試,確保系統行為符合預期。
- 簡單性:
- 保持狀態機設計簡單,易於理解和維護。
- 具體案例分析:
- 通過分析 HTTP 請求的不同場景,確定需要哪些狀態來處理這些場景。
通過應用這些設計原則,可以有效地識別和定義狀態,從而構建出清晰且可維護的狀態機。
總結
- 判斷行為還是信息: 如果某個項目改變了系統的行為,那麼它更可能是一個狀態。如果它只是提供了執行這些行為所需的信息,那麼它更可能是數據。
- 數量和範圍: 狀態的數量通常是有限且可枚舉的,而數據的範圍通常是無限的。
- 共享和獨立: 數據可以在多個狀態中共享,而狀態是彼此獨立且不共享的。
通過這些指導原則,你可以更清晰地區分狀態和數據,並設計出更簡潔和高效的狀態機。