
ReactやReduxなどのツールにより、Web開発は正しい方向に大きな一歩を踏み出すことができました。 ただし、それらだけでは大規模なアプリケーションを作成するには不十分です。 Webアプリケーションのクライアント側部分の開発状況は、ステートマシンの使用を大幅に改善できるようです。 それらについては、この資料で説明します。 ところで、おそらくこれらのマシンのいくつかを既に構築しているが、それについてはまだ知らない。
ステートマシンの紹介
ステートマシンは、計算の数学モデルです。 これは抽象的な概念であり、マシンはさまざまな状態を持つことができますが、ある時点ではそのうちの1つだけにとどまります。 最も有名なステートマシンはチューリングマシンだと思います。 これは、無制限の数の状態を持つマシンです。つまり、無制限の数の状態を持つことができます。 ほとんどの場合、状態の数が有限であるため、チューリングマシンは最新のインターフェイス開発のニーズを十分に満たしていません。 そのため、状態の数が限られているマシン、またはしばしば呼ばれるように、有限状態マシンは、私たちにとってより良いです。 これはMily マシンとMooreマシンです。
2つの違いは、ムーアオートマトンが以前の状態のみに基づいて状態を変更することです。 ただし、ネットワーク上で行われるユーザーのアクションやプロセスなど、多くの外部要因があります。つまり、ムーアマシンも私たちには適していないということです。 私たちが探しているのは、Milesマシンに非常によく似ています。 この有限状態マシンには初期状態があり、その後、入力データとその現在の状態に基づいて、新しい状態になります。
ステートマシンの動作を説明する最も簡単な方法の1つは、回転式改札機との類推を通じてそれを考慮することです。 限られた状態のセットがあります:それは閉じているか開いていることができます。 私たちの回転式改札口は、特定のエリアへの入り口をブロックします。 彼に3つのスラットを備えたドラムと、お金を受け取るメカニズムを持たせてください。 コインをコインアクセプターに降ろすと閉じたターンスタイルを通過できます。コインアクセプターはデバイスを開いた状態にし、ターンスタイルを通過するためにバーを押します。 改札口を通過した後、再び閉じます。 以下に、これらの状態、および可能な入力信号と状態間の遷移を示す簡単な図を示します。

回転式改札口の初期状態は「クローズ」(ロック)です。 何回彼のバーを押しても、彼は閉じたままになります。 ただし、コインをドロップすると、ターンスタイルは「オープン」(ロック解除)状態になります。 現時点では、回転式改札口は開いたままなので、別のコインは何も変更しません。 一方、ターンスタイルバーを押すことは理にかなっており、それを通り抜けることができます。 さらに、このアクションは、有限状態マシンを初期の「閉じた」状態に移行します。
回転式改札口を制御する唯一の関数を実装する必要がある場合は、おそらく2つの引数に焦点を当てる必要があります。これは現在の状態とアクションです。 Reduxを使用している場合は、これに慣れているかもしれません。 これは、現在の状態を取得し、アクションペイロードに基づいて次の状態を決定する既知のレデューサー関数に似ています。 レデューサーは、ステートマシンのコンテキストにおける遷移です。 実際、何らかの方法で変更できる状態を持つアプリケーションは、ステートマシンと呼ばれます。 実際、これらすべてが何度も何度も手動で実装されています。
ステートマシンの長所は何ですか?
職場ではReduxを使用していますが、これは私たちにぴったりです。 しかし、気に入らない点に気づき始めました。 何かが好きではないからといって、それが機能しないという意味ではありません。 これはすべて、プロジェクトに複雑さを追加し、より多くのコードを書くことを余儀なくされるという事実に関するものです。 サードパーティのプロジェクトを開始し、そこで実験の余地があったので、ReactとReduxでの開発アプローチを再考することにしました。 気になっていることについてメモを取り始め、ステートマシンの抽象化がこれらの問題のいくつかを解決する可能性が高いことに気付きました。 ビジネスに取り掛かり、JavaScriptでステートマシンを実装する方法を見てみましょう。
簡単な問題を解決します。 サーバーAPIからデータを取得し、ユーザーに表示する必要があります。 最初のステップは、問題について考えるとき、遷移ではなく状態の観点から考える方法を理解することです。 ステートマシンに移る前に、作成したいものがどのように見えるかについて、高レベルで説明します。
- [
の
]ボタンがページに表示されます。
- ユーザーはこのボタンをクリックします。
- システムはサーバーに要求を出します。
- データをダウンロードして解析しています。
- ページにデータが表示されます。
- エラーが発生すると、対応するメッセージが表示され、ページに
の
ボタンが再び表示されます。これにより、ユーザーはサーバーからデータを受信するプロセスを再び開始できます。

ここで、問題を分析し、直線的に考え、実際、最終結果へのすべての可能な経路をカバーしようとします。 あるステップは別のステップにつながり、次のステップは別のステップにつながります。 コードでは、これは分岐演算子として表現できます。 ユーザーとシステムのアクションに基づいて構築されたプログラムで思考実験を行ってみましょう。
ユーザーがボタンを2回クリックする状況はどうですか? サーバーからの応答を待っている間にユーザーがボタンをクリックするとどうなりますか? 要求は成功したが、データが破損した場合、システムはどのように動作しますか?
このような状況を処理するには、何が起こっているのかを示すさまざまなフラグが必要になるでしょう。 フラグの存在は、
if
の数の増加を意味し、より複雑なアプリケーションでは、競合の数の増加を意味します。

それは、移行期に考えるからです。 プログラムの変更が正確にどのように発生するか、およびそれらが発生する順序に焦点を当てます。 代わりに、アプリケーションのさまざまな状態に焦点を合わせると、すべてが大幅に簡素化されます。 いくつの条件がありますか? 彼らのインプットは何ですか? 同じ例を使用します。
-
idle
状態(単純)。 この状態では、
の
ボタンを表示し、ユーザーのアクションを待ちます。 可能なアクションは次のとおりです。
-
fetching
の状態。 要求はサーバーに送信されており、完了を待っています。 可能なアクションは次のとおりです。
- ステータスは
error
です。 エラーメッセージを表示し、[
の
]ボタンを表示します。 この状態は1つのアクションを取ります:
ここでは、同じプロセスについて説明しましたが、現在は状態と入力データを使用しています。

これにより、ロジックが簡素化され、予測しやすくなりました。 さらに、上記の問題のいくつかを解決しました。 マシンが
fetching
状態にあるとき、ボタンのマウスクリックに関連するイベントを受け入れないことに注意してください。 したがって、ユーザーがボタンをクリックしても、マシンは
fetching
状態にあるときにこのアクションに反応するように構成されていないため、何も起こりません。 このアプローチにより、コードロジックの予期しない分岐が自動的に排除されます。
これは、テスト中により少ないコードをカバーする必要があることを意味します。 さらに、統合テストなどの一部のタイプのテストは自動化できます。 このアプローチを使用すると、アプリケーションの実行内容を非常に明確に理解でき、定義済みの状態と遷移を通過してステートメントを生成するスクリプトを作成できると考えてください。 これらのステートメントは、可能性のある各状態に到達したこと、または特定の遷移シーケンスを完了したことを証明できます。
実際、どの状態が必要か、どの状態を持っているかを知っているので、可能なすべての状態を書き出す方が、可能なすべての遷移を書き出すよりも簡単です。 ところで、ほとんどの場合、状態はアプリケーションの機能のロジックを記述します。 しかし、トランジションについて話す場合、それらの意味はしばしば作業の開始時に不明です。 プログラムのエラーは、アプリケーションがこれらのアクション用に設計されていない状態にあるときにアクションが実行されるという事実の結果です。 さらに、アプリケーションが適切な状態にある場合でも、アクションが間違った時間に実行される可能性があります。 このようなアクションにより、アプリケーションは不明な状態になり、これによりプログラムが無効になるか、正しく動作しないという事実につながります。 もちろん、必要ありません。 ステートマシンは、このような問題に対する優れた対策です。 これにより、発生する可能性のある状況を明確に指定することなく、発生する可能性のある境界を設定するため、未知の状態に到達することを防ぎます。 ステートマシンの概念は、単方向のデータストリームに適しています。 一緒に、コードの複雑さを軽減し、システムが特定の状態になった方法に関する質問に明確な回答を提供します。
JavaScriptを使用してステートマシンを作成する
十分な話-それはプログラムする時間です。 同じ例を使用します。 上記のリストに基づいて、次のコードから始めましょう。
const machine = { 'idle': { click: function () { ... } }, 'fetching': { success: function () { ... }, failure: function () { ... } }, 'error': { 'retry': function () { ... } } }
状態はオブジェクトによって表され、可能な入力状態信号はオブジェクトのメソッドによって表されます。 ただし、初期状態はここにはありません。 上記のコードを変更して、次の形式にします。
const machine = { state: 'idle', transitions: { 'idle': { click: function() { ... } }, 'fetching': { success: function() { ... }, failure: function() { ... } }, 'error': { 'retry': function() { ... } } } }
意味のある状態をすべて特定したら、入力信号を送信してシステムの状態を変更する準備が整います。 これを行うには、次の2つの補助的な方法を使用します。
const machine = { dispatch(actionName, ...payload) { const actions = this.transitions[this.state]; const action = this.transitions[this.state][actionName]; if (action) { action.apply(machine, ...payload); } }, changeStateTo(newState) { this.state = newState; }, ... }
dispatch
機能は、現在の状態の遷移の中に、指定された名前のアクションがあるかどうかを確認します。 その場合、彼女はこのアクションを呼び出し、呼び出し中に転送されたデータを彼に渡します。 さらに、
action
ハンドラは
machine
をコンテキストとして呼び出されるため、
this.dispatch(<action>)
を使用して別のアクションを
this.dispatch(<action>)
し
this.dispatch(<action>)
this.changeStateTo(<new state>)
を使用して状態を変更したりできます。
この例のユーザーパスに従って、ディスパッチする必要がある最初のアクションは
click
です。 このアクションのハンドラーは次のようになります。
transitions: { 'idle': { click: function () { this.changeStateTo('fetching'); service.getData().then( data => { try { this.dispatch('success', JSON.parse(data)); } catch (error) { this.dispatch('failure', error) } }, error => this.dispatch('failure', error) ); } }, ... } machine.dispatch('click');
最初に、マシンの状態を
fetching
ます。 次に、サーバーへのリクエストを実行します。 promiseを返す
getData
メソッドを持つサービスがあるとします。 このプロミスが解決され、データが正常に解析された後、
succes
イベントをディスパッチします。
すべてが正常に進行している間。 次に、
success
アクションと
failure
アクションを実装し、
fetching
ステータスの入力を記述する必要があります。
transitions: { 'idle': { ... }, 'fetching': { success: function (data) { // this.changeStateTo('idle'); }, failure: function (error) { this.changeStateTo('error'); } }, ... }
以前のプロセスについて考える必要から自分を救ったことに注意してください。 ユーザーがボタンをクリックすることや、HTTPリクエストで何が起こるかは気にしません。 アプリケーションが
fetching
状態にあることはわかっており、これら2つのアクションのみが表示されることを期待しています。 これは、分離して動作する新しいアプリケーションエンジンを作成することに少し似ています。
最後に対処する必要があるのは、
error
状態です。 ここでコードを作成して再試行を実装すると非常に便利です。その結果、エラーが発生した後、アプリケーションを回復できます。
transitions: { 'error': { retry: function () { this.changeStateTo('idle'); this.dispatch('click'); } } }
ここでは、
click
ハンドラーで既に記述されているコードをコピーする必要があり
click
。 これを回避するには、両方のアクションで使用可能な関数としてハンドラーを宣言するか、最初に
idle
状態に切り替えてから
click
アクションを自分でディスパッチする必要があり
click
。
動作状態マシンの完全な例は、CodePenプロジェクトにあります。
ライブラリを使用してステートマシンを管理する
ステートマシンテンプレートは、React、Vue、Angularのいずれを使用しても機能します。 前のセクションで見たように、純粋なJSにステートマシンを実装することはそれほど困難ではありません。 ただし、これを特殊なライブラリに委任すると、プロジェクトの柔軟性が向上します。 ステートマシンの実装に適したライブラリの例には、 Machina.jsおよびXStateが含まれます。 ただし、この記事では、ステートマシンの概念を実装するReduxに似たライブラリであるStentについて説明します。
ステントは、ステートマシンコンテナの実装です。 このライブラリは、ReduxおよびRedux-Sagaプロジェクトからのいくつかのアイデアに従いますが、私の意見では、使いやすく、テンプレートによる制約が少なくなります。 最初にプロジェクトのドキュメントを作成し、次にコードを作成するという事実に基づいたアプローチを使用して開発されています。 このアプローチに続いて、APIの設計のみを数週間行いました。 ライブラリを自分で書いたので、ReduxおよびFluxアーキテクチャを使用して遭遇した問題を修正する機会がありました。
ステントでのステートマシンの作成
ほとんどの場合、アプリケーションは多くの機能を実行します。 その結果、1台のマシンだけを実行することはできません。 したがって、ステントを使用すると、必要な数のマシンを作成できます。
import { Machine } from 'stent'; const machineA = Machine.create('A', { state: ..., transitions: ... }); const machineB = Machine.create('B', { state: ..., transitions: ... });
後で
Machine.get
メソッドを使用してこれらのマシンにアクセスできます。
const machineA = Machine.get('A'); const machineB = Machine.get('B');
マシンをレンダリングロジックに接続する
私の場合のレンダリングはReactツールを使用して行われますが、他のライブラリを使用することもできます。 すべては、レンダリングを開始するコールバックを呼び出すことです。 私が作成したライブラリの最初の機能の1つは、
connect
機能でした。
import { connect } from 'stent/lib/helpers'; Machine.create('MachineA', ...); Machine.create('MachineB', ...); connect() .with('MachineA', 'MachineB') .map((MachineA, MachineB) => { ... });
どのマシンを操作したいかをシステムに通知し、名前を示します。
map
メソッドに渡すコールバックはすぐに呼び出され、これは1回行われます。 その後、いずれかのマシンの状態が変化するたびに呼び出されます。 これがレンダリング関数を呼び出す場所です。 この場所では、接続されたマシンに直接アクセスできるため、マシンとそのメソッドの現在の状態を取得できます。 このライブラリには、一度だけ呼び出す必要があるコールバックを処理するために使用される
mapOnce
メソッドと、この最初の1回限りのコールバックの実行をスキップするための
mapSilent
ます。
便宜上、Reactとの統合のために補助関数がエクスポートされています。 これは、 接続(mapStateToProps) Reduxコンストラクトに非常に似ています。
import React from 'react'; import { connect } from 'stent/lib/react'; class TodoList extends React.Component { render() { const { isIdle, todos } = this.props; ... } } // MachineA MachineB - , // Machine.create export default connect(TodoList) .with('MachineA', 'MachineB') .map((MachineA, MachineB) => { isIdle: MachineA.isIdle, todos: MachineB.state.todos });
ステントはコールバックし、オブジェクトを受け取ることを期待します。 つまり、Reactコンポーネントに
props
として送信されるオブジェクト。
ステントのコンテキストにおける状態とは何ですか?
これまでのところ、状態は単純な文字列です。 残念ながら、現実の世界では、通常の文字列以外の状態で保存する必要があります。 それが、ステント状態がプロパティを持つオブジェクトである理由です。 予約されているプロパティは
name
のみです。 それ以外はすべてアプリケーション固有のデータです。 例:
{ name: 'idle' } { name: 'fetching', todos: [] } { name: 'forward', speed: 120, gear: 4 }
Stentの私の経験では、状態オブジェクトが大きくなりすぎると、おそらくこれらの追加プロパティを処理できる別の状態マシンが必要になることがわかります。 さまざまな状態を特定するには時間がかかりますが、これは管理が容易なアプリケーションを作成する上で大きな前進だと思います。 これは、システムの動作を事前に計画し、将来のアクションのためのスペースを準備する試みのようなものです。
ステートマシンを操作する
材料の冒頭に示した例とほぼ同じ方法で、ステントを使用する場合、マシンの可能な(最終)状態を設定し、可能な入力信号を記述する必要があります。
import { Machine } from 'stent'; const machine = Machine.create('sprinter', { state: { name: 'idle' }, // transitions: { 'idle': { 'run please': function () { return { name: 'running' }; } }, 'running': { 'stop now': function () { return { name: 'idle' }; } } } });
run
アクションを
run
する初期状態
idle
があります。 マシンが
running
状態になったら、
stop
アクションを開始して、マシンを
idle
状態に戻すことができます。
上記の例のヘルパー関数
dispatch
および
changeStateTo
思い出してください。 ステントは同じ機能を提供しますが、ライブラリ内に隠されているため、自分で対処する必要はありません。 便宜上、
transitions
プロパティに基づいて、Stentは以下を生成します。
- マシンが何らかの状態にあるかどうかを確認するヘルパーメソッド。 したがって、
idle
状態になるとisIdle()
メソッドが作成され、running
状態になるとisIdle()
メソッドが作成されます。
- イベントをディスパッチするためのヘルパーメソッド:
runPlease()
およびstopNow()
その結果、この例では次の構成が利用できます。
machine.isIdle(); // machine.isRunning(); // machine.runPlease(); // machine.stopNow(); //
自動生成されたメソッドをサービス
connect
機能と組み合わせることにより、既製のソリューションを思い付くことができます。 ユーザーは、マシンの入力と状態の変化につながるアクションに影響を与えます。 状態の変化により、
connect
渡されたマッピング関数が呼び出され、状態の変化に関する通知を受け取ります。 次に、データが画面に出力されます。
入力データとアクションハンドラー
おそらく、この例で最も重要なのはアクションハンドラーです。 これは、システムが入力データおよび変更された状態にどのように反応するかを説明するため、ほとんどのアプリケーションロジックを記述する場所です。 Reduxの設計中に行われた最も成功したアーキテクチャの決定は、reducer関数の不変性と単純さであると時々思えます 。 本質的に、ステントアクションハンドラーは同じものです。 ハンドラーは現在の状態とアクションに関連付けられたデータを受け取り、その後新しい状態を返す必要があります。 ハンドラーが何も返さない場合(
undefined
)、マシンの状態は変更されません。
transitions: { 'fetching': { 'success': function (state, payload) { const todos = [ ...state.todos, payload ]; return { name: 'idle', todos }; } } }
リモートサーバーからデータをダウンロードするとします。 これを行うには、リクエストを実行し、マシンを
fetching
状態にします。 サーバーからデータが到着するとすぐに、
success
イベントが発生
success
ます。
machine.success({ label: '...' });
その後、
idle
状態に戻り、一部のデータを
todos
配列として保存します。 アクションハンドラを構成するためのオプションがいくつかあります。 最初の最も単純なケースは、新しい状態になる単純な文字列での作業です。
transitions: { 'idle': { 'run': 'running' } }
ここに示されているのは、
run()
アクションを使用した
{ name: 'idle' }
状態から
{ name: 'running' }
状態への遷移です。 このアプローチは、状態に追加のデータがなく、状態間の同期遷移が使用される場合に役立ちます。 したがって、状態オブジェクトに他の何かが格納されている場合、状態間のこのような遷移は、そのような追加データを破壊します。 同様に、状態オブジェクトを直接渡すことができます。
transitions: { 'editing': { 'delete all todos': { name: 'idle', todos: [] } } }
ここでは、
deleteAllTodos
アクションを使用して、
editing
状態から
idle
への移行を観察できます。
ハンドラー関数は既に見ており、アクションハンドラーの最後のバージョンはジェネレーター関数です。 Redux-Sagaプロジェクトは、このメカニズムのイデオロギー的刺激源になりました。次のようになります。
import { call } from 'stent/lib/helpers'; Machine.create('app', { 'idle': { 'fetch data': function * (state, payload) { yield { name: 'fetching' } try { const data = yield call(requestToBackend, '/api/todos/', 'POST'); return { name: 'idle', data }; } catch (error) { return { name: 'error', error }; } } } });
ジェネレーターの経験がない場合、上記のコードフラグメントは少し不思議に見えるかもしれません。 ただし、JavaScriptジェネレーターは強力なツールです。 彼らの助けを借りて、アクションハンドラを一時停止し、状態を数回変更し、非同期メカニズムを処理できます。
発電機実験
Redux-Sagaに初めて会ったとき、非同期操作をサポートするための非常に複雑な方法を提示することにしました。 実際、これはコマンドテンプレートの非常に機知に富んだ実装です。 このテンプレートの主な利点は、メカニズムとその実際の実装の課題を共有していることです。
つまり、システムに必要なことを伝えますが、これがどのように行われるべきかについては話しません。 Matt Hicks による一連の資料は、これを理解するのに役立ちました。 同じアイデアをステントにもたらしました。 システムに制御を移し、必要なものを通知しますが、実際には行いません。 アクションが完了すると、制御が戻ります。
現時点でシステムに転送できるものは次のとおりです。
- マシンの状態を変更するための状態オブジェクト(または文字列)。
- 補助的な
call
関数(同期関数を取ります。同期関数は、promiseまたは他のジェネレーター関数を返す関数です)。 このような関数を渡すことにより、基本的にシステムに次のように伝えます。「この関数を実行し、非同期の場合は待機します。 作業が完了したらすぐに結果をお知らせください。」
- ヘルパー関数の
wait
(別のアクションを表す文字列を受け取ります)。 このサービス関数を使用する場合、ハンドラーを一時停止し、別のアクションのディスパッチを待ちます。
上記の例を示す関数を次に示します。
const fireHTTPRequest = function () { return new Promise((resolve, reject) => { // ... }); } ... transitions: { 'idle': { 'fetch data': function * () { yield 'fetching'; // { name: 'fetching' } yield { name: 'fetching' }; // , // // getTheData checkForErrors const [ data, isError ] = yield wait('get the data', 'check for errors'); // , // fireHTTPRequest const result = yield call(fireHTTPRequest, '/api/data/users'); return { name: 'finish', users: result }; } } }
, , , . , Stent .
Redux Stent
▍
Redux ( Flux) , . , , . , . , , , .
Stent . :
const machine = Machine.create('todo-app', { state: { name: 'idle', todos: [] }, transitions: { 'idle': { 'add todo': function (state, todo) { ... } } } }); machine.addTodo({ title: 'Fix that bug' });
machine.addTodo
, . , : , . React
addToDo
. , . . .
▍
, Redux . Redux, , . , , , ? — Redux? , , , - . , , . , - , .
, ? ?
Redux, , . , . Stent , , . 例:
const machine = Machine.create('app', { state: { name: 'idle' }, transitions: { 'idle': { 'run': 'running', 'jump': 'jumping' }, 'running': { 'stop': 'idle' } } }); // machine.run(); // , // // running stop. machine.jump();
, .
Redux, Flux, . , Redux-, , , , . , , , , , . , , , .
まとめ
, — , . , , , . . , , , — . , — . — , — . - , .
親愛なる読者! ?
