在好久之前有寫過一篇文章上介紹 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 介面,以故事來說就是不同的人格 |
我們來看一下套用 State 設計模式後的調整吧~
建立 Context interface
1 2 3 4 5 6 7
| 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
| 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
| 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
| 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}) }
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}) }
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] }
type Character interface { eating() gatherWithFriends() sleeping() }
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}) }
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}) }
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 } }
type Boy interface { eating() gatherWithFriends() sleeping() changedCharacter(c Character) }
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{}
tomBoy.character = &OutgoingCharacter{boy: tomBoy} tomBoy.eating() tomBoy.gatherWithFriends() tomBoy.sleeping()
tomBoy.eating() tomBoy.gatherWithFriends() tomBoy.sleeping()
tomBoy.eating() tomBoy.gatherWithFriends() tomBoy.sleeping()
tomBoy.eating() tomBoy.gatherWithFriends() tomBoy.sleeping()
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
| 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 *TomBoy) eating() { m.character.eating() }
|
如果有新的人格(狀態)要增加的話,我們只需要在新增一個有實作 Character
介面的人格(狀態),在需要切換的時候執行 changedCharacter
替換就可以執行了,就不用在調整 Tom 類別內的程式碼囉!
比起在一堆 if
的情況想辦法再新增一個 if
去增加判斷,我想使用 State 設計模式的方式更好維護 🙂
State 設計模式優缺點
優點
- 🎉 單一職責原則,把各自不同的 State 放在各自的類別裡
- 🎉 減少在 Context 裡狀態條件的複雜的判斷
- 🎉 封閉開放原則,有新的 State 加入也不需要在調整到 Context 物件
缺點
- 😬 如果狀態不多或邏輯不複雜,容易變成過度設計
- 😬 因爲會需要把 State 拆成獨立的類別,檔案數量會變多
State 設計模式使用時機
如果一個物件會因爲不同的狀態表現出不同的行爲,這些狀態彼此又有相依(知道彼此)的狀況,那也許就會是適合使用 State 設計模式的好時機。
總結
以上就是 State 設計模式介紹,上述的程式碼是用 golang 完成的,如果需要其他語言的範例,歡迎參考 Refactoring Guru - State。
最後感謝你的收看!🎉 🎉 🎉
參考: