🐥
Stateパターンの整理
概要
状態に応じてオブジェクトの振る舞いを変えるパターン。
課題
例えばポケモンが健康状態のときに「たたかう」を選ぶと技を選べるが、眠っている状態だと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
すべてのコードは以下
State自体が状態を管理する場合の長所・短所は以下の通り。
- 長所
- 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)
}
}
}
すべてのコードは以下
Monsterが状態を管理する場合の長所・短所は以下の通り。
- 長所
- 全体の状態遷移の見通しがいい
- Monsterのコードを読めばすべての状態遷移の条件がわかる
- 全体の状態遷移の見通しがいい
- 短所
- MonsterがすべてのStateを知らないといけない
まとめ
- 状態遷移の管理はState自身が行うパターンと中央集権的に行うパターンがある
- どちらのパターンでも、Stateが実装すべき振る舞いはインターフェースとして定義されているため、Stateの追加は楽(必要なメソッドが実装されなければコンパイルで気付ける)。
- どちらのパターンでも、Stateインターフェース自体のメソッドを追加するとなったらとても大変
Discussion