0%

State 設計模式

在好久之前有寫過一篇文章上介紹 Strategy 設計模式的文章,但那時候還不清楚它與 State 設計模式有什麼不同。

它們其實很像,但根據 Refactoring Guru 的解釋,Strategy 與 State 最大的差別是在與:

  • Strategy 彼此之間的策略對象是完全獨立,它們並不會知道彼此存在 ,就像是在 Strategy 設計模式 大衛的女朋友們
  • State 沒有限制這些狀態對象之間的依賴關係

State 設計模式是什麼?

State 設計模式是一種行爲設計模式,只要內部的狀態改變時,就會改變外部的行爲

沒有使用 State 設計模式會怎麼樣嗎? 🤯

這次一樣用個故事來瞭解 State 設計模式的使用方式,這次要講的是人格分裂的湯姆的故事。

湯姆角色設定
湯姆從小發現自己擁有三個人格,分別是:

  • 外向人格
  • 自閉人格
  • 暴力人格

因此不同人格的特質就會顯示在湯姆的這幾個行爲上:

  • 吃飯
  • 跟朋友聚會
  • 睡覺

對了,這三個人格會在湯姆睡著時輪班。

人格彼此間的私怨

  • 「外向人格」討厭「自閉人格」,因此只會把控制權交給「暴力人格」
  • 「自閉人格」也討厭「外向人格」,因此只會把控制權交給「暴力人格」
  • 「暴力人格」跟「外向人格」、「內向人格」都沒有恩怨,所以輪班的時候就會隨機把控制權跟任一個

三種人格行爲
外向人格的行爲:

  • 吃飯:去 Buffet ~ 🍕
  • 跟朋友聚會:跟一大群朋友開心聊天到時間
  • 睡覺:輪班:明天換暴力傾向人格上班了

自閉人格的行爲:

  • 吃飯:我都沒有朋友,吃什麼,餓死算了。。😮‍💨
  • 跟朋友聚會:朋友是什麼?我想空氣就是我最好的朋友。。。
  • 睡覺:輪班:恩。。明天換暴力傾向人格了。。。

暴力人格的行爲:

  • 吃飯:吃飯總是吃到翻桌!🤬
  • 跟朋友聚會:沒跟朋友打起來才叫人意外!
  • 睡覺:隨便選一個啦!

沒有 State 設計模式的程式碼

以下用 golang 作爲範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package main

import (
"fmt"
"math/rand"
"time"
)

const (
OutgoingCharacterType = iota
ASDCharacterType
ViolenceTendencyCharacterType
)

type Tom struct {
character int
}

func (m *Tom) eating() {
// 如果外向人格
if m.character == OutgoingCharacterType {
fmt.Println("外向人格 - 吃飯:去 Buffet ~")
}
// 如果是自閉人格
if m.character == ASDCharacterType {
fmt.Println("自閉人格 - 吃飯:我都沒有朋友,吃什麼,餓死算了。。")
}
// 如果是暴力人格
if m.character == ViolenceTendencyCharacterType {
fmt.Println("暴力傾向人格 - 吃飯:吃飯總是吃到翻桌!")
}
}

func (m *Tom) gatherWithFriends() {
// 如果外向人格
if m.character == OutgoingCharacterType {
fmt.Println("外向人格 - 跟朋友相聚:跟一大群朋友開心聊天到時間")
}
// 如果是自閉人格
if m.character == ASDCharacterType {
fmt.Println("自閉人格 - 跟朋友相聚:朋友是什麼?我想空氣就是我最好的朋友。。。")
}
// 如果是暴力人格
if m.character == ViolenceTendencyCharacterType {
fmt.Println("暴力傾向人格 - 跟朋友相聚:沒跟朋友打起來才叫人意外!")
}
}

func (m *Tom) sleeping() {
// 如果外向人格
if m.character == OutgoingCharacterType {
fmt.Println("外向人格 - 輪班:明天換暴力傾向人格上班了")
m.changedCharacter(ViolenceTendencyCharacterType)
return
}
// 如果是自閉人格
if m.character == ASDCharacterType {
fmt.Println("自閉人格 - 輪班:恩。。明天換暴力傾向人格了。。。")
m.changedCharacter(ViolenceTendencyCharacterType)
return
}
// 如果是暴力人格
if m.character == ViolenceTendencyCharacterType {
// 隨便選出下一個
characters := []int{OutgoingCharacterType, ASDCharacterType}
rand.Seed(time.Now().UnixNano())
i := rand.Intn(2)
next := characters[i]
if next == OutgoingCharacterType {
fmt.Println("暴力傾向人格 - 輪班:明天居然是外向人格,嘖嘖")
m.changedCharacter(next)
return
}

if next == ASDCharacterType {
fmt.Println("暴力傾向人格 - 輪班:哼,明天居然是那個懦弱的自閉人格!嘖嘖!")
m.changedCharacter(next)
return
}
}
}

func (m *Tom) changedCharacter(c int) {
m.character = c
}

func main() {
tom := &Tom{character: OutgoingCharacterType}
fmt.Println("第 1 天")
tom.eating()
tom.gatherWithFriends()
tom.sleeping()
fmt.Println("第 2 天")
tom.eating()
tom.gatherWithFriends()
tom.sleeping()
fmt.Println("第 3 天")
tom.eating()
tom.gatherWithFriends()
tom.sleeping()
fmt.Println("第 4 天")
tom.eating()
tom.gatherWithFriends()
tom.sleeping()
fmt.Println("第 5 天")
tom.eating()
tom.gatherWithFriends()
tom.sleeping()
}

1
外向人格 - 吃飯:去 Buffet ~
外向人格 - 跟朋友相聚:跟一大群朋友開心聊天到時間
外向人格 - 輪班:明天換暴力傾向人格上班了
2
暴力傾向人格 - 吃飯:吃飯總是吃到翻桌!
暴力傾向人格 - 跟朋友相聚:沒跟朋友打起來才叫人意外!
暴力傾向人格 - 輪班:明天居然是外向人格,嘖嘖
3
外向人格 - 吃飯:去 Buffet ~
外向人格 - 跟朋友相聚:跟一大群朋友開心聊天到時間
外向人格 - 輪班:明天換暴力傾向人格上班了
4
暴力傾向人格 - 吃飯:吃飯總是吃到翻桌!
暴力傾向人格 - 跟朋友相聚:沒跟朋友打起來才叫人意外!
暴力傾向人格 - 輪班:哼,明天居然是那個懦弱的自閉人格!嘖嘖!
5
自閉人格 - 吃飯:我都沒有朋友,吃什麼,餓死算了。。
自閉人格 - 跟朋友相聚:朋友是什麼?我想空氣就是我最好的朋友。。。
自閉人格 - 輪班:恩。。明天換暴力傾向人格了。。。
暴力傾向人格 - 輪班:明天居然是外向人格,嘖嘖

問題:判斷式因爲不同的狀態而增加 🥲

上面的問題跟 Strategy 設計模式 差不多,都會因爲狀態而增加。

用這個例子來看,湯姆目前只有分裂成三個人格(狀態),每個行爲的內容又很單純,因此看起來好像還可以接受;然而如果隨著人格(狀態)的增加與行爲變得複雜,那就會很難以維護了 🤯

因此我們就看看看 State 可以怎麼改善這個問題吧!

State Pattern 組成

成員 用途
Context (interface) 一個介面外部持有狀態的載體,可以改變狀態
ConcreteContext 實作 Context 介面,以這個故事來說就是湯姆
State (interface) 一個介面,用來封裝不同狀態所對應的行爲
ConcreteState 實作 State 介面,以故事來說就是不同的人格

UML

我們來看一下套用 State 設計模式後的調整吧~

建立 Context interface

1
2
3
4
5
6
7
// Context interface
type Boy interface {
eating()
gatherWithFriends()
sleeping()
changedCharacter(c Character)
}

建立 Context 實作(湯姆)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Context implementation - Tom
type TomBoy struct {
character Character
day int
}

func (m *TomBoy) eating() {
m.character.eating()
}

func (m *TomBoy) gatherWithFriends() {
m.character.gatherWithFriends()
}

func (m *TomBoy) sleeping() {
m.character.sleeping()
}

func (m *TomBoy) changedCharacter(c Character) {
m.day += 1
m.character = c
fmt.Printf("第 %v 天 ================ \n", m.day)
}

建立 State 的 interface(人格)

1
2
3
4
5
6
// State interface
type Character interface {
eating()
gatherWithFriends()
sleeping()
}

建立 State 的 interface(不同的人格)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// State implementation - 外向
type OutgoingCharacter struct {
boy Boy
}

func (c *OutgoingCharacter) eating() {
fmt.Println("外向人格 - 吃飯:去 Buffet ~")
}

func (c *OutgoingCharacter) gatherWithFriends() {
fmt.Println("外向人格 - 跟朋友相聚:跟一大群朋友開心聊天到時間")
}

func (c *OutgoingCharacter) sleeping() {
fmt.Println("外向人格 - 輪班:明天換暴力傾向人格上班了")
c.boy.changedCharacter(&ViolenceTendencyCharacter{boy: c.boy})
}

// state implementation - 自閉
type ASDCharacter struct {
boy Boy
}

func (c *ASDCharacter) eating() {
fmt.Println("自閉人格 - 吃飯:我都沒有朋友,吃什麼,餓死算了。。")
}

func (c *ASDCharacter) gatherWithFriends() {
fmt.Println("自閉人格 - 跟朋友相聚:朋友是什麼?我想空氣就是我最好的朋友。。。")
}

func (c *ASDCharacter) sleeping() {
fmt.Println("自閉人格 - 輪班:恩。。明天換暴力傾向人格了。。。")
c.boy.changedCharacter(&ViolenceTendencyCharacter{boy: c.boy})
}

// state implementation - 暴力傾向
type ViolenceTendencyCharacter struct {
boy Boy
}

func (c *ViolenceTendencyCharacter) eating() {
fmt.Println("暴力傾向人格 - 吃飯:吃飯總是吃到翻桌!")
}

func (c *ViolenceTendencyCharacter) gatherWithFriends() {
fmt.Println("暴力傾向人格 - 跟朋友相聚:沒跟朋友打起來才叫人意外!")
}

func (c *ViolenceTendencyCharacter) sleeping() {
next := nextCharactor()
if next == OutgoingCharacterType {
fmt.Println("暴力傾向人格 - 輪班:明天居然是外向人格,嘖嘖")
c.boy.changedCharacter(&OutgoingCharacter{boy: c.boy})
return
}

if next == ASDCharacterType {
fmt.Println("暴力傾向人格 - 輪班:哼,居然是那個懦弱的自閉人格,真是看不習慣!")
c.boy.changedCharacter(&ASDCharacter{boy: c.boy})
return
}
}

套用 State 設計模式的完整程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package main

import (
"fmt"
"math/rand"
"time"
)

const (
OutgoingCharacterType = iota
ASDCharacterType
ViolenceTendencyCharacterType
)

func nextCharactor() int {
characters := []int{OutgoingCharacterType, ASDCharacterType}
rand.Seed(time.Now().UnixNano())
i := rand.Intn(2)
return characters[i]
}

// State interface
type Character interface {
eating()
gatherWithFriends()
sleeping()
}

// State implementation - 外向
type OutgoingCharacter struct {
boy Boy
}

func (c *OutgoingCharacter) eating() {
fmt.Println("外向人格 - 吃飯:去 Buffet ~")
}

func (c *OutgoingCharacter) gatherWithFriends() {
fmt.Println("外向人格 - 跟朋友相聚:跟一大群朋友開心聊天到時間")
}

func (c *OutgoingCharacter) sleeping() {
fmt.Println("外向人格 - 輪班:明天換暴力傾向人格上班了")
c.boy.changedCharacter(&ViolenceTendencyCharacter{boy: c.boy})
}

// state implementation - 自閉
type ASDCharacter struct {
boy Boy
}

func (c *ASDCharacter) eating() {
fmt.Println("自閉人格 - 吃飯:我都沒有朋友,吃什麼,餓死算了。。")
}

func (c *ASDCharacter) gatherWithFriends() {
fmt.Println("自閉人格 - 跟朋友相聚:朋友是什麼?我想空氣就是我最好的朋友。。。")
}

func (c *ASDCharacter) sleeping() {
fmt.Println("自閉人格 - 輪班:恩。。明天換暴力傾向人格了。。。")
c.boy.changedCharacter(&ViolenceTendencyCharacter{boy: c.boy})
}

// state implementation - 暴力傾向
type ViolenceTendencyCharacter struct {
boy Boy
}

func (c *ViolenceTendencyCharacter) eating() {
fmt.Println("暴力傾向人格 - 吃飯:吃飯總是吃到翻桌!")
}

func (c *ViolenceTendencyCharacter) gatherWithFriends() {
fmt.Println("暴力傾向人格 - 跟朋友相聚:沒跟朋友打起來才叫人意外!")
}

func (c *ViolenceTendencyCharacter) sleeping() {
next := nextCharactor()
if next == OutgoingCharacterType {
fmt.Println("暴力傾向人格 - 輪班:明天居然是外向人格,嘖嘖")
c.boy.changedCharacter(&OutgoingCharacter{boy: c.boy})
return
}

if next == ASDCharacterType {
fmt.Println("暴力傾向人格 - 輪班:哼,居然是那個懦弱的自閉人格,真是看不習慣!")
c.boy.changedCharacter(&ASDCharacter{boy: c.boy})
return
}
}

// Context interface
type Boy interface {
eating()
gatherWithFriends()
sleeping()
changedCharacter(c Character)
}

// Context implementation - Tom
type TomBoy struct {
character Character
day int
}

func (m *TomBoy) eating() {
m.character.eating()
}

func (m *TomBoy) gatherWithFriends() {
m.character.gatherWithFriends()
}

func (m *TomBoy) sleeping() {
m.character.sleeping()
}

func (m *TomBoy) changedCharacter(c Character) {
m.day += 1
m.character = c
fmt.Printf("第 %v 天 ================ \n", m.day)
}

func main() {

tomBoy := &TomBoy{}

// 第 1 天
tomBoy.character = &OutgoingCharacter{boy: tomBoy} // 從外向人格開始
tomBoy.eating()
tomBoy.gatherWithFriends()
tomBoy.sleeping()

// 第 2 天
tomBoy.eating()
tomBoy.gatherWithFriends()
tomBoy.sleeping()

// 第 3 天
tomBoy.eating()
tomBoy.gatherWithFriends()
tomBoy.sleeping()

// 第 4 天
tomBoy.eating()
tomBoy.gatherWithFriends()
tomBoy.sleeping()

// 第 5 天
tomBoy.eating()
tomBoy.gatherWithFriends()
tomBoy.sleeping()
}

使用 State 設計模式後,我們可以看到在 Tom 的 method 裏,已經去除了針對不同狀態判斷的一大堆 if 的判斷,變成直接呼叫內部的 state 相對應的 method 去執行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Before
func (m *Tom) eating() {
// 如果外向人格
if m.character == OutgoingCharacterType {
fmt.Println("外向人格 - 吃飯:去 Buffet ~")
}
// 如果是自閉人格
if m.character == ASDCharacterType {
fmt.Println("自閉人格 - 吃飯:我都沒有朋友,吃什麼,餓死算了。。")
}
// 如果是暴力人格
if m.character == ViolenceTendencyCharacterType {
fmt.Println("暴力傾向人格 - 吃飯:吃飯總是吃到翻桌!")
}
}

// After
func (m *TomBoy) eating() {
m.character.eating()
}

如果有新的人格(狀態)要增加的話,我們只需要在新增一個有實作 Character 介面的人格(狀態),在需要切換的時候執行 changedCharacter 替換就可以執行了,就不用在調整 Tom 類別內的程式碼囉!

比起在一堆 if 的情況想辦法再新增一個 if 去增加判斷,我想使用 State 設計模式的方式更好維護 🙂

State 設計模式優缺點

優點

  • 🎉 單一職責原則,把各自不同的 State 放在各自的類別裡
  • 🎉 減少在 Context 裡狀態條件的複雜的判斷
  • 🎉 封閉開放原則,有新的 State 加入也不需要在調整到 Context 物件

缺點

  • 😬 如果狀態不多或邏輯不複雜,容易變成過度設計
  • 😬 因爲會需要把 State 拆成獨立的類別,檔案數量會變多

State 設計模式使用時機

如果一個物件會因爲不同的狀態表現出不同的行爲,這些狀態彼此又有相依(知道彼此)的狀況,那也許就會是適合使用 State 設計模式的好時機。

總結

以上就是 State 設計模式介紹,上述的程式碼是用 golang 完成的,如果需要其他語言的範例,歡迎參考 Refactoring Guru - State
最後感謝你的收看!🎉 🎉 🎉

參考: