ステートマシン図(Ⅱ)

誤解しがちなモデリングの技 第4回

皆川 誠

本記事は、2009年4月22日公開のものを再掲載しています。

はじめに

連載第4回のテーマは「ステートマシン図(II)」です。前回の記事に引き続き、ステートマシン図を描く際に誤って使われることが多いモデル要素や、{あまり嬉しくない|誤った}ステートマシン図の描き方/使い方などをいくつか紹介していきます。

 

その1: ChoiceとJunctionの違い

いくつかの遷移をまとめたり、逆にガード条件によって何本かの遷移に振り分けて表記したりできるように、UMLのステートマシン図にはChoice擬似状態とJunction擬似状態という二種類の擬似状態が用意されています。ところが、ChoiceとJunctionの振る舞い/意味付けの違いを明確に意識せずに適当に使ってしまっているステートマシン図を見かけることがあります。

あるデバイスの状態遷移を考えてみます。このデバイスでは電源ON時に初期化処理が実行されますが、タイミングによって初期化に失敗することがあるものとします。初期化に失敗した時、最大3回初期化処理の再試行(リトライ)をします(つまり、最初の初期化処理とリトライ3回分で最大4回初期化処理が実行される場合があります)。リトライを3回実行しても初期化に失敗してしまう場合はエラー状態になり、初期化に成功した場合は正常稼動中状態になります。

上記のようなデバイスの状態遷移をChoice擬似状態を使った例(図1)とJunction擬似状態を使った例(図2)として、それぞれステートマシン図で描いてみました。ただし、どちらかのステートマシン図は上記の仕様と合致しません(間違った動作をします)。あなたはどちらのステートマシン図が正しいと思いますか?

図1:あるデバイスの状態遷移 (Choice擬似状態)
図1:あるデバイスの状態遷移 (Choice擬似状態)
図2:あるデバイスの状態遷移 (Junction擬似状態)
図2:あるデバイスの状態遷移 (Junction擬似状態)

 

まず、Choice擬似状態(図1)の方の動作の一部を順に追ってみましょう。Choice擬似状態の意味付けは「動的条件分岐(dynamic conditional branch)」で、「初期化中」状態で「初期化失敗」した時の遷移は以下のような手順で実行されます。

  1. ガード条件 [ 初期化失敗 ] が評価される。
  2. [ 初期化失敗 ] がtrueなので遷移が有効になり、遷移アクション「リトライ回数 = リトライ回数 + 1」が実行され、現在の状態が一時的にChoice擬似状態になる。
  3. ガード条件 [ リトライ回数 <= 3 ] と [ リトライ回数 > 3 ] が評価され、どちらかtrueになった方の遷移で「初期化中」か「エラー」のどちらかの状態が現在の状態となる。

同様に、Junction擬似状態(図2)の方の動作の一部を順に追ってみましょう。Junction擬似状態の意味付けは「静的条件分岐 (static conditional branch)」で、「初期化中」状態で「初期化失敗」した時の遷移は以下のような手順で実行されます。

  1. ガード条件 [ 初期化失敗 ] と [ リトライ回数 <= 3 ] と [ リトライ回数 > 3 ] がそれぞれ(可能であれば同時に)評価される。
  2. 上記の評価の結果遷移のパスが確定すると遷移が起こる。その遷移パスに遷移アクションがあった場合はこの段階で実行される(この例では「リトライ回数 = リトライ回数 + 1」)。

以上のように、ChoiceとJunctionではガード条件と遷移アクションの評価/実行順が微妙に入れ替わったりします。この例で言うと、Choice擬似状態の例の方は仕様どおり最大3回のリトライで「エラー」状態に遷移しますが、Junction擬似状態の例の方は最大4回リトライが起こる可能性があります。

ちなみに、図2 のステートマシン図をJunction擬似状態を使わずに描くとしたら 図3 のステートマシン図とだいたい等価な意味合いになります。

図3:あるデバイスの状態遷移 (Junctionを使わないで描いた場合)
図3:あるデバイスの状態遷移 (Junctionを使わないで描いた場合)

 

また、分岐先の遷移にかかるガード条件の値域の範囲(ガード条件の組み合わせ方)について、Choice擬似状態とJunction擬似状態では扱い(意味合い)が若干異なってくるので少し注意が必要です。

図4:Junction擬似状態の例
図4:Junction擬似状態の例

 

たとえば、図4 のようなステートマシン図の一部で、現在の状態が「状態2」で a = 4, b = 4 の時にイベント e2 が発生した場合、遷移パスを確定するために必要なすべてのガード条件 [ a < 5 ] , [ b < 3 ], [ b = 5 ], [ b > 7 ] が評価されます。この状況では b の値が 4 なのでJunction擬似状態から先に分岐するどの遷移も有効にならないため、遷移全体がガードされて(ブロックされて)現在の状態は「状態2」のまま変わりません。これは「正しい(確定的な)」状態遷移です。

図5:Choice擬似状態の例 (不確定になる可能性のある状態遷移)
図5:Choice擬似状態の例 (不確定になる可能性のある状態遷移)

 

図5 は 図4 とほぼ同じ形の状態遷移をChoice擬似状態を使って描いてみた図です。この状態遷移の上で、現在の状態が「状態2」で a = 4, b = 4 の時にイベント e2 が発生した場合、まず「 e2 [ a < 5 ] 」の遷移が起こって現在の状態が一時的にChoice状態に移ります。ところが、この段階で b の値が 4 なのでChoice擬似状態から先に分岐するどの遷移も有効にならないため「状態4」「状態5」「状態6」のいずれにも遷移することができなくなってしまいます。このような場合、この状態遷移は「不確定」ということになってしまいます。つまり、Choice擬似状態から先に分岐する遷移にかけられるガード条件の組み合わせは、すべての可能性のある値域をカバーするように(必ずどれか1本の遷移だけが有効になるように)設定しなければならないということです。

図6:Choice擬似状態の例 ( [else] を使って確定的にした状態遷移)
図6:Choice擬似状態の例 ( [else] を使って確定的にした状態遷移)

 

図6 は 図5 の状態遷移を確定的にするため、分岐先の遷移をひとつ増やして「 [ else ] 」という特別なガード条件(他のガード条件がすべて成り立たなかった時に真となるガード条件)を設定した例です。この状態遷移であれば、b の値が 4 や 6 であった時でも遷移が有効になるので確定的な状態遷移になります。

 

その2: 据え置きイベント

ある図書館の蔵書のライフサイクルを考えてみます。この図書館では以下のような手順で蔵書を管理しています:

  1. 新しく入荷した図書はタグなどを付けて装丁が整えられた後、蔵書として記録/管理されるようになる(この時点から蔵書インスタンスのライフサイクルが開始される)。
  2. 新たに記録/管理された蔵書は貸出可能な状態になる。
  3. 貸出可能な蔵書は期限付きで図書館利用者に貸し出される(貸出中の状態になる)。
  4. 貸出中の蔵書を他の図書館利用者に貸し出すことはできない。
  5. 図書館利用者が貸出中の蔵書を返却することで蔵書は再度貸出可能な状態になる。
  6. 各々の蔵書は年に一度損耗状況の確認が行われ、読めなくなったもの(ページ抜け、破れ、汚れ等で読めなくなった蔵書)は廃棄される。
  7. 貸出中に損耗確認期日が到来した蔵書については、返却後ただちに損耗確認が行われる。

上記の記述を基に 図7 のようなステートマシン図を描いてみました。

図7:図書館の蔵書のライフサイクル (その1)
図7:図書館の蔵書のライフサイクル (その1)

 

このステートマシン図は特に間違っているところがない正しい図なのですが、「貸出中」状態の入れ子あたりが「ちょっとイヤな感じ」に思えます。このような入れ子状態の形になっているのは『貸出中に損耗確認期日が到来した蔵書については、返却後ただちに損耗確認が行われる』という振る舞いに対応するためなのですが、たとえば「延滞(貸出期限を過ぎても返却されていない)」状態や「督促中(貸出期限を一定期間以上過ぎて督促状が送られている)」状態など振る舞いの拡張を想像すると、「貸出中」の入れ子状態がかなり複雑になってきてしまいそうです。

実は(ほとんど知られていませんが)UMLのステートマシン図には「据え置きイベント(deferred event)」という概念が用意されていて、今回の例のような場合に上手く使用すると不要な状態を増やさずに同等の振る舞いを表現することができます。

図8:図書館の蔵書のライフサイクル (その2)

 

図8 は据え置きイベントを使って描き直したステートマシン図の例です。「貸出中」状態に「defer」という特別なアクションが付けられた内部遷移が記述されています。このようにアクションとして「defer」が付けられたイベント(据え置きイベント)は、他の状態に遷移するまで捨てられずに処理が据え置かれます。図8 の例で据え置きイベントが作用するシナリオを順に追ってみると、たとえば以下のような感じになります:

  1. 現在の状態が「貸出中」になっている。
  2. 「貸出中」状態にいる間に「損耗確認期日到来」イベントが発生する。
  3. 「損耗確認期日到来」イベントは捨てられずに据え置かれる。
  4. 「返却」イベントが発生して現在の状態が「貸出中」から「貸出可」に遷移する。
  5. 現在の状態が「貸出可」になると、据え置かれていた「損耗確認期日到来」イベントが(あたかも「貸出可」状態にいる時に発生したかのように)処理される。
  6. 現在の状態が「貸出可」から「損耗確認中」に遷移し、損耗確認アクションが実行される。

このように、据え置きイベントは「何かの状態にいる時に、その状態では処理されないイベントを一時的に据え置いて取っておく(記憶しておく)」ような動作を簡潔に表現することができます。据え置きイベントは知っていると意外と便利な表記のひとつなのですが、残念なことに現状ではあまり広く知られている表記とは言えません(ほとんど知られていません)。そのため、据え置きイベントを使用するステートマシン図を描く時は、それを読む人のために簡単な補足説明を付けておくなどすると良いでしょう。

 

おわりに

私はいつも(UMLで規定されている各種の図のうち)ステートマシン図がもっともプログラミング時の視点に近い図だと感じています(もちろん構造的な側面はクラス図などと組み合わせて補完が必要ですが...)。オブジェクト指向システムを構成するインスタンス群は、それぞれが独立した状態遷移マシンとみなすこともできるので、個々のインスタンスのライフ・サイクルをしっかり理解しておくことはとても重要です。また、モデルから実行可能なコードを自動生成するMDA (Model Driven Architecture) 的な開発環境を利用する場合、状態モデルの重要度はさらに増してきます。

ところが、上手に整理された「綺麗な」ステートマシン図を描くためのセオリー的な方法はあまり確立していないようで、今のところこの辺りのモデリング・スキルは経験とセンスによるところが大きいと言わざるを得ない感があります。経験を深めセンスを磨くために、「正確」で「綺麗」な良いモデルをできるだけたくさん見ることが必要だと思います。