状態遷移のはざま:遷移中状態をどう扱うべきか
状態遷移とは
状態遷移というと急に難しく聞こえるが、やっていることはとてもシンプルだ。
機能が「今どんな状態にいるか」をはっきり決めておき、その状態ごとに振る舞いを切り替えるだけの仕組みである。
外から何かトリガーが飛んでくれば、その状態を切り替えることで動作を自然に変えられる。イベント処理と状態ごとのロジックをきれいに分けられるため、コードの見通しは一気によくなる。
この設計手法は保守性が高く、しかも適用範囲が広い。組込みソフトでは非常に重要な設計パターンだ。
ただし、非同期処理が絡み始めると話は変わる。状態が一瞬で切り替わらず、中途半端な状態が生まれると、設計は途端に扱いづらくなる。今回はそのあたりのややこしさを整理しながら、どう向き合えばよいのかを順に解きほぐしていく。
プロダクトの状態遷移
あまりに単純な例だと実務レベルの複雑さは見えづらい。冗長ではあるが、あえて複雑な状態遷移を用いて説明する。例として多くの組み込み機器が備えるUSB充電機能の簡易的な状態遷移図を用いる。
様々な状態が見えるが、この図で実際に充電を行うのはCHARGING状態のみである。物理的な状態は「充電している」「充電していない」のたった2状態だが、製品品質のためには「充電していない状態」の細分化が避けては通れない。
なぜなら以下は全て「充電していない」状態だが、復帰条件が異なるからだ。
- 高温を検知し充電が停止している。温度が下がるまで充電を始められない
- ユーザ操作によって充電を禁止されている
- エラーによって安全のため充電を停止している
状態遷移のはざま。遷移中とは?
教科書的な状態遷移は状態から状態への遷移は一瞬であるとしている。しかしそれをそのまま物理世界に持ち込むことは難しい。
充電の例でいうと充電の開始条件が整い、充電ICに設定を行い、それが完了してようやく充電を開始する。充電ICによっては設定後一定時間の待ちが必要になることもある。
つまり、状態遷移は瞬間的な遷移ではなく、遷移途中が存在する。
この「遷移中状態」をどう扱えばよいのだろうか?
遷移中状態として新たに定義してみると?
充電遷移中、充電停止遷移中という状態を定義することを考えてみる。
充電開始遷移中
充電している状態は1つで、それ以外は非充電状態だ。
つまり開始時はこうなる。
ここで疑問が生まれる。
遷移中に発生したイベントはどうすればいいのか?状態Aが処理するイベントと、状態Bが処理するイベントは異なるだろう。とすると
充電開始遷移中は状態ABCすべてのイベントを受けられないといけないとなってしまう。
さらに状態A~Cのどこから遷移したのかを知らなければイベントを適切に処理することはできない。
充電停止遷移中
停止時はこうなる。
遷移中状態を個別定義する
上記に挙げた疑問は専用の遷移状態を作れば解決する。状態機械における状態とはイベント解釈が一致する集合であるため、この分岐は妥当であるが、この方法は状態数が爆発的に増加してしまう。
遷移中状態は成立条件がある
では遷移中という状態を考える必要は無いのか?それはNoだ。
遷移中という状態は遷移元、遷移先が1つであれば問題なく適用できる。この構成であれば遷移中のイベント解釈に迷うことはないからだ。
充電の例に当てはめると、設計の工夫で対応することは可能だ。下の図では周期的に充電許可イベントの発行判定を行うことで充電状態への遷移を1つに限定している。この構成であれば遷移中状態を導入しても破綻しない。
このような工夫により遷移中状態を導入することは可能だが、全ての状態をこの考え方だけでカバーすることは困難だ。
遷移中状態を作らずに遷移中を扱おうとすると、この問題は単純化される。
物理状態が変わるのを待って状態を変える、物理状態先行か
先に状態を変えてしまう、論理状態先行のどちらかを採用するのだ。
物理状態先行とは
物理状態先行ではまず物理状態を変更し、その完了後に状態遷移する。
停止状態::イベント処理関数(){
ペリフェラル制御(完了コールバック)
}
完了コールバック(){
状態遷移(RUN状態)
}
この構成は状態と現実が一致するため非常に分かりやすい。
しかしデメリットとして遷移前の状態が長くなってしまうことが挙げられる。
上記例では停止状態はペリフェラル制御をしている間も再度RUNイベントを受信するかもしれない。エラーが起きるかもしれない。
つまり物理状態先行においては遷移中に発生したイベントを処理するのか、無視するのか、保留するのかを設計しなければならない。
メリット
- 状態と現実が一致しやすいため分かりやすい
デメリット
-
遷移中の複雑さを、遷移元状態に吸収させる必要がある
遷移元状態が遷移中を内包するため、肥大化する
論理状態先行とは
停止状態::イベント処理関数(){
状態遷移(RUN状態)
ペリフェラル制御(完了コールバック)
}
この構成は状態を即時変化させるため、教科書的な状態遷移として振る舞う。そのため遷移としてはシンプルになることが多い。また、実処理を待たない分応答性がよく、例えばユーザー操作に素早く応答できる。
一方で物理状態が遅れて進行するため、物理処理のシリアライズが必要になる。例えば状態遷移タスクと処理タスクを分けるなど。処理タスクはタスクが必要なわけではないため、単純なキューで実現できる場合もある。
特にタスクを作る場合、物理状態先行と比べるとメモリリソースを多く消費することはデメリットである。また原理上未来のイベントが積まれた状態となるため、即時処理する(=イベントを割り込ませる)必要がある場合は追加設計が必要になる。
メリット
- 素直な状態遷移を組める
→保守性が良い
デメリット
- キューやタスクといった追加リソースが必要
- イベントキューには未来の処理がすでに投入されている。そのため停止、キャンセル時はキュー整合性や処理中断設計が必要になる
この2系統について整理すると表のようになる。
| 項目 | 論理状態先行 | 物理状態先行 |
|---|---|---|
| 状態遷移のタイミング | 先に状態を変える | 物理処理が終わってから変える |
| 状態と物理の一致 | 一時的にズレる | 常に一致する |
| UI等への応答性 | 高い | 低め(物理待ち) |
| 実装の特徴 | 状態と物理処理を分離する必要がある | 遷移中も元状態に留まる |
| 遷移中イベント | 新状態として扱いやすい | 処理・無視・保留の判断が必要 |
| 実装コスト | 高め(タスク/キュー分離など) | 低め |
適用判断
1.非同期処理の有無
非同期処理では、状態変化の開始から完了までに時間がかかり、その間に別のイベントが入り込む可能性がある。非同期処理がないのであれば物理状態と論理状態は常に一致する。
2.その機能への要求整理
主な観点は非同期処理の多さ、状態遷移表の複雑さの観点である。状態遷移表が単純なら保守性はほどほどでも成立するかもしれないが、複雑なのであればより保守性は重要になる。
3.迷うなら論理状態先行をまず考える
論理状態先行は状態遷移を素直に書くことができる。判断に迷うのであれば保守性を重視し論理状態先行を考えると良い。
ただしシステム観点でタスクやキューを作ることが許容できるか?というリソース観点は重要である。リソースを消費することで複雑さに対処するという判断は間違っていない。機能観点、システム観点で問題が無ければ素直に保守性を優先すればよいだろう。
まとめ
状態遷移は保守性に優れた素晴らしい設計パターンだ。しかし遷移中をどう扱うかは難しい設計課題だ。
遷移中状態は1対1の遷移など限定的な場面では有効であるが、一方でイベント解釈の異なる遷移中を別々に定義すると爆発的に状態が増えてしまう懸念がある。
本記事では、遷移中について以下の2系統に整理した。
- 物理状態先行
- 論理状態先行
実務ではどちらを採用するかは設計に大きな影響を与える。どちらにもメリット・デメリットは存在するが、非同期処理の多さ、要求される応答性、利用できるリソースといった観点から判断する必要がある。
状態遷移は、分かったつもりでも急に難しく感じる瞬間がある。そんなときに本記事が考え方を整理する手助けになれば幸いだ。
Discussion