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

その1: ガード条件の取り扱い

まずは 図1 に示すような構造を持つ(ひとつの「ヒーター」とひとつの「温度センサー」を部品として持つ)単純な「保温器」を想像してみてください。

図1:単純な「保温器」のクラス図
図1:単純な「保温器」のクラス図

この「保温器」は「温度センサー」から取得した「現在温度」が設定された「上限温度」と「下限温度」の範囲に入るように「ヒーター」のON/OFFを制御するものとします。この「保温器」のステートマシン図(状態遷移図)を考えてみましょう。

図2:ガード条件の間違った使い方の例
図2:ガード条件の間違った使い方の例

図2 はガード条件の使い方を誤ってしまっている典型的な例です。この状態遷移図に従って実装された「保温器」は、おそらく「現在温度」が「上限温度」を超えても「ヒーター」がOFFにならず、どこまでも加熱し続けてしまいます(場合によってはペットの熱帯魚が全滅してしまったり、火災が発生してしまったりします)。

このステートマシン図でまずいのは「加熱中」状態と「非加熱中」状態の間の遷移にかけられているガード条件の部分です。おそらく、このようなステートマシン図を描く人は『これらのガード条件はいつでも常にチェックされていて、その条件が満たされたら遷移が起こる』という間違った解釈をしてしまっているのでしょう。この例では、トリガー(イベント)の指定がされていない自動遷移(ラムダ遷移)に対してガード条件がかけられている形式になっているので、このステートマシン図で示されている動作を「保温器」インスタンスが生成された時から順に正確に追ってみると以下のようになります。

  1. 最初に現在の状態が「加熱中」になる。
  2. 「加熱中」状態のentryアクション「ヒーターON」が実行される。
  3. entryアクションの実行が終わったら「非加熱中」状態に自動遷移しようとする。
  4. この時、自動遷移にかけられているガード条件「 [ 現在温度 > 上限温度 ] 」が一回だけ評価される。
  5. この時点ではヒーターがONになったばかりなので、おそらく「現在温度」は「上限温度」を超えていない。ガード条件を満たさないのでこの自動遷移はブロック(阻止)される(現在の状態は「加熱中」のまま)。
  6. 以後、「電源OFF」イベントが起こらない限り現在の状態は「加熱中」状態のままで、前のステップでブロックされた自動遷移も二度と起こらない(ここにかかっているガード条件の評価も行われない)ので、電源入りっぱなしの「ヒーター」はいつまでも加熱を続ける...。

ガード条件というのは「遷移が起ころうとするのを{ブロック|ガード}するために付加する条件」で、そのガード条件がかけられている遷移が起ころうとする時にだけ評価されます。決して「ガード条件はいつでもチェックされている」と勘違いしないでください。

...では、この保温器を正しく動作させるためにはどのようなステートマシン図を描けば良いのでしょうか?

ひとつの方法として 図3 のようにポーリング的な自己遷移を追加するやり方が考えられます。

図3:ポーリング的なステートマシン図の例
図3:ポーリング的なステートマシン図の例

このステートマシン図は「加熱中」状態および「非加熱中」状態に「after(1秒)」というトリガーが付けられた自己遷移が追加されていることを除いて 図2 のステートマシン図と同じです。ここで使われている「after(時間)」という記述はUMLで用意されているTimeEventの一種で、この例では現在の状態が「加熱中」状態(または「非加熱中」状態)になった時から1秒後にafterイベントが自動的に発火し、一度現在の状態を抜けてから再度同じ状態に遷移します。つまり、現在の状態が「加熱中」(または「非加熱中」)状態にある間、常に1秒間隔で現在の状態に入りなおしているということです。このようにしておくと、1秒間隔で現在の状態が変わる(入りなおされる)ので、その都度自動遷移(ラムダ遷移)を起こそうとしてガード条件が評価されるようになります。

他に 図4 のような記述をすることもできそうです。

図4:whenイベントを利用した記述の例 (その1)
図4:whenイベントを利用した記述の例 (その1)

このステートマシン図では 図2 でガード条件になっていた部分が「when( 条件式 )」という記述に置き換わっています。この記述はUMLで用意されているChangeEventの利用例になっています。whenイベントは「条件式の値がいつでもチェックされていて、この式の値が偽(false)から真(true)に変わった時に発火する」というように解釈されます。このような便利なモデル要素は知らないと損ですよね。

さて、図4 のステートマシン図は一見良さそう(正しく動作しそう)に思えるかもしれませんが、実はまだあまいところがあります。whenイベントは「条件式の値が偽(false)から真(true)に変わった時に発火する」ので、既にその条件式を満たしている状況の時にはこのイベントは起こりません。そのため、図4 のステートマシン図では「保温器」のインスタンスが生成された時に既に「現在温度」が「上限温度」を超えていた場合、「ヒーター」がONになりっぱなしになって温度がどんどん上がっていってしまいます。

このような状況を避けるため、たとえば 図5 のように初期状態からの遷移を条件によって振り分けるようにするなどの対処をしておくと良いでしょう。

図5:whenイベントを利用した記述の例 (その2)
図5:whenイベントを利用した記述の例 (その2)

その2: 非決定な状態遷移

次は、「冷房」「暖房」「除湿」という三つの運転モードを持つエアコンを想像してみてください。このエアコンは、運転中に(手元のリモコンの)「運転切替」ボタンを押す毎に「冷房」→「暖房」→「除湿」→「冷房」→「暖房」...というように現在の運転モードが循環的に切り替わるものとします。

このような「エアコン」の状態遷移を表現しようとする時、状態遷移図に慣れていない人は 図6 のようなステートマシン図を描いてしまうことが意外と多いようです。

図6:非決定な状態遷移
図6:非決定な状態遷移

このステートマシン図では、「停止中」状態から「運転開始」イベントが起こると「運転中」状態に遷移しますが、その「運転中」状態の入れ子の「冷房」「暖房」「除湿」のどの状態が現在の状態になるか決定することができません。

図7:入れ子の初期状態を明示した正しい例
図7:入れ子の初期状態を明示した正しい例

図7 のステートマシン図では、「運転中」状態の入れ子の状態遷移に初期状態が明示されています。このステートマシン図では、「停止中」状態から「運転開始」イベントが起こると、現在の状態は「運転中」状態の入れ子の「冷房」状態になることが明示されています。

これと同じような意味を表わす描き方として、図8 のように入れ子の外部の状態から直接入れ子の内部の状態に遷移線を引いて表現することもできます。

図8:入れ子の状態まで直接遷移線を引いた正しい例
図8:入れ子の状態まで直接遷移線を引いた正しい例

その3: 履歴状態の取り扱い

UMLのステートマシン図には履歴状態(History State)という便利なモデル要素が用意されています。履歴状態は、ある入れ子の状態遷移から抜けて別の状態に移る際に直前にどの状態にいたか記憶しておき、後でその直前にいた状態に現在の状態を戻してくれる機構を表現しています。このモデル要素を利用すると、いくつかのモードを持つようなシステムの中断(suspend)/再開(resume)処理などをシンプルに描き表したりすることができます。

このような便利な履歴状態ですが、なぜか正しく表記されることが少ないようです。例として前出のエアコンの状態遷移を考えてみてください。ただし、今度は「運転停止」「運転開始」で運転を再開した時に、直前の運転モードで運転再開するようにしたいものとします。

図9:間違った履歴状態の使い方の例
図9:間違った履歴状態の使い方の例

図9 は良く見られる「間違った」履歴状態の使い方の例です。このステートマシン図中で小さい丸で囲まれた「H」のアイコンが履歴状態です。おそらく、このようなステートマシン図を描く人の気持ちとしては、『「運転中」状態に遷移してきた時、この入れ子状態に置いてある履歴状態が直前に居た入れ子の状態(「冷房」「暖房」「除湿」のいずれか)に自動的に遷移してくれる(んじゃないかな)』といった感じだと思うのですが、これでは履歴状態は機能しません(前出の 図6 と同じように非決定な状態遷移図になってしまいます)。

UMLの仕様書をしっかり読んでみると、「履歴状態に入ってくる遷移線によって状態遷移が起こる場合、履歴状態は現在の状態を記憶されている直前に居た状態にする」という旨の記述が見つかります。つまり、履歴状態のアイコンに入っていく遷移線が無いと履歴状態はその機能を発揮しないんですね。

図10:履歴状態の使い方の例 (その1)
図10:履歴状態の使い方の例 (その1)

図10 では、履歴状態のアイコンに遷移していく遷移線を明示してみました。「停止中」状態で「運転開始」イベントが発生すると履歴状態に遷移するので、履歴状態は記憶されている直前に居た入れ子の状態(「冷房」「暖房」「除湿」のいずれか)に現在の状態を遷移してくれます。

このステートマシン図は一見良さそう(正しそう)に思えるのですが、まだ甘いところがあります。それは『まだ「運転中」状態に一度も遷移していない時に履歴状態へ遷移が起こったらどうすれば良いのか?』が明示されていないという点です。この「エアコン」のインスタンス生成直後、現在の状態は「停止中」にあります。その状況で「運転開始」イベントが起こると「運転中」状態中の履歴状態に遷移してきますが、この時点ではこの履歴状態は「直前にどの状態に居た」という情報を記憶していません(初めて「運転中」状態に遷移してきたのですから当然ですね...)。ということで、このステートマシン図はまだ「非決定な(現在の状態が特定できない場合がある)状態遷移図」になっています。

このように非決定な状態遷移になってしまうことを避けるために、(初期状態と同じように)最大1本だけ履歴状態から出て行く遷移線を引くことができます(状態遷移が決定的にできる場合はこのような遷移線を引かなくてもOKです)。この遷移線は、履歴状態が直前に居た状態に関する記憶を持たない場合のデフォルトの遷移先として用いられます。このような遷移線を追加した正しいステートマシン図を 図11 に示します。

図11:履歴状態の使い方の例 (その2)
図11:履歴状態の使い方の例 (その2)

このステートマシン図だと、このエアコンはたとえば以下のような動作をします(適当なシナリオを想定しています)。

  1. インスタンス生成直後。現在の状態は「停止中」状態になる。
  2. 「運転開始」イベントが発生すると、「運転中」状態の中の履歴状態への遷移が起こる。
  3. この時点で履歴状態は直前に居た状態に対する記憶を持たないので、現在の状態をデフォルト遷移で指定されている「冷房」にする。
  4. 「運転切替」イベントが発生し、現在の状態が「暖房」になる。
  5. もう一回「運転切替」イベントが発生し、現在の状態が「除湿」になる。
  6. この時点で「運転停止」イベントが発生し、現在の状態は「停止中」に遷移する。ここで、履歴状態は直前に「除湿」状態に居たことを記憶する。
  7. 「停止中」の状態で、再度「運転開始」イベントが発生すると、「運転中」状態の中の履歴状態への遷移が起こる。
  8. この時点で履歴状態は直前にいた状態に対する記憶を持っているので、現在の状態を「除湿」に戻す(デフォルト遷移は無視される)。

これで、このエアコンは運転再開する毎にいちいち運転モードを指定しなおす必要がなくなりました。

おわりに

UMLのステートマシン図は、デビッド・ハレル氏が提唱したハレルチャート(ハレルの状態遷移図)をベースに、さらに様々な仕組みを拡張したとても高機能な状態遷移図になっています。その高機能さ故にすべての機能を使い切るのは難しいですが、「こういう状態変化を表現したいんだけど、うまく描けないなぁ...」などという時にUMLの仕様書を読み直して調べてみると「あまりよく知られていないけれど(実は)とても便利なモデル要素」が見つかったりすることがあります。そういう時、「あぁ、多くの人達が長い時間をかけていろいろ試行錯誤してきた結果のノウハウがUML仕様の一部としてまとめられているんだなぁ」ということを実感します。こういった「先人達の知恵」は有難く有効活用していきたいものですね。