状態遷移をreducer関数で表現する
業務上のイベントと、それに伴う状態遷移がある
これをどのように表現し、どう実装するか?
という問題に対して、イベントを型で表し、状態遷移を関数で表現する方法の解説です。
大雑把にこんなイメージ
事業のイベントとして型を定義する
code:ts
type OrderEvent =
| OrderInitialized
| OrderItemCreated
| OrderItemUpdated
複数のイベントに基づいて現在の状態を復元する
(events: OrderEvent[]) => OrderState
この関数をもっと深ぼる
後者の関数を、state -> event -> stateすなわちreducerとして実装する
(state:State, event:Event) => State
code:ts
type OrderEvent =
| OrderInitialized
| OrderItemCreated
| OrderItemUpdated
...
type OrderState =
| {name: '見積中', ...}
| {name: '見積完了', ...}
| {name: '入金待ち', ...}
...
type Reducer = (state: OrderState, event:OrderEvent) => OrderState
// 永続化層からeventを復元
const orderEvents = restoreFromDB(orderId)
const currentStatus = orderEvents.reduce(reduceOrderEvents, initialState)
// reduceOrderEventsに、どのステートのときにどのイベントが来たら、どの様なステートに遷移するのかを記述する
こうすることで、事業の状態遷移を純粋関数として記述ができる
純粋関数なのでテストが楽
また、複雑な状態遷移に関しては、reducerを複数組み合わせて実現する
このような型です
code:ts
type Reducer = (status: OrderStatus, event: OrderEvent) => OrderStatus;
type Middleware = (next: Reducer) => Reducer;
型がやや難しい
type Middleware = (next: Reducer) => Reducerが、
解説こっちに書いてます
ただし、req->resを(state,event)->stateと読み替える
例:条件に応じてreducerを分岐する
code:ts
const someMiddleware: Middleware = (next) => (status, event) => {
//もしも、このreducerが処理するべき内容なら、新しいstatusを返す
if(expected(status, next)) return reduceSomething(status,next)
// ↑この場合、後続処理を行わないことになる
// そうでなければ、後続処理に任せる
return next(status, next)
}
reducerで書いて嬉しいところ
純粋関数として書ける
テストが読み書きしやすい
イベントを増やしたり、遷移の要件などが変わったときに、テストが全部通って変えられる安心感は大きいです
他の方法は?
対応パターンを値レベルで実現してそれを永続化することで、利用者側が遷移のフローをコントロールできるようにするのはありかもしれない
それと比べる場合、イベントや遷移条件などが動的でなく、事業においても変化が少ない場合にはアプリケーションコードとして書き起こす今回のやり方が良い
データモデルでドメインを駆動する の本にここらへんの話題があった
デメリットは?
一応挙げるとすれば以下のようなものが上げられますmiyamonz.icon
でも最近はAI使えば理解して図に起こしてくれそう
StateやEventの型をよしなに絞るのがちょっと難しい
特定のmiddleware内でだけ存在するeventなど、そこらへんがうまくいかないかも。ちゃんと試行錯誤できてないので不明