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

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

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

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

```plantuml
object InterfaceAdapter

package StateMachine {
    interface EventListener
    class MachineContext
    abstract State
    class ConcreteStates as "ConcreteState(s)"
    class Actions as "Action(s)"
}

EventListener <|.. MachineContext
EventListener <|.. State
MachineContext "1" *- "1" State : current state
State <|-- ConcreteStates
Actions<- ConcreteStates

InterfaceAdapter -> EventListener
```

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

```plantuml
object InterfaceAdapter
class MachineContext
class ConcreteStates as "ConcreteState(s)"
class Actions as "Action(s)"

InterfaceAdapter -> MachineContext
MachineContext -> ConcreteStates
ConcreteStates -> Actions
```

# 実装例
例題として次の仕様を持つライトをステートマシンで表現します。
- 外部インターフェイスとして、電源ONと電源OFFのボタンがある
- 電源が入っていない状態で電源ONボタンを押すと、青色と赤色にそれぞれ1秒間だけ光った後に消灯状態となる
- 消灯状態で電源ONボタンを押すと、青色が光る
- 青色に光っている状態で電源ONボタンを押すと、赤色が光る
- 赤色に光っている状態で電源ONボタンを押すと、青色が光る
- 電源が入っている状態で電源OFFボタンを押すと、電源が落ちる

```plantuml
LightsOut: entry: -
LightsOut: do: 青色と赤色の順番にそれぞれ1秒間だけ光り、その後は消灯状態になる
LightsOut: exit: -

BlueLighting: entry: -
BlueLighting: do: 青色に光り続ける
BlueLighting: exit: -

RedLighting: entry: -
RedLighting: do: 赤色に光り続ける
RedLighting: exit: -

[*] --> LightsOut
LightsOut -> BlueLighting : 電源ON
BlueLighting -> RedLighting : 電源ON
RedLighting -> BlueLighting : 電源ON

LightsOut -> [*] : 電源OFF
BlueLighting --> [*] : 電源OFF
RedLighting --> [*] : 電源OFF
```
クラス図で表すとこんな感じになります。
```plantuml
object InterfaceAdapter

interface EventListener {
+void onStart(StartEvent)
+void onStop(StopEvent)
}
class MachineContext {
+void onStart(StartEvent)
+void onStop(StopEvent)
}
abstract State {
+void {abstract} entry()
+void {abstract} do()
+void {abstract} exit()
}
class LightsOut {
+void do()
}
class BlueLighting {
+void do()
}
class RedLighting {
+void do()
}

EventListener <|.. MachineContext
EventListener <|.. State
MachineContext "1" *- "1" State : current state
State <|-- LightsOut
State <|-- RedLighting
State <|-- BlueLighting

LightsOut --> BlueLight
LightsOut --> RedLight
BlueLighting --> BlueLight
RedLighting --> RedLight

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

MachineContextは実行に必要な情報を保持します。Go言語の場合にはcontext.Contextでデータ自体を保持しても構わないです。
```go:machinecontext.go
type MachineContext struct {
	current State
}

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

func (m MachineContext) OnStop() {
	m.current.OnStop()
}
```
　Stateの実装クラスです。ステートマシン図のステートを表現しており、主にアクティビティおよびアクションの呼び出しおよび状態遷移を担当します。これによりロジックと状態遷移を分離することができ、ロジックのテストがやりやすくなります。
　ちなみにStateを変更する実装は要件によって結構ブレるのではないかと思います。
```go: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を呼び出して実現するのが理想です。
```go:rediight.go
func RedLighting() {
	fmt.Println(" - Red lighting ...")
}
```
entrypointです。
```go: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
```
# まとめ
　状態遷移は複雑になりがちなためドメインロジックと分離するのが保守性の向上につながります。細かい実現の仕方は要件次第ですが、現在の状態と入力イベントの組み合わせで管理することを意識することが重要ではないかと思います。