WIZ-CODE.blog

JavaScriptやAjaxをテーマとしたブログです。

*

ステートマシンの仕組みのおさらい その2

   

前回の投稿から一年がたってしまいましたが、ステートマシン図の解説の続きをやります。ステートマシン図には疑似状態(擬似状態とも)と呼ばれる、特殊な用途で用いられる状態が存在します。前回の記事でも開始疑似状態という疑似状態が登場しました。今回はそれ以外に使用されるいくつかの疑似状態を見ていきたいと思います。

履歴疑似状態

まず、サブ状態の遷移先を制御する疑似状態である「履歴疑似状態(History PseudoState)」を解説します。履歴疑似状態はコンポジット状態あるいは直交状態のサブ状態として定義されます。もし親状態が履歴疑似状態を持つと、その親状態に遷移した際、最初のサブ状態の遷移先が通常開始疑似状態となるところ、直前までアクティブだったサブ状態に遷移します。つまり、履歴疑似状態は直前のサブ状態を記録する働きがあるのです。ステートマシン図に履歴疑似状態を組み込むことで、コンポジット状態などの内部を持つ状態に遷移したとき、アクティブにするサブ状態の順番を制御することができます。

履歴疑似状態には「浅い状態」と「深い状態」の2種類があります。2つの違いは履歴を記録する範囲にあります。浅い履歴疑似状態は自身が属する領域内の履歴のみ記録します。つまり、サブ状態のひとつが入れ子、つまりコンポジット状態だったとしても、それより下の階層の履歴については記録しません。一方、深い履歴疑似状態は自身が属する領域を起点に、下位の階層すべての直前のサブ状態の履歴を記録します。

下の図を見ますと、「再生中」というコンポジット状態の内部に「H」と書かれた白抜きの丸があります。これが履歴疑似状態を表します。このイラストでは、まず開始疑似状態から「停止中」という状態への遷移が描かれ、イベントが省略されているものの続いて「再生中」というコンポジット状態に遷移する流れが描かれています。この図はミュージックプレーヤーを表すものとしてください。ここでプレーヤーの再生ボタンが押され、「停止中」から「再生中」へ遷移が起こるとします。すると、この親状態は履歴疑似状態を持つものの、直前の履歴情報が存在しないため、通常の開始疑似状態を経由して「シングル」状態へ遷移します。最初の遷移のときは履歴疑似状態が機能しないことに注意してください。続いて、リピートボタンが押されて「再生中」の内部状態が「シングル」から「リピート」に変わります。ここでいったん、曲の再生を停止し親状態が「停止中」に戻るとします。このとき履歴疑似状態は領域内で最後にアクティブだったサブ状態を記録します。それでは再び「再生中」へ遷移する状況を考えます。このとき、履歴疑似状態は直前のサブ状態の履歴を記録しているので、「シングル」ではなく「リピート」状態がはじめにアクティブ化され、曲の再生が終了しても同じ曲がリピート再生されるようになります。

次に「深い」履歴疑似状態を表す例を用意しました。下の図は同じミュージックプレーヤーを表す図ですが、新たに「電源ON」というコンポジット状態で全体をラップする形にしています。深い履歴疑似状態は「H*」と書かれた白抜きの丸によって表します。上の図と異なり履歴疑似状態の位置が「電源ON」状態のサブ状態として描かれているのが分かると思います。前述のとおり、深い履歴疑似状態は自身の属する領域に入れ子となったサブ状態があるときは、その下位の階層すべてを対象に履歴を記録するので、この図では「電源ON」状態内の履歴に加えて、入れ子となっている「再生中」状態内の履歴も記録されます。

フォーク疑似状態とジョイン疑似状態

2つ以上の処理を並行して行うようなケースがあるとき、フォーク/ジョイン疑似状態(Fork/Join PseudoState)を使用することでうまく表現することができます。フォーク/ジョイン疑似状態はペアで用いられ、並行処理開始の入り口にフォーク疑似状態を、並行処理終了の出口にジョイン疑似状態を配置します。ステートマシン図では並行処理を複数の領域を持つ直交状態として表現するので、これらの疑似状態は直交状態をはさむように描かれます。フォーク/ジョイン疑似状態はともに黒で塗りつぶされた縦長の矩形で描かれます。

並行処理開始の入り口に配されるフォーク疑似状態は、1つ以上の外部遷移を受け取り、2つ以上の外部遷移を出力します。出力側の外部遷移にトリガやガードを書くことはできません。また、出力される外部遷移の遷移先は、それぞれ異なる領域の状態でなければなりません。一方で、ジョイン疑似状態は直交状態内の各領域から2つ以上の外部遷移を受け取り、1つ以上の外部遷移を出力します。こちらはフォーク疑似状態とは反対に、入力側の外部遷移にガードやトリガを書くことができません。

上記の図のフローを詳しく見ていきますと、まず開始疑似状態から直交状態「勉強する」への遷移があります。このときフォーク疑似状態によって外部遷移が2つに分割され、それぞれ別の領域のサブ状態「英語を勉強する」と「YouTubeを観る」に遷移します。個々の領域はそれぞれ独立しているので、上側の領域は「英語を勉強する」が完了すると次に「国語を勉強する」に遷移し、一方で下側の領域は「YouTubeを観る」の状態にとどまります。図には記述されていませんが、ここで状態「勉強する」から「ゲームで遊ぶ」に遷移するトリガが発生したと仮定しましょう。出口となるジョイン疑似状態には、上側の領域の「国語を勉強する」と、下側の領域の「YouTubeを観る」が接続しているので、トリガによってこれらのサブ状態は同時に退場(完了)し、続いて直交状態「勉強する」を抜け出て「ゲームで遊ぶ」への遷移が実行されます。

なお、直交状態を記述するとき、必ずしもフォーク/ジョイン疑似状態を書く必要はありません。たとえば、下図のように同じステートマシン図をフォーク/ジョイン疑似状態なしで描くことが可能です。しかし、フォーク/ジョイン疑似状態を用いる図面の方が、並行処理を表現していることが視覚的にわかりやすい点などメリットがあります。

選択疑似状態

選択疑似状態(Choice PseudoState)は、遷移先をその時点の条件によって決定したいというときに用います。選択疑似状態に遷移すると、その場で条件が評価され、その結果によって遷移先が決定されます。このような遷移先を動的に決定することを動的条件分岐といいます。条件の評価後は選択疑似状態にとどまることはできず、いずれかの状態に遷移しなければなりません。そのため、条件を指定する際に漏れがあると遷移先が存在しないケースが生じる恐れがあります。このようなことがないように、通常は条件分岐にelse文を設けて確実にいずれかの状態に遷移するよう設計します。なお、選択疑似状態は白抜きのひし形によって表現されます。

ジャンクション疑似状態

ジャンクション疑似状態(Junction PseudoState)もまた条件によって遷移先を分岐させるための表記ですが、選択疑似状態とは異なり事前に評価された結果によって遷移先を決定します。このようなケースを静的条件分岐といいます。ジャンクション疑似状態には、外部から受け取る複数の遷移の遷移先をひとつの状態に集約する働きと、外部へ出力される複数の遷移の遷移元を単一にする働きがあります。ジャンクション疑似状態は黒で塗りつぶされた丸で表現されます。開始疑似状態と全く同じ表示形式であることに注意してください。ただし、入力側の遷移の有無で容易に判断することができます。静的条件分岐を表現するのにジャンクション疑似状態は必須ではありませんが、適用するケースによっては、状態と遷移が交差した複雑な関係が整理され、より簡潔な図面になる場合があります。それでは、ジャンクション疑似状態の効果を示すために、比較のためジャンクション疑似状態を使用しないケースを先に示してみたいと思います。

この図は風呂の自動湯沸かし器のシステムを簡易的に書いたものです。システムが起動すると、まず湯船の水位が十分でないときは「注水中」へ、水位が十分のときは「待機中」へ選択疑似状態を経由して遷移します。「湯沸かし開始」トリガが発生すると、現在の状態が「注水中」の場合、水位と水温のふたつを調べ、「待機中」のときは水温のみ調べて、それぞれの条件に合った遷移を実行します。この図ではジャンクション疑似状態を使用していませんが、遷移の矢印が筋交い状に描画されるなど、視覚的に把握しずらい構成になっているのが分かると思います。その他に気になるところとして、まず同じ状態からの遷移に同名のトリガが2つ書かれている点です。この場合、異なるのはガード条件のみなので、むしろ上述の選択疑似状態を使用した方が入力側の遷移(トリガ)がひとつになって簡潔な表現になると思います。続いてそれぞれのガード条件を見ていくと、「注水中」からの遷移が複数の条件が組み合わさった複雑なガード条件であることが分かります。図面の可読性を改善するには、これらの要因を取り除く必要がありますが、このケースはジャンクション疑似状態を使用することでより簡潔な表現に変えられます。では、これを次にジャンクション疑似状態で書きかえた図を見ていくことにします。

このように、あいだにジャンクション疑似状態をはさむことでいくつかの改善が見られました。まず、ジャンクション疑似状態の入力側ですが、「注水中」と「待機中」状態からの遷移がそれぞれ2つから1つに減り、トリガ名の重複もなくなりました。また出力側を見ると、「注水中」のガード条件の後半部分がこちらに分割されて単一の条件に書きかえられ、ガード条件の可読性が改善しています。何よりもジャンクション疑似状態を中継することで、状態遷移のフローがまとめられ視覚的に見やすくなったのではないでしょうか。

ジャンクション疑似状態はある時点でいずれかの状態になるというケースを表現するのに適しています。たとえば、ある時点で状態「A」か「B」のいずれかになるが、またある時点では状態「C」か「D」のいずれかになるといったケースです。この図でいうと、「注水中/待機中」から「湯沸かし中/保温中」への遷移をフェーズの移行ととらえることができるでしょう。このように状態が選択的に遷移する箇所をジャンクション疑似状態を使用して置き換えるとうまくいくかもしれません。

停止疑似状態

最後に解説するのは停止疑似状態(Terminate PseudoState)です。停止疑似状態は文字通り、ステートマシンの全体の振る舞いを停止させる疑似状態です。終了状態と似ていますが、終了状態への遷移はそれを含むコンポジット状態ないし直交状態を終了させるだけであり、ステートマシンの遷移は継続します。一方、停止疑似状態に遷移すると、それがどの階層にあろうと即座にステートマシンは動作を停止します。また、終了状態に遷移した後、その親状態は即座にExitアクションを実行しますが、停止疑似状態に遷移するケースではExitアクションは実行されません。ステートマシン全体で生じる以降の振る舞いすべてが停止されるのです。停止疑似状態はバツ印で表現されます。前回に解説した「退場点」に似ていますが、こちらはバツを丸で囲みません。なお、停止疑似状態に遷移後、そこからまた別の状態へ遷移するということは想定されていません。そのため、たとえばいったん停止した処理を再開させるレジューム機能のようなものを停止疑似状態を使用して表現することはできません。

ステートマシン図の仕組みについて解説してきましたが基本的な事柄については以上です。補足になりますが、ステートマシン図を設計する際は、状態と遷移を描いた図面だけでは設計にモレが生じる可能性があるので、各状態と遷移の対応関係を網羅的に把握する状態遷移表を作成することが一般的といいます。ここでは詳しく扱いませんが、状態遷移表の表記方法はUMLで定められていないため、表の行列の項目をトリガ/状態としたり、遷移前状態/遷移後状態としたり様々です。実際にこれは図面の整合性を確認するのに威力を発揮するので、ぜひ活用してみてください。

参考書籍: かんたん UML入門

ステートマシンをJavaScriptで実装するためのライブラリAsync-FSMを公開しています。
Async-FSM – Finite StateMachine JavaScript Library

 - JavaScript/Ajax, ゲーム , ,