[状態マシン図] 完了遷移の発火条件

UML2.0状態マシン図の完了遷移について考えてみよう。まず用語定義から。

遷移にはイベントを０個か１個設定することができるが、イベントが設定されていない遷移のことを、ヌル遷移という。図１の状態マシン図のうち、開始疑似状態を起点とする遷移と、s2を起点とする遷移が、ヌル遷移である。

![comp1.png](https://qiita-image-store.s3.amazonaws.com/0/11188/01a0b1d2-4ad9-017f-4c68-5ad90f505c0b.png)

図１

ヌル遷移のうち、状態を起点とするヌル遷移を完了遷移(Completion transition)という。図１の状態マシン図のうち、s2を起点とする遷移が完了遷移である。開始疑似状態、選択疑似状態や、ジャンクション疑似状態など、疑似状態を起点とするヌル遷移は、完了遷移とは呼ばない。

図１のように、完了遷移にガード条件が設定されていない場合は、完了遷移が起点とする状態が完了したときに、直ちに発火する。s2が完了するとただちに発火し、s3に遷移する。
  	
![comp3.png](https://qiita-image-store.s3.amazonaws.com/0/11188/982a6ffc-dc6a-3d84-b28b-4fc750e92d0b.png)

図３

図３のように、完了遷移に、ガード条件が設定されていれば、完了遷移が起点とする状態が完了したときに、ガード条件が評価され真であれば、発火する。つまり、s2が完了したタイミングでg3が評価され、真であれば発火し、s3に遷移する。

---

どのような実装コードに対応するのか確認しておく。
  	
![comp1.png](https://qiita-image-store.s3.amazonaws.com/0/11188/01a0b1d2-4ad9-017f-4c68-5ad90f505c0b.png)
図１

図１の実装は、以下のようになる。状態マシン図の実装は実装言語依存であるため、Ｃ言語ライクな疑似言語で書くことにする。

```c:source1.c
状態 s = s1;
while(true) {
  イベント e = イベント受信();
  if (s == s1 && e == e2) {
    s = s2;    // 最適化されるとこの行はなくなるだろう。
    s = s3;
  } else if (s == s3 && e == e1) {
    s = s1;
  }
}
```

s2ではイベント待ちにならない。

また、それぞれの状態に入場時アクション、退場時アクションが設定されていると、以下のようになる。
  	
![comp2.png](https://qiita-image-store.s3.amazonaws.com/0/11188/f4978526-5ac8-ac4f-1725-fcfbe9203383.png)

図２

図２の実装は、以下のようになる。

```c:source2.c
状態 s = s1;
entry1;
while(true) {
  イベント e = イベント受信();
  if (s == s1 && e == e2) {
    exit1;
    s = s2;
    entry2;
    exit2;
    s = s3;
    entry3;
  } else if (s == s3 && e == e1) {
    exit3;
    s = s1;
    entry1;
  }
}
```

また、完了遷移にガード条件が設定されていると、以下のようになる。
  	
![comp3.png](https://qiita-image-store.s3.amazonaws.com/0/11188/ffa16054-634a-ea93-781f-41c6fb51cb1d.png)

図３

図３の実装は、以下のようになる。

```c:source3.c
状態 s = s1;
while(true) {
  イベント e = イベント受信();
  if (s == s1 && e == e2) {
    s = s2;
    if (g3) {
      s = s3;
    }
  } else if (s == s2 && g3) {
    s = s3;
  } else if (s == s3 && e == e1) {
    s = s1;
  }
}
```

s2に遷移したときにg3が評価され、真であればs3に遷移し、偽であればs2にとどまる。また、イベントを受信したときに、s2にいれば、イベントの種類がなんであっても、その都度g3が評価される。

このようになる。アクションは文でも関数呼出しでも良いし、ガード条件は値がboolな変数、式や戻り値がboolな関数呼出しでも良い。

---

状態が「完了する」のはいつなのか、「完了する」タイミングをすべてリストアップしてみよう。

（１）状態が、単純状態の場合、以下のような順に判定される

（１－１）状態にdoアクティビティが設定されていた場合は、doアクティビティが完了したとき、状態が完了したとみなす。

![comp4.png](https://qiita-image-store.s3.amazonaws.com/0/11188/a954f6be-25ba-5720-85bb-2992679fa8ec.png)

図４

図４では、湯沸し中に入って、まずランプを点灯し、お湯を沸かしはじめる。お湯が沸いたら、湯沸し中が完了し、湯沸し中を起点とする完了遷移が発火し、ランプを消灯して、s3に遷移する。

（１－２）状態にdoアクティビティが設定されておらず、入場時アクションが設定されていた場合は、入場時アクションを実行し終わった時、状態が完了したとみなす。

![comp2.png](https://qiita-image-store.s3.amazonaws.com/0/11188/97bf21ed-5f39-d1b0-0c54-c1d0cf1184f1.png)

図２

図２では、s2に入ってentry2を実行し終わった時を、s2が完了したとみなす。entry2を実行し終わったときに、s2を起点とする完了遷移が発火し、exit2を実行し、s3に遷移する。

（１－３）状態にdoアクティビティも、入場時アクションも設定されていなかった場合は、その状態に入場したとき、状態が完了したとみなす。

![comp1.png](https://qiita-image-store.s3.amazonaws.com/0/11188/01a0b1d2-4ad9-017f-4c68-5ad90f505c0b.png)
図１

図１では、s2に入場したときに、s2は完了したとみなされ、ただちにs3に遷移する。

---

（２）状態がコンポジット状態で、内部に１つ以上の領域を持っている場合

すべての領域が完了したときに、その状態が完了したとみなす。
領域が２つの状態を持つ直交状態であれば、２つの領域の両方が完了したとき、その状態が完了したとみなす。

![comp7.png](https://qiita-image-store.s3.amazonaws.com/0/11188/552cd917-6b10-2b92-5f36-5015c083fe6c.png)

図７

図７では、状態s2にいるときは、上側の領域が完了し、かつ、下側の領域が完了したとき、s2が完了したとみなす。

---

（３）領域の完了判定
領域が完了したかどうかは、次の順番に判定する。

（３－１）領域に１つ以上の終了状態がある場合は、いずれかの終了状態に遷移したとき、その領域が完了したとみなす。

![comp6.png](https://qiita-image-store.s3.amazonaws.com/0/11188/c8bad163-d958-57c5-1b60-84423fac5032.png)

図６

図６では、s21でe3がおきるか、s22でe3がおきると、s2を起点とする完了遷移が発火し、s3に遷移する。終了状態に遷移しない限りは、完了遷移は発火しない。

（３－２）領域に終了状態が１つもない場合は、領域のいずれかの状態が完了したとき、その領域が完了したとみなす。

![comp5.png](https://qiita-image-store.s3.amazonaws.com/0/11188/3e8c62d3-026b-1d57-3423-8eb85338eb9e.png)

図５

図５では、s2の内部の領域に終了状態が１つもないので、s21が完了したときか、s22が完了したときを、s2が完了したときとみなす。なので、図５の場合は、s2に入場して、s21に入場したときにs2は完了し、ただちにs2を起点とする完了遷移が発火してs3に遷移するため、s22に遷移することはない。おそらくモデルの設計ミスである。レビューで指摘する。

---

以下の例で、これまでの話しをまとめてみる。

![comp7.png](https://qiita-image-store.s3.amazonaws.com/0/11188/de3383d3-e513-4e7d-0f62-1195a26bc8ca.png)
図７

図７では、s2を起点とする完了遷移が設定されている。s2は２つの領域があり、上の領域にも下の領域にも終了状態がある。なので、上の領域で、s22にいてe31がおきて、上の領域が完了し、かつ、下の領域で、s24にいてe33がおきて、下の領域が完了すると、s2が完了し、s2を起点とする完了遷移発火する。

上の領域と、下の領域で、どちらが先に終了状態に遷移するかはわからないが、あとに遷移した側を待って、同期をとって、s2を起点とする完了遷移が発火する。

![comp8.png](https://qiita-image-store.s3.amazonaws.com/0/11188/0e7e4ea3-a599-f62d-a11d-51ce6d35745e.png)

図８

図８では、s2に３つの領域が設定されている。３番目の領域には終了状態がないので、s25にいても、s26にいても、３番目の領域は完了しているとみなされる。なので、１番目と２番目の領域の両方が終了状態遷移したときに、s2を起点とする完了遷移が発火する。

![comp9.png](https://qiita-image-store.s3.amazonaws.com/0/11188/818e4126-a48d-0105-65e4-4ce3ac0f08db.png)

図９

図９では、s2にいるときは、イベントを受信するたびに、s2の内部で発火するかどうかが試され、発火しても発火しなくても、その結果s2にとどまるならば、引き続き上位を見て、s2を起点とする各遷移が発火するかどうかが試される。

具 体的には、s2を起点とするタイムアウトが設定されているので、５秒おきに、g4が評価され、g4が真であればs4に遷移し、g4が偽であれば、s2にとどまるので、g3が評価される。このように、明示的なイベントの評価が先で、完了遷移に設定されているガード条件の評価は後に行われる。

もし、s21にいて、イベントが発生すると、以下のような疑似コードになる。

```c:source21.c
// 先にs21（下位の状態）を処理
if (e == e23) {
  s = s23;
} else if (g22) {
  s = s22;
}
// 次にs2(上位の状態)を処理
if (e == タイムアウト5秒 && g4) {
  s = s4;
} else if (g3) {
  s = s3;
}
```

このように階層をまたがって複数の箇所に完了遷移が設定されていると、読み取るのがかなり難しくなり、レビューに時間がかかる。慎重に設計してほしい。

---

参考文献

1. UML2.4.1 仕様書<br>
[http://www.omg.org/spec/UML/2.4.1/](http://www.omg.org/spec/UML/2.4.1/)<br>
Infrastructure specification<br>
p.583 Completion transitions and completion events