04

20

[状態マシン図] 状態マシン図を実装する

2014.04.20(21:22)

状態マシン図の設計モデルができたら、今度はそれを実装に変換しよう。

状態マシン図を実装する方法はいろいろあり、それぞれ一長一短がある。ここではもっとも簡単な形の実装例をひとつあげてみよう。

状態マシンは、「今の状態と発生したイベントから、次の状態を決定する」関数のモデル、と考えることができる。今回紹介する実装例は、

今の状態と 発生したイベントを引数にとり、次の状態を返す関数、
次の状態 = イベントの処理(今の状態, 発生したイベント);
という関数を呼びだして、状態を更新しつづけるものである。組込みシステムでは、RTOSから1つイベントを受信してはこの関数を呼び出すイベントループを構成する。以下のような実装になる。

状態 s = 最初の状態;
while(true) {
  イベント e = イベントを受信();
  s = イベントの処理(s, e);
}

これで状態マシンを実装することができる。具体的な状態マシン図と実装を見てみよう。実装に使う言語はC言語にしよう。

code102.png
typedef enum State { S1,S2 } State;
typedef enum Event { E1,E2 } Event;

void main(void)
{
  State s = S1;
  while(true) {
    Event e = イベント受信();
    s = イベント処理(s,e);
  }
}

State イベント処理(State s,Event e)
{
  if ((s == S1) && (e == E2)) {
    s = S2;
  } else if ((s == S2) && (e == E1) && g1()) {
    a1();
    s = S1;
  }
  return s;
}

イベント処理関数は、引数eに、E1でもE2でもないイベントが渡されたら、それを無視して、今の状態と同じ状態を返す。

また、S2を起点とする遷移にはガード条件g1()が設定されている。S2の状態にいるときに、イベントE1がおきたとき、g1()が成立したならば、S1に遷移することを示している。実装を確認してほしい。

各遷移にアクションを設定し、各状態に入場時アクション、退場時アクションを設定してみよう。

code152.png
typedef enum State { Undefined,S1,S2 } State;
typedef enum Event { E1,E2 } Event;

void main(void)
{
  State s = Undefined;
  a0();
  s = S1;
  entry1();
  while(true) {
    Event e = イベント受信();
    s = イベント処理(s,e);
  }
}

State イベント処理(State s,Event e)
{
  if ((s == S1) && (e == E2)) {
    exit1();
    a2();
    s = S2;
    entry2();
  } else if ((s == S2) && (e == E1) && g1()) {
    exit2();
    a1();
    s = S1;
    entry1();
  }
  return s;
}

StateにUndefinedという値を追加した。a0()を実行する間は、
状態はS1でもS2でもない、Undefinedの状態である。

entry1()とexit1()を実行する間は、状態はS1であり、
entry2()とexit2()を実行する間は、状態はS2となるように、
アクションと状態を更新する順序を工夫している。

Undefinedの状態は、履歴疑似状態の実現でも必要となるので、
状態マシンの実装はいつも、Undefinedの状態から始まるように
しておけばよい。

このように、
次の状態 = イベントの処理(今の状態, 発生したイベント);
という関数呼出しを繰り返せば、状態マシンは実装することができる。

コンポジット状態ではイベントの処理の関数を入れ子にしてゆけばよい。直交状態では、領域ごとにイベントの処理の関数を作り、それぞれの領域ごとに状態を覚えてゆけばよい。履歴疑似状態では、その領域または、その領域以下のすべての状態を覚えておくための変数を別に用意し、履歴が設定されている状態を退場するときに、その変数に覚えておいて、次回入場するときにそれを戻せばよい。

より複雑な状態マシン図も実装してみてほしい。

また、状態マシン図の別の実装としては、GoF23パターンの1つ、Stateパターンが有名である。

参考記事
  1. [状態マシン図] 遷移 と イベント[ガード]/アクション
    http://saltheads.blog134.fc2.com/blog-entry-141.html
  2. [状態マシン図] UML2.0 状態機械 モデル
    http://saltheads.blog134.fc2.com/blog-entry-138.html
  3. [Haskell] Haskellで 状態マシンを書く
    http://saltheads.blog134.fc2.com/blog-entry-80.html

04

14

[状態マシン図] 直交状態

2014.04.14(23:49)

状態は内部に領域を0個以上持つことができる。以下の図で、状態2は領域を1つ持っていて、その領域には開始疑似状態があり、状態3から始まる。状態2にいるときは、状態3か状態4のどちらかの状態を取る。そのことを状態3 or 状態4という。状態3と状態4はどちらか一方しか取ることができないという意味で、このことを OR状態という。状態2にいるときは、状態3 or 状態4である。

orth341.png

次の図では、状態2には2つの領域があり、状態2に遷移すると、2つの領域が同時並行して動作するようになる。直交合成状態と言い、2つの領域が直交している、という。

状態1からイベント2で状態2に遷移したときは、
上側の領域は、状態3から始まり、
下側の領域は、状態5から始まる。
その状態を、状態3 and 状態5の状態という。つまり、状態3かつ状態5の状態であり、このことをAND状態という。
orth342.png

上の図と同じことを、直交状態を使わずに表現したのが次の図である。
状態2は、状態3and5の状態から始まる。そこからイベント4が起きると、状態4and5の状態に遷移する。
上の図は、状態2の内部に、領域が2つあって、上の領域は2つ、下の領域は3つの状態を持っている。
下の図は、状態2の内部に、領域が2つあって、状態が2x3=6つの状態がある。
このように、直交状態の複数の領域を1つの領域になおすと、それぞれの領域が持つ状態のかけ算だけ、状態があることがわかる。

またこういうことも言える。上の図の状態マシンをテストする場合は、下の図のすべてを遷移するテストを書く必要がある、ということである。直交する領域が増えれば増えるほど、描くのは 簡単だが、テストがたいへんになる、ともいえる。

orth343.png

状態の外から、状態の中の領域の中の状態に直接遷移することもできる。
下の図で、状態1にいるときに、イベント4がおきると、状態4に遷移する。
そのときは、下の領域は、開始疑似状態から始まって、状態5になる。
つまり、イベント4が起きると、状態2の、状態4and5から始まる。

入ることができれば、出ることもできる。
下の図で、状態7にいるときに、イベント1が起きると、状態1に遷移する。
そのとき上の領域は、状態3にいても、状態4にいても、状態2を退場して、状態1に遷移する。

orth344.png

このことを、直交状態を使わない図で描くと以下のようになる。
合っているかどうか確かめてほしい。

orth345.png

さらに特殊な例として、直交する領域を横断するような遷移を描くこともできる。下の図で、状態4にいるときに、イベント9が起きたら、状態6に遷移する。

orth346.png

直交する領域をまたがる遷移が発火するときは、その領域を含む状態を退場して、再度、入場すると解釈すると決められている。つまり、上の図は、下の図と解釈される。

orth347.png

状態6に入るのだから、上側の領域は開始疑似状態から始まり、状態3に遷移する。このことを、直交状態を使わない図で描くと以下のようになる。状態5でも、状態6でも、状態7でも、状態4であるときにイベント9が起きると、状態3and6に遷移する。これも合っているかどうか確かめてほしい。

orth348.png

説明用に、直交状態を使わない例をあげたが、状態の数が増え、同じイベントがたくさんの箇所にあらわれ、非常に読みにくく間違えやすいモデルになるので、理屈がわかったら、素直に直交状態を使って設計してほしい。

参考文献
  1. UML2.0仕様書
    http://www.amazon.co.jp/dp/4274066630/
    p.723 遷移実行系列 「直交する状態の領域を横断する遷移は、全直交状態からの退場と、すべての領域への再入場を強制する」

04

12

[状態マシン図] 終了同期を取る

2014.04.12(23:08)

状態マシンを設計していて、何か2つのことを同時並行しておこない、
両方が終了したら、次の状態に遷移したい、というモデルを作りたいことがある。

そのときに、直交状態+終了状態+完了遷移を組み合わせるとうまく実現できる。

36_state.png

状態21と状態22が直交していて、両方が終了状態まで到達したときに、状態9に遷移する。

このように、内部に直交する領域が複数ある場合は、すべての領域が終了状態になったときに、
その状態が終了状態となると決められている。

その領域が終了状態なったかどうかは、以下のアルゴリズムで判断する。
  1. その状態が単純状態であった場合は、入場時アクションを実行終わったところで、終了状態に到達している。
  2. その状態が複合状態(コンポジット状態)であった場合は、
    1つまたは複数ある領域(==状態)のすべてが終了状態に到達しているときに、終了状態となる。
  3. その領域に終了状態が1つ以上ある場合は、いずれかの終了状態に到達しているならば、終了状態である。
  4. その領域に終了状態が1つもない場合は、1と同じ。

このため、以下の場合は、

37_state.png
この場合は、領域23は状態10と状態11のどちらにいても終了状態であると判断され、
状態2にいればIS_IN(状態23が終了状態)は常にTRUEとなり、流れは上のモデルと同く、
状態21と状態22がともに終了状態に到達したときに、状態9に遷移する。

直交合成状態で説明したが、サブマシンを持つ場合でも同様である。

参考文献
  1. UML2.0 仕様書
    http://www.amazon.co.jp/dp/4274066630/
    p.723 完了遷移と完了イベント

04

01

[状態マシン図] UML2.0 仕様の細かな注意点

2014.04.01(07:27)

状態マシン図を勉強している中級者以上向けに、細かな注意点をいくつか説明しておこう。

(1) 領域は名前空間を継承している。
直交状態で、互いに直交している領域に、同じ名前の状態があっても良いことになっている。
それは領域に名前をつけて区別できるからである。

上位状態の名前:領域の名前:領域内の下位状態の名前、
というふうに名前空間がつくので、「領域内の下位状態の名前」が互いに同じでも良い。
領域に名前をつける工夫をすることで、凝集度が高いモデルが作れる可能性がある。
astah*は、領域に名前が付けられなくて、ちょっと残念。

フルパスの名前空間は読むと長いので、レビューしやくするためには、
やはり、上位下位を含めすべての状態に別の名前をつけるのがおススメ。


01_region_inherites_namespace.png


(2) 内部遷移は、状態が保有するのではなく、領域が保有する。
astah*では、内部遷移は、状態が保有している。
UML2.0仕様では、状態が保有する遷移は存在せず、領域が保有する遷移しか存在しない。
つまり、内部遷移は、状態が保有する領域が遷移を保有するしかない。
このように決まってはいるが、実用上は、状態が保有していれば十分なので、
この点は、ツールの実装仕様で良い。

02_internal_transition.png

内部遷移は遷移の仲間ではあるが、sourceやtargetとする節点はどこかなど、
気にしてはいけない。領域は節点ではない。

また、状態が保有する遅延トリガは、ツールでサポートされておらず、扱いが難しいので、
遅延トリガは使わずに分析、設計する。

(3) 遷移1本に、トリガは複数設定できる。
astah*では、遷移1本にトリガは0個か1個であって、2個以上は設定できないことに
なっている。これはこのほうがシンプルで統一的でわかりやすい。
この点も、ツールの実装仕様で良い。
たとえ、UMLでできるからと言って、1本の遷移に2つ以上のトリガを設定しないこと。

03_transition_has_many_triggers.png

(4) 最終状態は、疑似状態の仲間ではなく、状態を継承している。
図を描画しているときは、疑似状態か状態かあまり意識しないが、
実際の動きとしては、最終状態は状態の仲間であり、
・最終状態を起点とする遷移を持てない。
・入場時アクションなどを持てない。
など多くの制約付きの状態であるが、状態のサブ状態にある最終状態であれば、
イベントを受け取れるし、タイムアウトもできることを知っておく。

04_final_state.png

(5) 遷移は名前空間を継承している。
コード生成するときは、もちろん遷移1本ずつ違う名前が必要。
これは分析モデル作成時には不要なので気にしないで良い。

05_transition_inherits_namespace.png

(6) ジャンクション疑似状態も、選択疑似状態も、
2つ以上の入力、2つ以上の出力を同時に持てる

06_junction_choice.png

UML仕様上は、ジャンクション疑似状態、選択疑似状態ともに、複数入力、複数出力できるので、
以下のような状態マシン図も描ける。しかしこれはとてもわかりにくいので避ける。
たとえUMLでできるからといってやらないこと。状態マシン図はフローチャートではない。

ジャンクション疑似状態は、1出力のみ、
選択疑似状態は、1入力、2出力のみ。2出力の片方は必ず[else]、という規約をチームで決めておこう。

071_junction_choice.png

上のジャンクション疑似状態は、以下のモデルと同等の振る舞いをする。

072_junction_choice.png

上の選択疑似状態は、以下のモデルと同等の振る舞いをする。
状態を起点とする完了遷移(明示的なイベントが設定されていない遷移)はただちに発火する。

073_junction_choice.png
モデルはわかりやすく描こう。


参考リンク
  1. UML2.4.1仕様
    http://www.omg.org/spec/UML/2.4.1/
  2. [状態マシン図] UML2.0 状態機械 モデル
    http://saltheads.blog134.fc2.com/blog-entry-138.html

03

12

[状態マシン図] 遷移 と イベント[ガード]/アクション

2014.03.12(20:39)

UML2.0 State Machine (状態機械) モデルでは、
遷移には、イベント(トリガ)とガードとアクションがそれぞれ0個または1個設定できます。

uml20_state_machine_transition.png
遷移に、
イベント [ガード] / アクション
と書きます。
event_guard_action0.png

ですが、遷移がどの状態または疑似状態を始点とするかによって、 イベントが設定できないところ、ガードが設定できないところ、があります。 次の状態マシン図は、それを説明するために設定できるところは全部設定してみた図です。

e[g]/aは全部を設定できるところ、
[g]/aはガードとアクションだけ設定できるところ、
/aはアクションだけを設定できるところです。

★はもし、状態の中の終了条件が成立し次第、
その状態を退場して遷移したい場合、イベントもガードも設定しないことを示しています。
event_guard_action1.png
s18サブマシン状態の下位状態です。
event_guard_action2.png
ルールを文章で書くとこうなります。

/アクションのみしか設定できないところ
  1. 開始疑似状態を始点とする遷移
  2. 履歴疑似状態を始点とする遷移
  3. 状態を始点とする遷移のうち、その状態を完了遷移で退場させたい遷移 ★
  4. フォーク疑似状態を始点とする遷移
  5. ジョイン疑似状態を始点とする遷移
  6. 入場点を始点とする遷移
  7. 退場点を始点とする遷移

[ガード] / アクション のみ設定できるところ
  1. 選択疑似状態を始点とする遷移
  2. ジャンクション疑似状態を始点とする遷移

イベント [ガード] / アクション を設定できるところ
  1. 状態を始点とする遷移 (★を除く)

状態マシン図をレビューするときは、このこともレビュー観点にいれておきましょう。
プロフィール

島敏博

Shima Toshihiro 島敏博
信州アルプスハイランド在住。HaskellとElixirが好き。組み込みソフトウェアアーキテクト、C++プログラマ、山歩き、美術館巡り、和食食べ歩き、日本赤十字社救急法指導員、インデックス投資、クラシック音楽、SESSAME会員、状態マシン設計、モデル駆動開発、ソフトウェアプロダクトライン、Rubyist、実践ビジネス英語

■ ツイッター
http://twitter.com/saltheads
■ Facebook
http://www.facebook.com/saltheads
■ Qiita
http://qiita.com/saltheads

印刷する場合は、ブラウザの印刷メニューではなく、このページの上から3cmくらいの青いところにある、「印刷」を押してみてください。少しうまく印刷できます。まだ完全ではないのですが、これで勘弁してください。


カテゴリ
最新記事
月別アーカイブ
最新コメント
検索フォーム
リンク
sessame
RSSリンクの表示