🐥

Stateパターンの整理

2024/11/16に公開

概要

状態に応じてオブジェクトの振る舞いを変えるパターン。

課題

例えばポケモンが健康状態のときに「たたかう」を選ぶと技を選べるが、眠っている状態だと1ターン
お休みになる。また、「にげる」を選んだときも、眠っていると1ターン休みになる。

これは何も考えないと以下のような実装になる。

type Monster struct {
	state string
}

func (m *Monster) Fight() {
	if m.state == "healthy" {
		fmt.Println("Fighting")
	} else if m.state == "sleeping" {
		fmt.Println("Sleeping")
	}
}

func (m *Monster) Retreat() {
	if m.state == "healthy" {
		fmt.Println("Retreating")
	} else if m.state == "sleeping" {
		fmt.Println("Sleeping")
	}
}

しかしこれは、ポケモンの状態が増えていけば行くほどif文を追加していく必要がある。

Stateパターンによる実装

Stateインターフェースを実装する型によって振る舞いを制御する。

var _ State = (*Healthy)(nil)

type Healthy struct{}

func (h *Healthy) Fight() {
	fmt.Println("Fighting")
}
func (h *Healthy) Retreat() {
	fmt.Println("Retreating")
}

var _ State = (*Sleeping)(nil)

type Sleeping struct{}

func (s *Sleeping) Fight() {
	fmt.Println("Sleeping")
}
func (s *Sleeping) Retreat() {
	fmt.Println("Sleeping")
}

以下では、Monsterは7:00-23:00の間は起きていて、それ以外の時間は寝てしまうと仮定して、状態遷移を観察していく。
状態遷移を管理する方法として、以下の2通りがある。

  • State自身が管理する方法
  • Monsterによって管理する方法
    上記のそれぞれについて、実装パターンを見ていく

State自身が状態管理する方法

Sleepingステートは7:00になると起きないといけないことを知っており、Healthyステートは23:00になったら寝ないといけないということを知っている。
これに基づいて実装すると以下のようになる。

type Monster struct {
 	state State
}

func (m *Monster) ChangeState(state State) {
	m.state = state
	fmt.Println("現在のstate: ", m.state.CurrentState())
}

type State interface {
	Fight()
	Retreat()
	CheckClock(*Monster, int)
	CurrentState() string
}

func (h *Healthy) CheckClock(m *Monster, hour int) {
	if hour >= 23 || hour < 7 {
		m.ChangeState(&Sleeping{})
	}
}

func (h *Healthy) CurrentState() string {
	return "Healthy"
}

func (h *Sleeping) CheckClock(m *Monster, hour int) {
	if hour >= 7 && hour < 23 {
		m.ChangeState(&Healthy{})
	}
}

func (h *Sleeping) CurrentState() string {
	return "Sleeping"
}

func main() {
	m := Monster{state: &Healthy{}}
	for {
		for hour := 0; hour < 24; hour++ {
			fmt.Printf("今は%d時00分\n", hour)
			m.state.CheckClock(&m, hour)
			m.state.Fight()
			m.state.Retreat()
			time.Sleep(1 * time.Second)
		}
	}
}
// 今は0時00分
// 現在のstate:  Sleeping
// Sleeping
// Sleeping
// ...
// 今は7時00分
// 現在のstate:  Healthy
// Fighting
// Retreating

すべてのコードは以下
https://go.dev/play/p/zIEcVVAnYJi

State自体が状態を管理する場合の長所・短所は以下の通り。

  • 長所
    • Stateに注目したときの振る舞いが理解しやすい
      • Stateのコードを読めば他の状態への遷移の条件がわかる
  • 短所
    • 全体の状態遷移の見通しが悪い
      • 全体の状態遷移を把握したければ、すべての状態のコードを読まないといけない

Monsterが状態管理する方法

ステートは自身がどのような状態に遷移するかは全く知らず、Monsterが全体の状態遷移を管理する。
これに基づいて実装すると以下のようになる。

func (m *Monster) ChangeState(hour int) {
	if hour >= 23 || hour < 7 {
		m.state = &Sleeping{}
	} else {
		m.state = &Healthy{}
	}
	fmt.Println("現在のstate: ", m.state.CurrentState())
}

type State interface {
	Fight()
	Retreat()
	CurrentState() string
}

func (h *Healthy) CurrentState() string {
	return "Healthy"
}

func (h *Sleeping) CurrentState() string {
	return "Sleeping"
}

func main() {
	m := Monster{state: &Healthy{}}
	for {
		for hour := 0; hour < 24; hour++ {
			fmt.Printf("今は%d時00分\n", hour)
			m.ChangeState(hour)
			m.state.Fight()
			m.state.Retreat()
			time.Sleep(1 * time.Second)
		}
	}
}

すべてのコードは以下
https://go.dev/play/p/TlhtvDW_yf_R

Monsterが状態を管理する場合の長所・短所は以下の通り。

  • 長所
    • 全体の状態遷移の見通しがいい
      • Monsterのコードを読めばすべての状態遷移の条件がわかる
  • 短所
    • MonsterがすべてのStateを知らないといけない

まとめ

  • 状態遷移の管理はState自身が行うパターンと中央集権的に行うパターンがある
    • どちらのパターンでも、Stateが実装すべき振る舞いはインターフェースとして定義されているため、Stateの追加は楽(必要なメソッドが実装されなければコンパイルで気付ける)。
    • どちらのパターンでも、Stateインターフェース自体のメソッドを追加するとなったらとても大変

Discussion