2020-04-13 (Updated at: 2020-05-05)

状態論 (1)

#状態論 #thought #state-management

アプリケーションの開発では「状態」という言葉がよく使われるが、「状態」とはなんだろうか。 「状態」は単なるデータと何が違い、なぜアプリケーション開発において重要なのだろうか。 本論では、主にWebアプリケーションの文脈をベースとしながら「状態」の本質を考える。

状態 (state) とはなにか

状態というものを考えるにあたって、アプリケーションそのものを再考しよう。 ユーザーのアクションに対して、アプリケーションは何かしらの振る舞い(behavior)を見せる。言い換えると、振る舞いはアクションがアプリケーションに作用した結果である。アプリケーションを関数 $f$ と置くと、次のような式になるだろうか。

$$ f(Action) = Behavior $$

しかし echo のような単純なコマンドラインツールならまだしも、一般的なアプリケーションにおいてはこの式は成り立たない。簡単なカウンターアプリを想像してみても、カウントアップするという同じアクションの結果として表示される数は 1, 2, 3 と変わっていく。 つまり、アプリケーションの振る舞いの決定にはアクションとは別の要因が存在する。ならばその要因を新たな変数として捉える必要がある。これがアプリケーションの 「状態 (state)」 と呼ばれるものだ。同じアクションに対して結果が変わりうるアプリケーションはかならずその内部に状態を持っている。

$$ f(Action, State) = Behavior $$

しかしこの式もまだ不完全である。アクションは多くの場合、アプリケーションの状態を変化させる。カウンターアプリではカウントアップというアクションは保持する数値の状態を変化させる。そしてアクションによって変更された状態は、次のアクションに対する振る舞いを変化させる。つまり、アクションと振る舞い、状態との関係は次のような式で表現できる。アプリケーションは関数 $f$ と $g$ が合成して成り立っている。

$$ \begin{aligned} State_{i} &= g(Action_i, State_{i-1})\cr Behavior_{i} &= f(Action_i, State_i) \end{aligned} $$

ここから、アプリケーションの振る舞いはアクションと、その時点での状態によって決定することがわかる。 しかし実際のプログラムの上での状態は単純な1つの変数であることはほとんどない。一般的にはクラスのフィールドや現在時刻、インメモリのデータなど、さまざまな状態の組み合わせとなる。そして「状態管理 (state management)」の責務は、どの状態がどのような形で保持され参照されるのかを管理することだ。

状態管理ライブラリの Reduxは 関数 $g$ をJavaScriptの関数で表現する。Reduxではアプリケーションの状態を単一のJSONオブジェクトに集約し、アプリケーションの構造を単純化する。

function reducer(state: State, action: Action): State;
currentState = actions.reduce((state, action) => reducer(state, action), initialState);

ちなみに、任意の時点の状態 $State_n$ は次のように変形できる。

$$ \begin{aligned} State_{n} &= g(Action_n, State_{n-1}) \cr State_{n} &= g(Action_n, g(Action_{n-1}, State_{n-2})) \cr State_{n} &= g(Action_n, g(Action_{n-1}, g(Action_{n-2}, State_{n-3}))) \cr …\cr State_{n} &= g(Action_n, g(Action_{n-1}, g(Action_{n-2}, …, g(Action_{1}, State_0)))) \end{aligned} $$

つまり現在の状態は、初期状態と開始からの今までのアクションの列によって決定される。

宣言的UI

さて、ここからは特にGUIアプリケーションに注目する。昨今のGUIアプリケーションの設計のメインストリームには関数型プログラミングのパラダイムが強く影響している。その中でも中心にあるのが、 $f(\it{State}) = \it{UI}$ の考え方だ。この考えに沿ったUI構築の設計は「宣言的UI」とも呼ばれる。利点はいくつかあるが、代表的なものは以下のものだ。

  1. 再現性: 同一の状態を与えれば同一のUIを再現できる(デバッグしやすい)
  2. 再利用性: 関数自身は状態を持たないため、別の関数との合成や再利用などが容易である

$f(State) = \it{UI}$ とは、UIの出力が現在のアプリケーションの状態にのみ依存するということだ。この関係を先ほどの式に加えてみよう。

$$ \begin{aligned} \it{State_{i}} &= f_{\it{State}}(Action_i, State_{i-1})\cr \it{Behavior_{i}} &= f_{\it{Behavior}}(Action_i, State_i)\cr \it{UI_{i}} &= f_{\it{UI}}(State_i) \end{aligned} $$

ところでUIの出力は当然ながらアプリケーションの振る舞いのうちに含まれているはずだ。そこで振る舞いのうち UIの出力である部分と、そうでないものを次のように分ける。

$$ \it{Behavior} = \it{UI} + \it{Business} $$

そうすると振る舞いについての式は次のように変形できる。$f_{\it{Behavior}}(Action_i, State_i) - f_{\it{UI}}(State_i)$ は結局 $Action_i$ と $State_i$ を変数とする関数であるから、改めて $f_{Business}(Action_i, State_i)$ と置くことができる。

$$ \begin{aligned} f_{\it{Behavior}}(Action_i, State_i) &= f_{\it{UI}}(State_i) + \it{Business_{i}}\cr \it{Business_{i}} &= f_{\it{Behavior}}(Action_i, State_i) - f_{\it{UI}}(State_i)\cr &= f_{\it{Business}}(Action_i, State_i) \end{aligned} $$

こうして、GUIアプリケーションの状態と振る舞いの関係を単純化した次の式が成り立つ。

$$ \begin{aligned} \it{State_{i}} &= f_{\it{State}}(Action_i, State_{i-1}) \cr \it{Behavior_{i}} &= f_{\it{UI}}(State_i) + f_{\it{Business}}(Action_i, State_i) \end{aligned} $$

Reactはこの式をそのままJavaScriptで表現できるライブラリであり、ひとつひとつのコンポーネントがこの $f_{\it{UI}}$ となるように設計されている。また、Angularでもコンポーネントクラスが公開する状態をテンプレートHTMLを通してUIに反映するため、テンプレートHTMLが $f_{\it{UI}}$ の役割をもつ。共通するのは、UIが「操作 (manipulate)」されるのではなく「描画 (render)」される、というアプリケーションの状態とUIの出力の関係である。状態が先にあり、その投影としてUIがあるという主従の関係が重要である。 裏を返せば、ReactであろうとAngularであろうと、レンダリング後のDOMをアプリケーションが直接操作して値を書き換えた瞬間にこの関係は崩壊する。

Reactive UI

$f_{\it{UI}}(\it{State})$ で問題となるのが、状態が変更されたタイミングでUIを再描画しなおす方法だ。 $f_{\it{Business}}(Action, State)$ は何らかのアクションによってトリガーされるが、$f_{\it{UI}}(State)$ は状態の変化をなんらかの方法で知る必要がある。 つまり、状態が監視可能 (observable)であることが重要だ。

Angularは、RxJSの Observable として管理された状態をテンプレートHTMLに接続することによってリアクティブなUIを構築できる。

<ng-container *ngIf="state$ | async as state">
    {{ state.count }} 
</ng-container>

Observable#subscribe() メソッドのコールバックでコンポーネントクラスのフィールドを更新するのは宣言的ではあるが、リアクティブではない。いったんコンポーネントクラスのフィールドを経由すると、そのUIはコンポーネントクラスの状態を投影したものになる。 そしてアプリケーションの状態とコンポーネントクラスの状態を同期する責務はアプリケーション側に残される。 この部分にバグがあれば、アプリケーションの状態を変えてもUIに投影されず、 $f_{\it{UI}}(State)$ が機能していないことになる。リアクティブプログラミングの考え方は、宣言的UIを堅牢にする。

状態と情報

ここまではアプリケーションの状態と振る舞いの関係についての話だった。ここからは状態そのものについて考える。 状態とは何なのか、そのひとつの答えが先ほどの式である。

$$ \it{Behavior_{i}} = f_{\it{Behavior}}(Action_i, State_i) $$

状態とは「振る舞いを決定する変数」、あるいは「振る舞いに影響を与える変数」である。どんなデータでも状態となるわけではなく、状態とそうでないデータの間には違いがある。この違いを、本論では「状態と情報の違い」として表現したい。

「情報 (information)」は、アプリケーションの外から舞い込んでくるデータである。ユーザーが入力した住所、OSの現在時刻、Webアプリが実行されたURLなどさまざまあるが、すべての「情報」に共通するのはそれが外部からやってくるということだ。

「情報」それ自体には数値や文字列以上の意味はない。「情報」に意味を与えるのはアプリケーションだ。住所の文字列はアプリケーションが入力を受け取って初めて「住所」という意味を持つ。なぜなら、アプリケーションはその振る舞いを決定するために「住所」という変数が必要であるからだ。つまり、 「情報」はアプリケーションの"解釈"によって「状態」になる。

この解釈の材料となるのはアプリケーションの裏にある ユースケース だ。テキストボックスの入力イベントを、「住所が変更された」と解釈し、「住所の変更」アクションを発行するためには、ユースケースを深く知っている必要がある。 つまり、状態はユースケースに依存するデータであるとも言える。であるなら、情報はユースケースに依存しないデータ だ。

例1. APIレスポンスと状態

ドメイン駆動設計などの設計パターンでは、システムの中でユースケースに依存する部分としない部分の境界が重要になるが、状態はユースケースに依存するということを意識しておくことが必要だ。そして、「状態管理」と「情報の保持」を区別して考えることが重要だ。

例えば、フロントエンドが「プロフィール表示」というユースケースの中で必要なユーザーデータをバックエンドに要求したとしよう。 このとき、バックエンドAPIがRESTfulであれば、そのレスポンスはユースケースに依存しない「情報」だ。この「情報」はどのように扱うべきだろうか? アプリケーションが求めているのは「プロフィール表示の対象ユーザー」だ。よってアプリケーションは「プロフィール表示の対象ユーザー」を状態として管理する必要がある。つまり、次のように状態を定義してアクションを発行する。

type State = {
  profileView: {
    user: User
  }
}


backendApi.getUser(userId).then(user => {
  store.dispatch(ProfileViewActions.finishFetchingUser(user));
});

次の例のように、データの型に合わせて保持するのは状態管理ではなく、情報の保持である。複数のユースケースで同じユーザーのデータを何度も取得したくないという要求は、ユースケースに依存しない層でバックエンドAPIのキャッシュなどで解決するものだ。

type State = {
  users: {
    [id: string]: User
  }
}

例2. URL変化と状態

シングルページアプリケーションはURLに応じて振る舞いを変える。だとするとURLは「状態」だろうか? 答えはNoだ。ブラウザのURLはアプリケーションの外から与えられる「情報」である。 アプリケーションはURLを解釈し、パスやパラメータをアプリケーションが必要とする状態に変える責務を持つ。

例えば、プロフィール画面が /profile/:userId のようなパスで表示されるシングルページアプリケーションであれば、次のように状態を定義し、URL中のパラメータの変更イベントを購読してアクションを発行することで状態を更新する。 そうして更新された状態をもとにアプリケーションは振る舞いを決定できる。

type State = {
  profileView: {
    params: {
      userId: string;
    };
    user: User
  }
}

routeParams.subscribe(routeParams => {
  store.dispatch(ProfileViewActions.changeParams({userId: routeParams['userId']}))
});

store.select(state => state.profileView.params).subscribe(params => {
  backendApi.getUser(params.userId).then(user => {
    store.dispatch(ProfileViewActions.finishFetchingUser(user));
  });
})

例3. フォーム入力変化と状態

ユーザーがフォームに入力しているデータはまだ「情報」である。 ユーザーが送信ボタンを押したとき、あるいはバリデーション処理が実行されるとき、アプリケーションはその時点の入力内容を解釈し、状態を変化させる。 パフォーマンスの面からみてもそうだが、情報と状態の違いの面から見ても、フォームの入力内容を状態としてそのまま同期することは望ましくない。送信やバリデーションといったそれぞれのユースケースにとって必要な状態だけが管理されるべきだろう。

まとめ

アプリケーションを関数として単純化して捉えることで、アプリケーションの振る舞いは状態とアクションによって決定されることを定式化できた。状態管理は、状態をどのような形式で保持し、アプリケーションに適用するかという重要な責務をもつ。状態を持つ領域と持たない領域を明確に区別することが肝要だ。 宣言的UI $f(\it{State}) = \it{UI}$ とリアクティブプログラミングの考え方は、振る舞いの決定を単純化、安定化するのに役立つ。予測可能性と再現可能性の高いアプリケーションはデバッグやメンテナンスが容易になる。

そして状態と情報の違いについては、そのデータがユースケースによる解釈を受けているかどうかが大きな違いであることに着目した。外部から与えられる「情報」をアプリケーションがユースケースに沿って解釈することで、振る舞いに影響を与える「状態」になる。

なお、今回はサンプルコードにはRedux形式のインターフェースを使った。しかしReduxは状態管理の王道ではあるが唯一の正解ではない。中央集権的な状態管理、分散型の状態管理など状態管理にもいろいろなアプローチがある。これに関しては状態論 (2) に続く予定。

このエントリーをはてなブックマークに追加
comments powered by Disqus