2024-07-06|閱讀時間 ‧ 約 32 分鐘

區分狀態和數據,設計出簡潔高效的狀態機


情境:有一個變數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');


這段代碼建立了一個狀態機,其邏輯如下:

  1. 初始狀態是 idle,接收到 FETCH 事件後進入 loading 狀態。
  2. loading 狀態中,執行 fetchData 請求,請求成功則進入 success 狀態,並將變數 A 設置為 0 或 1;請求失敗則進入 retry 狀態,並增加重試次數。
  3. retry 狀態中,如果重試次數達到 3 次,則進入 failure 狀態,否則返回 loading 狀態重新嘗試。
  4. successfailure 狀態是終結狀態,表示狀態機的運行結束。


如何辨別是狀態還是數據?

從上述例子可以看出變數 A 其實並不適合作為狀態機中的狀態。

在設計狀態機時,辨別是狀態還是數據是一個關鍵問題。以下是一些指導原則,可以幫助你做出這個區分:

狀態的特徵

  1. 行為驅動: 狀態反映系統在特定時間點上的行為或狀態。不同的狀態導致系統有不同的行為方式。例如,“加載中”、“重試中”、“成功”和“失敗”這些狀態會導致系統採取不同的行為。
  2. 有限且明確: 狀態的數量通常是有限的,並且在設計時可以明確列出來。每個狀態都有明確的開始和結束條件。
  3. 可觀察和區分: 每個狀態都是系統中的一個獨立的狀態,應該是可以觀察和區分的。你應該能夠清楚地描述在某個狀態下系統的行為特徵。

數據的特徵

  1. 信息驅動: 數據是系統運行所需的具體信息或變量。數據用來記錄狀態機運行過程中的細節和參數,比如計數器、標識符、返回的數據等。
  2. 可變性和靈活性: 數據可以是變化的,可能有無限多個值。數據的值通常是動態改變的,並且它們的變化並不會改變狀態機的結構,只是影響其內部邏輯。
  3. 共享和獨立: 數據可以在多個狀態之間共享和使用,而狀態則不能在其他狀態中直接引用。數據是運行時期的屬性,而狀態是行為時期的屬性。

在這個例子中:

  • 狀態:
    • 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 狀態機中的應用示例:

  1. 單一責任:
    • loading 狀態負責處理 HTTP 請求。
    • retry 狀態負責處理重試邏輯。
    • success 狀態表示請求成功。
    • failure 狀態表示請求失敗。
  2. 明確的狀態轉移:
    • idle -> loading (接收到 FETCH 事件)
    • loading -> success (請求成功)
    • loading -> retry (請求失敗)
    • retry -> loading (重試次數未達到限制)
    • retry -> failure (重試次數達到限制)
  3. 有限狀態:
    • 狀態數量控制在合理範圍內 (idle, loading, retry, success, failure)
  4. 原子狀態:
    • 在任何時刻,系統只會處於上述五個狀態之一。
  5. 行為一致性:
    • loading 狀態中,系統總是嘗試發送 HTTP 請求。
  6. 狀態與數據分離:
    • 重試次數和請求結果數據保存在 context 中,而不是狀態中。
  7. 可觀察性:
    • 每次狀態轉移時,記錄當前狀態和上下文數據。
  8. 可測試性:
    • 為每個狀態和轉移條件編寫單元測試,確保系統行為符合預期。
  9. 簡單性:
    • 保持狀態機設計簡單,易於理解和維護。
  10. 具體案例分析:
  • 通過分析 HTTP 請求的不同場景,確定需要哪些狀態來處理這些場景。

通過應用這些設計原則,可以有效地識別和定義狀態,從而構建出清晰且可維護的狀態機。


總結

  1. 判斷行為還是信息: 如果某個項目改變了系統的行為,那麼它更可能是一個狀態。如果它只是提供了執行這些行為所需的信息,那麼它更可能是數據。
  2. 數量和範圍: 狀態的數量通常是有限且可枚舉的,而數據的範圍通常是無限的。
  3. 共享和獨立: 數據可以在多個狀態中共享,而狀態是彼此獨立且不共享的。

通過這些指導原則,你可以更清晰地區分狀態和數據,並設計出更簡潔和高效的狀態機。

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.