0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ステートマシン図で状態遷移とロジックを分離して実装に落とし込む方法

Posted at

目的

組み込みアプリケーションをステートマシンから実装した時のメモです。
ステートマシンでは状態遷移とロジック(アクション)が適切に管理しないと混沌とした実装になってしまいます。本記事では状態遷移とロジックを分離して実装する方法を共有いたします。

背景

  • デバイスに組み込むアプリケーションをステートマシンを軸に設計して実装を行った
  • ステートマシンから実装に起こす際にはinterfaceで表現する方法を用いた ← コレを説明

クラス図

イベントと状態を抽象化してアクションを呼び出す方法です。

この設計は外部からの入力をEventListenerに渡すことで、その時点のConcreteState(s)を経由してAction(s)を呼び出してくれます。
これを具象クラスのフローで表すと次のようになります。

実装例

例題として次の仕様を持つライトをステートマシンで表現します。

  • 外部インターフェイスとして、電源ONと電源OFFのボタンがある
  • 電源が入っていない状態で電源ONボタンを押すと、青色と赤色にそれぞれ1秒間だけ光った後に消灯状態となる
  • 消灯状態で電源ONボタンを押すと、青色が光る
  • 青色に光っている状態で電源ONボタンを押すと、赤色が光る
  • 赤色に光っている状態で電源ONボタンを押すと、青色が光る
  • 電源が入っている状態で電源OFFボタンを押すと、電源が落ちる

クラス図で表すとこんな感じになります。

コード

実装コードはGithubに登録しています。詳細はそちらをご覧ください。
https://github.com/rtkym/study-statemachine

MachineContextは実行に必要な情報を保持します。Go言語の場合にはcontext.Contextでデータ自体を保持しても構わないです。

machinecontext.go
type MachineContext struct {
	current State
}

func (m *MachineContext) OnStart() {
	m.current.OnStart()
}

func (m MachineContext) OnStop() {
	m.current.OnStop()
}

 Stateの実装クラスです。ステートマシン図のステートを表現しており、主にアクティビティおよびアクションの呼び出しおよび状態遷移を担当します。これによりロジックと状態遷移を分離することができ、ロジックのテストがやりやすくなります。
 ちなみにStateを変更する実装は要件によって結構ブレるのではないかと思います。

redlighting.go
type RedLighting struct {
	mc *MachineContext
}

func (s RedLighting) Entry() {
	fmt.Println("RedLighting#Entry")
	s.Do() // Doの処理は非同期実行だけど割愛
}

func (s RedLighting) Do() {
	light.RedLighting()
}

func (s RedLighting) Exit() {
	// 本当はDoをキャンセルする処理が必用
	fmt.Println("RedLighting#Exit")
}

func (l RedLighting) OnStart() {
	l.mc.changeState(&BlueLighting{mc: l.mc})
}

func (l RedLighting) OnStop() {
	fmt.Println("[Red] Power off")
	l.mc.terminate()
}

アクションはロジックを独立して表現します。テストの観点から外部通信処理はinterfaceで分離して実装します。今回の例だとライトのON/OFFなので、点灯と消灯を行えるinterfaceを呼び出して実現するのが理想です。

rediight.go
func RedLighting() {
	fmt.Println(" - Red lighting ...")
}

entrypointです。

main.go
func main() {
	machine := statemachine.Start()

	machine.OnStart()
	machine.OnStart()
	machine.OnStart()

	machine.OnStop()
}

実行結果は次の通りです。

> go run main.go
LightsOut#Entry
 - Blue lighting ...
 - Red lighting ...
LightsOut#Exit
BlueLighting#Entry
 - Blue lighting ...
BlueLighting#Exit
RedLighting#Entry
 - Red lighting ...
RedLighting#Exit
BlueLighting#Entry
 - Blue lighting ...
[Blue] Power off
Terminate

まとめ

 状態遷移は複雑になりがちなためドメインロジックと分離するのが保守性の向上につながります。細かい実現の仕方は要件次第ですが、現在の状態と入力イベントの組み合わせで管理することを意識することが重要ではないかと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?