🦋

状態遷移の管理方法を考える

2023/09/05に公開

GUI開発と状態管理の現状について考えてみる

まずはGUI開発においてなぜ状態管理が必要であるかを考え、現状の開発環境でのベストプラクティスを確認します。

GUI開発について

GUIは動画や画像とは違って操作によってデータを変更し即座に画面に反映することができます。旧来のGUI開発では操作ののち、データを変更して画面にどのように反映するかまでロジックを書いていました。WebブラウザのDOM操作APIを使用して開発するとそうなります。しかし、最近のGUI開発においては変更したデータをどのように画面に反映させるかはライブラリの責務となりました。宣言的UIと呼ばれています。開発者はデータの変更のみに注力できるようになりました。

宣言的UIは現代のGUIにおいてデファクトスタンダードになっていて、Flutter、SwiftUI、Jetpack Compose、React など多くの宣言的UIライブラリが実用されています。データの反映について責務を負い、f(State) = UI の関数モデルで扱い、状態の更新の度に再計算・描画を行います。そして、データを更新するためのイベントハンドラが関数の副作用として記述されます。

状態管理の現状について

宣言的UIライブラリを用いた開発において、状態の更新の過程、つまり状態の遷移方法については開発者に任せられています。

宣言的UIライブラリの守備範囲

ON/OFFスイッチが個別にあり、それぞれのBooleanを上書くだけなら簡単ですが、トグルスイッチのように直前の状態を参考に次の状態が決まることは多くあります。さらに現実はもっと複雑で、ログインしているか、送信済みではないか、非同期で進行していた通信は完了しているか、いわゆる複雑なアプリというものはおおよそこの参考にすべき状態の数が多くなります。

ここに管理の仕組みが必要になっていると考えています。

既存の状態管理ライブラリについて

JavaScriptで書かれた著名なライブラリの、Reduxを確認してみます。

Reduxのコンセプト

This is the basic idea behind Redux: a single centralized place to contain the global state in your application, and specific patterns to follow when updating that state to make the code predictable.

https://redux.js.org/tutorials/essentials/part-1-overview-concepts#state-management

状態遷移処理中に任意の直前の状態を参照可能にします。
f(State, Action) = State の関数モデルを適用し、テスタブルかつリーダブルな実装になります。

Redux concepts
https://redux.js.org/tutorials/essentials/part-1-overview-concepts

複雑な状態管理のベストプラクティス

Best Practice の一つとして Treat Reducers as State Machines (STRONGLY RECOMMENDED) が書かれています。

Now, since you're defining behavior per state instead of per action, you also prevent impossible transitions.

https://redux.js.org/style-guide/#treat-reducers-as-state-machines

アプリの状態を直接利用するのではなく、状態としての値を定義し、ステートマシンとして記述することで制御する方法が提案されています。

ステートマシンの実装
const IDLE_STATUS = 'idle';
const LOADING_STATUS = 'loading';
const SUCCESS_STATUS = 'success';
const FAILURE_STATUS = 'failure';

const fetchIdleUserReducer = (state, action) => {
  // state.status is "idle"
  switch (action.type) {
    case FETCH_USER:
      return {
        ...state,
        status: LOADING_STATUS
      }
    }
    default:
      return state;
  }
}

// ... other reducers

const fetchUserReducer = (state, action) => {
  switch (state.status) {
    case IDLE_STATUS:
      return fetchIdleUserReducer(state, action);
    case LOADING_STATUS:
      return fetchLoadingUserReducer(state, action);
    case SUCCESS_STATUS:
      return fetchSuccessUserReducer(state, action);
    case FAILURE_STATUS:
      return fetchFailureUserReducer(state, action);
    default:
      // this should never be reached
      return state;
  }
}

状態遷移を管理する

Reduxのベストプラクティスにならい、ステートマシンを実装する方法について考えます。

Reduxを利用したステートマシンの実装

状態が増えれば増えるほどコードは追いづらくなります。可読性を考えると、switch 文での箇条書きでは一向に全容が見えません。さらに、テストを書くにしても実際の使われ方とテストの入力がほとんど同じになるので、テストケースの正当性もレビューによって保証しにくいものになります。

ステートチャートを利用したステートマシンの実装

状態が多くなっても全容を把握可能にするにはやはり図が必要だと考えます。
しかし、平面的に遷移を記述していくだけでは状態と遷移経路の増加に伴い追いづらくなります。

State explosion
https://slides.com/davidkhourshid/finite-state-machines#/33

その場合はステートに階層構造を持たせることで記述を簡易化することが考えられます。

State explosion
https://slides.com/davidkhourshid/finite-state-machines#/35

このようなステートマシンのためのスマートな実装が欲しくなります。また、このような遷移図から実装が起こせるなら、チーム間のコミュニケーションが円滑になるので望ましいものです。図と実装を正しく対応させられる仕組みがあるなら、なお良いと思います。

それらを満たすライブラリとサービスがありました。

https://xstate.js.org

ステートチャートエディタが提供されており、コードを出力できます。それを組み込むだけでステートマシンの実装が完了します。最高のノーコードツールがありました。

Statechart

複雑になっても図から挙動が把握でき、複雑な条件分岐はライブラリに渡される自動生成されたオブジェクトに収められ、実装はシンプルなものになります。

まとめ

宣言的UIライブラリが普及した今も状態管理の実装は開発者の手に委ねられていて、アプリケーションの規模によって適切な実装方法を選択することが重要です。複雑になってしまった状態管理の一つの改善案として、ステートチャートとステートマシンを用いた方法を紹介しました。

Discussion