1. 位置づけ
単一方向のデータフロー(unidirectional data flow)を強化するためのパターン。
2. 役割
データ層を切り離し、データを予測可能にする(ReactがUIを予測可能にするように、Fluxはデータを予測可能にする)。
具体的な手法:
-
明示的なデータを使用し、派生データを使用しない(宣言してから使用し、一時的にデータを生成しない)。
-
データとビューの状態を分離する(データ層を抽出する)。
-
連鎖的な更新による連鎖的な影響を避ける(ModelとViewが互いに影響し合い、データフローが不明確になるのを防ぐ)。
効果:
-
データの一貫性の向上
-
バグの正確な特定が容易になる
-
ユニットテストが容易になる
3. 構造
actionの生成 actionの伝達 stateの更新
view交互 -----------> dispatcher -----------> stores --------------> views
dispatcher はグローバルに一つだけで、store は複数存在し得る。dispatcher は action の配信/伝達のみを担当し、action から具体的な state の変化へのマッピングは store が管理する。そのため、store は単なる状態の集合である model ではなく、action に基づいて state を更新するロジックも含まれる。その後は state から view への連携となり、これはデータバインディングの具体的な実装に依存する。例えば、Reactではイベントを発火させて更新を通知する(暗黙的な setState())。
ビジネスロジックの大部分は store 内にあり、インタラクションや非同期操作に関連するごく一部が view(Reactコンポーネントなど)にある。
業務では連鎖的な更新が頻繁に発生する。例えば、あるメッセージを既読にする操作により、メッセージリスト内のそのメッセージの表示スタイルを更新し、さらに未読メッセージ数を1つ減らす必要がある。連鎖的な更新は単一方向のデータフローを不明確にする。Fluxは、必ずトップレベルで action をトリガーするように制約することでこれを回避する。1つの view インタラクションで一連の action をトリガーする(連鎖的な action をフラットにし、連鎖関係をトップレベルに集約させ、インタラクション操作に直接関連付ける)。1つの view インタラクションで1つの大きな action をトリガーし、その大きな action が配下の連鎖的な action をトリガーするのではない。
store によって制御の反転(IoC)を実現する。store は外部から内部の state に影響を与えるための setXXX() を提供しない。唯一の方法は、dispatcher に登録されたコールバックを通じて外部データを受け取り、自ら内部の state を更新することであり、これにより関心の分離を明確に保つ。
[caption id="attachment_1422" align="alignnone" width="625"]
flux-simple-f8-diagram-explained[/caption]
単一のdispatcher
中心的なハブであり、すべてのデータフローはここを通過する。コールバックの登録リストを持ち、各 store と連携する。dispatcher自体は、action をすべての store に伝達することだけを担当する。各 store は dispatcher に自分自身を登録し、コールバックを提供する。dispatcher が action を受け取ると、登録されているすべての store がそれぞれのコールバックを通じて action とその付随データを受け取る。
アプリケーションの規模が大きくなると、dispatcher はより複雑になり、各 store 間の依存関係(各 store が登録したコールバックを呼び出す順序)も管理するようになる。store は、他の store の更新が完了するのを待ってから自分自身を更新することを明示的に宣言できる。
複数のstore
アプリケーションの状態とロジックを含む。役割としてはMVCにおける肥大なModelに相当するが、ORMにおけるmodelが1つのデータレコードを表すのとは異なり、複数の state を管理する。Backboneの collection とも異なり、ORMスタイルのオブジェクトの集合をシンプルに管理する。
1つの store はアプリケーションの特定の機能ブロックに対応する内部状態の管理を担当する。つまり、store は具体的なデータモデル(ORM model)や型(Backbone collection)によって分割されるのではなく、ビジネス機能によって分割される。例えば、ImageStoreは一連の画像の状態を記録し、TodoStoreは一連のToDoアイテムを記録する。このように、store はデータ面ではmodelの集合を表し、ロジック面では単一の機能を表す。
store が dispatcher に登録するコールバックは action を引数として受け取る。store 内は switch 文になっており、action の type に基づいて具体的な state 更新メソッドに振り分ける。store の更新が完了すると、イベントをブロードキャストして view に状態が変化したことを伝え、対応する view が新しい状態を取得して自分自身を更新する。
複数のview
自身が依存する store からのブロードキャストイベントをリッスンする特殊な view があり、これらはcontroller-viewと呼ばれる。store からデータを取得し、配下の view へ伝達するロジックを持つ。1つのcontroller-viewは通常、ページ上の1つの論理的なコンテンツに対応し、view の論理的なグルーピングのような役割を果たす。
controller-viewは store からのイベントを受け取ると、まず store が公開している getter を通じて新しいデータを取得し、自身の setState() や forceUpdate() を呼び出して render() を実行する。render() が実行されると、配下の view の render() も実行される。
通常、大きな state の塊を階層の下へと伝達し、配下では必要なものだけを利用する。これは、管理すべき状態を減らすためである(きめ細かな状態の分割を行わない)。トップレベルのcontrollerが外部から状態を更新するのと比較して、これにより配下のコンポーネントの機能を可能な限り純粋に保つことができる。
複数のaction
一般的にヘルパーメソッドを使用して、action の生成や store への登録プロセスをラップする。内部で store と action の連携を維持する(action の type を通じて)。
action はサーバーなどの他の場所から発生することもある。データ初期化時や、サーバーがエラーコードを返した場合、あるいはサーバーデータが更新された場合に、action をトリガーしてビューを同期させる。
4. 特徴
同期の強制
action の配信/伝達と store 内部の state 更新はすべて同期的に行われる。非同期操作の場合は、完了時に手動で action をトリガーする。この仕組み自体は非同期操作の管理をサポートしない。
アプリケーションの情報フローを非常に明確にする。バグが発生したシーンに対応する state を、store、対応する action、そして view 層で action がトリガーされた地点へと遡ることができる。プロセス内のすべてのステップが同期しているため、action に対応する state は予測可能であり、実行タイミングによる予期せぬ事態は発生しない。
制御の反転(IoC)
store は自ら内部の state を更新し、外部からは更新されない。そのため、他の部分は具体的な state の変化を知る必要がなく、状態の変化は store のみに関連する。また、store は action を受け取るだけなので、store のユニットテストを行う場合は、初期状態を与えて action を投入し、最終的な状態が期待通りであるかを確認するだけで済む。
セマンティックなaction
store は action に基づいて state を更新する必要があるため、1つの action は一連の state 更新操作の名前に相当する。これにより意味(セマンティクス)が生まれる。action は状態をどう更新するかは知らないが、期待される結果を記述しており、比較的安定している(アプリケーションの特定の機能を記述しているだけなので、action を修正する必要性は低い)。例えば、MARK_THREAD_READ は特定のメッセージを既読にしたいことを表す。
追加のセマンティック情報は、状態変化の追跡に役立つ。デバッグツール(Redux DevToolsなど)を使用することで、状態変化を追跡可能にできる。
連鎖的なactionの禁止
連鎖的な更新によるデバッグの複雑化を避けるため、ある action が別の action をトリガーすることは許可されない。そのため、action は「アトミック」であり、複雑な階層関係を持たない。
5. 規約
ベストプラクティス セクション、すなわちFluxの慣習的なルール。
store
-
データのキャッシュ
-
データアクセス用の
getterのみを公開し、setterを提供しない -
dispatcherからの特定のactionに応答する -
データが変化したときは常にchangeイベントを発火させる
-
dispatchプロセスの間だけchangeイベントを発火させる
内部状態を維持し、内部でのみ状態を更新する。特定の action に注目し、データが変化したときは理由を問わずchangeを発火させるが、それ以外(dispatcher に起因しない場合など)では発火させない。
action
setterではなくユーザーの行動を記述する(例えば、set-page-idではなくselect-pageであるべき)。
container
-
viewを制御するためのReactコンポーネント -
基本的な職務は、
storeからの情報を収集し、自身のstateに保存すること -
propsや UI ロジックを含まない
要するにcontroller-viewのこと。普通の view との違いは上記の通り。
view
-
containerによって制御されるReactコンポーネント
-
UIとレンダリングロジックを含む
-
すべての情報とコールバックをpropsとして受け取る
普通の view であり、特に変わった点はない。
コメントはまだありません