ステートマシン
ステートマシン(Finite State Machine、FSM)は、有限個の状態と、許可された状態遷移で処理を表します。状態名を持つだけでなく、「どの入力で、どの状態へ進めるか」を一か所で定義することが中心です。
採用する場面
- 承認、公開、決済など、遷移の可否が業務ルールになる
- 同じ遷移をAPI、画面、バッチなど複数箇所から実行する
- 再試行、取消、期限切れなどで分岐が増え、条件文だけでは全体を追いにくい
- 現在状態とイベントの組み合わせを網羅的にテストしたい
読み込み中・成功・失敗の表示だけならDiscriminated Unionで十分なことがあります。状態が一つ増えただけでステートマシンを導入せず、不正遷移を防ぐ必要が生じた時点で使います。
実装方針
状態ごとに保持できるデータが異なる場合は、Discriminated Unionで不可能な組み合わせを表現できないようにします。
type Review =
| { status: "draft"; body: string }
| { status: "submitted"; body: string; submittedAt: Date }
| { status: "approved"; body: string; approvedAt: Date }
遷移関数は現在状態とイベントを受け取り、次の状態または具体的な拒否理由を返します。状態を追加したときはneverによる網羅性検査で、分岐の更新漏れを検出します。
状態ごとに保持するデータや操作が大きく異なる場合は、一つの型へメソッドを集めず、状態ごとのクラスへ分割する選択肢も検討します。支払いと配送のように独立して遷移する状態は、一つのステートマシンへまとめず別々に定義し、必要なら複合型で組み合わせます。
型と実行時の責務
幽霊型やTypestateは、型が保たれる一つの処理内で呼び出し順を制限できます。一方、次の問題は型だけでは防げません。
- データベースや外部APIから不正な状態値が入る
- 二つの要求が同じ状態を同時に更新する
- 古い画面や別バージョンのクライアントから操作される
- 認可されていない利用者が遷移を要求する
外部入力は実行時に検証し、永続化時は条件付き更新やトランザクションで現在状態を再確認します。認可は遷移規則とは別に、操作時点の利用者と対象資源で判定します。