Go設計模式(22)-狀態模式

狀態模式使用的相對較少,主要是因為會引入大量的狀態類,導致程式碼比較難維護。但是合適的場景使用狀態模式,可以把複雜的判斷邏輯簡化。

UML類圖位置:https://www。processon。com/view/link/60d29bf3e401fd49502afd25

本文程式碼連結為:https://github。com/shidawuhen/asap/blob/master/controller/design/22status。go

1。定義

1。1狀態模式

狀態模式

:當一個物件的內在狀態改變時允許改變其行為,這個物件看起來像是改變了其類。

UML

Go設計模式(22)-狀態模式

1。2分析

狀態機有3個組成部分:狀態(State)、事件(Event)、動作(Action)。事件觸發狀態的轉移及動作的執行。

只看定義和UML,可能比較難理解使用狀態模式有什麼好處,舉個例子就清晰了。

假設有四種狀態A、B、C、D,同時有四種觸發事件E1、E2、E3、E4,如果不使用狀態模式,寫出來的樣子是這樣的:

func E1() {   if status == “A” {      //狀態遷移+動作執行   } else if status == “B” {      //狀態遷移+動作執行   } else if status == “C” {      //狀態遷移+動作執行   } else if status == “D” {      //狀態遷移+動作執行   }}func E2() {   if status == “A” {      //狀態遷移+動作執行   } else if status == “B” {      //狀態遷移+動作執行   } else if status == “C” {      //狀態遷移+動作執行   } else if status == “D” {      //狀態遷移+動作執行   }}

單看虛擬碼可能覺得還好,但是細想一想,如果動作執行比較複雜,程式碼是不是就很醜了。後期如果狀態或者事件變更,如何確保每一處都進行了更改?這時候狀態模式便起作用了。

我們建立四個類,如UML中的ConcreteStateA、ConcreteStateB、ConcreteStateC、ConcreteStateD,分別代表四種狀態。每個狀態類中有4個Handle函式,分別對應4個事件。透過這種方式,將糅雜在一起的邏輯進行了拆分,程式碼看起來優雅了很多。

2。應用場景

這麼多年都沒有用過狀態模式,使用場景確實有點少。

實際業務場景中做過跨境履約單的狀態機,履約單需要經歷接單、清關中、清關成功、發貨等狀態。這種場景相對簡單,狀態只能單方向流轉、單獨的介面觸發指定狀態流轉到下一個狀態,複雜的部分在於下一個狀態可能有多個,有的可以跳過。這種情況下,使用陣列維護狀態機,比狀態模式要好。

如果狀態多、動作執行邏輯複雜,那使用狀態模式還是挺合理的,一般遊戲中使用狀態模式相對多一些。本次借用《設計模式之美》裡超級馬里奧的例子,使用超級馬里奧介紹實在是太合適了,一是因為馬里奧有多種狀態、多種觸發事件,特別適合使用狀態模式;二是超級馬里奧大家都玩過,業務情況大家都熟悉。為了幫助大家回憶,我找了馬里奧全系列變身形態https://zhuanlan。zhihu。com/p/250931383。

3。程式碼實現

Go設計模式(22)-狀態模式

馬里奧狀態有小馬里奧(Small Mario)、超級馬里奧(Super Mario)、斗篷馬里奧(Cape Mario),小馬里奧吃了蘑菇變為超級馬里奧,小馬里奧和超級馬里奧獲得斗篷變成斗篷馬里奧,超級馬里奧和斗篷馬里奧碰到怪物變成小馬里奧。

package mainimport “fmt”type Mario struct { score  int64 status MarioStatus}type MarioStatus interface { Name() ObtainMushroom() ObtainCape() MeetMonster() SetMario(mario *Mario)}/** * @Author: Jason Pang * @Description: 小馬里奧 */type SmallMarioStatus struct { mario *Mario}/** * @Author: Jason Pang * @Description: 設定馬里奧 * @receiver s * @param mario */func (s *SmallMarioStatus) SetMario(mario *Mario) { s。mario = mario}func (s *SmallMarioStatus) Name() { fmt。Println(“小馬里奧”)}/** * @Author: Jason Pang * @Description: 獲得蘑菇變為超級馬里奧 * @receiver s */func (s *SmallMarioStatus) ObtainMushroom() { s。mario。status = &SuperMarioStatus{ mario: s。mario, } s。mario。score += 100}/** * @Author: Jason Pang * @Description: 獲得斗篷變為斗篷馬里奧 * @receiver s */func (s *SmallMarioStatus) ObtainCape() { s。mario。status = &CapeMarioStatus{ mario: s。mario, } s。mario。score += 200}/** * @Author: Jason Pang * @Description: 遇到怪獸減100 * @receiver s */func (s *SmallMarioStatus) MeetMonster() { s。mario。score -= 100}/** * @Author: Jason Pang * @Description: 超級馬里奧 */type SuperMarioStatus struct { mario *Mario}/** * @Author: Jason Pang * @Description: 設定馬里奧 * @receiver s * @param mario */func (s *SuperMarioStatus) SetMario(mario *Mario) { s。mario = mario}func (s *SuperMarioStatus) Name() { fmt。Println(“超級馬里奧”)}/** * @Author: Jason Pang * @Description: 獲得蘑菇無變化 * @receiver s */func (s *SuperMarioStatus) ObtainMushroom() {}/** * @Author: Jason Pang * @Description:獲得斗篷變為斗篷馬里奧 * @receiver s */func (s *SuperMarioStatus) ObtainCape() { s。mario。status = &CapeMarioStatus{ mario: s。mario, } s。mario。score += 200}/** * @Author: Jason Pang * @Description: 遇到怪獸變為小馬里奧 * @receiver s */func (s *SuperMarioStatus) MeetMonster() { s。mario。status = &SmallMarioStatus{ mario: s。mario, } s。mario。score -= 200}/** * @Author: Jason Pang * @Description: 斗篷馬里奧 */type CapeMarioStatus struct { mario *Mario}/** * @Author: Jason Pang * @Description: 設定馬里奧 * @receiver s * @param mario */func (c *CapeMarioStatus) SetMario(mario *Mario) { c。mario = mario}func (c *CapeMarioStatus) Name() { fmt。Println(“斗篷馬里奧”)}/** * @Author: Jason Pang * @Description:獲得蘑菇無變化 * @receiver c */func (c *CapeMarioStatus) ObtainMushroom() {}/** * @Author: Jason Pang * @Description: 獲得斗篷無變化 * @receiver c */func (c *CapeMarioStatus) ObtainCape() {}/** * @Author: Jason Pang * @Description: 遇到怪獸變為小馬里奧 * @receiver c */func (c *CapeMarioStatus) MeetMonster() { c。mario。status = &SmallMarioStatus{ mario: c。mario, } c。mario。score -= 200}func main() { mario := Mario{ status: &SmallMarioStatus{}, score:  0, } mario。status。SetMario(&mario) mario。status。Name() fmt。Println(“——————————-獲得蘑菇\n”) mario。status。ObtainMushroom() mario。status。Name() fmt。Println(“——————————-獲得斗篷\n”) mario。status。ObtainCape() mario。status。Name() fmt。Println(“——————————-遇到怪獸\n”) mario。status。MeetMonster() mario。status。Name()}

輸出:

➜ myproject go run main。go

小馬里奧

——————————-獲得蘑菇

超級馬里奧

——————————-獲得斗篷

斗篷馬里奧

——————————-遇到怪獸

小馬里奧

總結

仔細看上面的程式碼

對事件觸發狀態的轉移及動作的執行的改動會很簡單

可快速增加新的事件

增加新的狀態也方便,只需新增新的狀態類,少量修改已有程式碼

壞處就是類特別多,類裡的函式也會特別多,即使這些函式根本無用。不過能獲得更好的擴充套件性,還是值得的。

最後

大家如果喜歡我的文章,可以關注我的公眾號(程式設計師麻辣燙)

我的個人部落格為:https://shidawuhen。github。io/

往期文章回顧:

設計模式

招聘

思考

儲存

算法系列

讀書筆記

小工具

架構

網路

Go語言