設計模式 in Go: State
行爲模式旨在解決對象間通信和交互相關的問題,專注於定義那些複雜到無法靜態設計的協作策略,這些協作策略使得程序可以在運行時動態地進行職責派遣以實現更好的擴展。
今天我們開始第 4 個行爲模式的學習 —— State Pattern(狀態模式)。
問題背景:
當一個對象的行爲需要根據其內部狀態的變化而變化時,通常的做法是使用 if-else 語句來處理不同的情況。然而,隨着狀態和行爲數量的增加,這種方法變得繁瑣,並導致代碼難以維護和擴展。
添加大量的 if-else
語句會導致代碼難以閱讀、理解和修改。這違反了開閉原則(Open-Closed Principle),因爲每個新的狀態或行爲都需要修改現有的代碼,可能會引入錯誤並使代碼基礎更加脆弱。
狀態模式允許對象通過改變其內部狀態動態地更改行爲,而無需修改其類或修改使用該對象的代碼。
解決方案:
狀態模式提供了一個更加優雅和易於維護的解決方案。通過將每個狀態的行爲封裝在單獨的狀態對象中,該模式消除了對大量 if-else
語句的需求。相反,上下文對象將行爲委託給當前的狀態對象,該狀態對象封裝了與該狀態相關的行爲。
這種方法遵循單一職責原則(Single Responsibility Principle),因爲每個狀態對象只負責自己的行爲。此外,它還促進了上下文與狀態對象之間的松耦合,因爲上下文只需要瞭解狀態接口,而不需要關心具體的實現細節。
狀態模式相比使用if-else
語句提供了多項優勢:
-
提高可讀性:通過將每個狀態的行爲分離到自己的類中,使代碼更易於閱讀和理解。這改善了代碼組織,並使得更容易理解每個狀態的邏輯。
-
增強可維護性:添加新狀態或修改現有狀態變得更加容易。可以通過創建新的狀態類來添加新狀態,而無需修改現有的代碼。這促進了代碼重用,並使系統更加易於維護和擴展。
-
減少複雜性:通過消除對複雜
if-else
語句的需求來簡化代碼。每個狀態的行爲都被封裝在其自己的類中,從而使代碼庫更模塊化且更容易管理。 -
提高可測試性:通過允許獨立地測試每個狀態的行爲來促進單元測試。這種行爲的隔離使得爲每個狀態編寫有針對性和集中的測試變得更加容易。
通過使用狀態模式,代碼庫變得更加靈活、模塊化和易於維護。它促進了更好的職責分離,並允許更容易地添加新的狀態或修改現有的狀態。總體而言,與使用大量的 if-else 語句相比,狀態模式提供了一個更乾淨和更具擴展性的解決方案。
狀態模式主要包括兩個主要組件:
-
上下文(Context):它維護對當前狀態對象的引用,並將行爲委託給該狀態對象。
-
狀態(State):狀態接口定義了一組方法,封裝了與每個狀態相關的行爲。每個具體的 State 類實現這些方法,以提供特定狀態對應的行爲。
通過這兩個組件的協同工作,狀態模式使得對象可以根據其內部狀態的變化動態地改變行爲,而無需修改代碼或增加複雜的條件判斷語句。
上下文對象可以通過設置新的狀態對象來改變其內部狀態。這使得上下文可以根據當前狀態動態地更改其行爲。上下文將方法的執行委託給當前的狀態對象,該狀態對象封裝了與該狀態相關的行爲。通過這種方式,上下文可以在運行時根據不同的條件靈活地切換狀態,從而保持代碼的清晰和模塊化。
這是一個模擬自動售貨機工作方式的例子:1)我們選擇一臺閒置的自動售貨機,2)我們投入硬幣,3)選擇產品並等待商品發放。VendingMachine 包含一個指向 State 對象的引用,該引用可以被 VendingMachine 更改。每個 State 對象定義自己的行爲。
示例代碼:
package state
import"fmt"
// State represents the interface for different states of the vending machine
type State interface {
InsertCoin()
SelectItem()
DispenseItem()
}
// VendingMachine represents the context object that maintains the current state
type VendingMachine struct {
currentState State
}
func(v *VendingMachine) SetState(state State) {
v.currentState = state
}
func(v *VendingMachine) InsertCoin() {
v.currentState.InsertCoin()
}
func(v *VendingMachine) SelectItem() {
v.currentState.SelectItem()
}
func(v *VendingMachine) DispenseItem() {
v.currentState.DispenseItem()
}
// NoSelectionState represents the state when no item is selected
type NoSelectionState struct{}
func(n *NoSelectionState) InsertCoin() {
fmt.Println("✅ Coin inserted.")
}
func(n *NoSelectionState) SelectItem() {
fmt.Println("✅ Please select an item.")
}
func(n *NoSelectionState) DispenseItem() {
fmt.Println("❌ No item selected.")
}
// HasSelectionState represents the state when an item is selected
type HasSelectionState struct{}
func(h *HasSelectionState) InsertCoin() {
fmt.Println("❌ Coin already inserted.")
}
func(h *HasSelectionState) SelectItem() {
fmt.Println("❌ Item already selected.")
}
func(h *HasSelectionState) DispenseItem() {
fmt.Println("✅ Item dispensed.")
}
// SoldState represents the state when an item is sold
type SoldStatestruct{}
func(s *SoldState) InsertCoin() {
fmt.Println("❌ Please wait, item being dispensed.")
}
func(s *SoldState) SelectItem() {
fmt.Println("❌ Item already selected and dispensed.")
}
func(s *SoldState) DispenseItem() {
fmt.Println("❌ Item already dispensed.")
}
運行測試演示下:
package state_test
import (
"fmt"
"state"
"testing"
)
funcTestState(t *testing.T) {
vendingMachine := &state.VendingMachine{}
noSelectionState := &state.NoSelectionState{}
hasSelectionState := &state.HasSelectionState{}
soldState := &state.SoldState{}
fmt.Println("------------ CurrentState: NoSelection ---------------------")
vendingMachine.SetState(noSelectionState)
vendingMachine.InsertCoin()// Output: Coin inserted.
vendingMachine.SelectItem()// Output: Please select an item.
vendingMachine.DispenseItem()// Output: No item selected.
fmt.Println("------------ CurrentState: HasSelection --------------------")
vendingMachine.SetState(hasSelectionState)
vendingMachine.InsertCoin()// Output: Coin inserted.
vendingMachine.SelectItem()// Output: Item already selected.
vendingMachine.DispenseItem()// Output: Item dispensed.
fmt.Println("------------ CurrentState: HasSoldState --------------------")
vendingMachine.SetState(soldState)
vendingMachine.InsertCoin()// Output: Please wait, item being dispensed.
vendingMachine.SelectItem()// Output: Item already dispensed.
vendingMachine.DispenseItem()// Output: Item already dispensed.
}
Running tool: /opt/go/bin/go test -timeout 30s -run ^TestCommand$ command -v -count=1 -timeout=1m
=== RUN TestCommand
press [save] button
the receiver `f` which will do the `save` action
receiver save the file
press [close] button
the receiver `f` which will do the `close` action
receiver close the file
press [save] shortcut
the receiver `f` which will do the `save` action
receiver save the file
--- PASS: TestCommand (0.00s)
PASS
ok command 0.001s
ps:此處我們優先考慮可讀性,不會太關注編碼標準,如註釋、camelCase 類型名等。我們將多個文件的代碼組織到一個 codeblock 中僅僅是爲了方便閱讀,如果您想測試可以通過 git 下載源碼 github.com/hitzhangjie/go-patterns。
在這個例子中,自動售貨機通過外部調用以下函數過來改變其狀態,vendingMachine.SetState(state) 。實際上,狀態模式不要求狀態轉換必須由上下文(Context)的 Context.SetState(nextState) 方法控制:
-
可以通過 Context 的 SetState(nextState) 方法進行控制;
-
或者可以在具體的狀態類的方法中控制狀態轉換,例如,在 noSelectionState.SelectItem(…) 返回之前執行 vendingMachine.State = hasSelectionState。
狀態模式的變體:
根據系統的具體需求,可以使用幾種狀態模式的變體:
-
上下文驅動的狀態轉換: 在這種變體中,Context 對象基於其內部邏輯或外部事件來驅動狀態轉換。它會根據某些條件或觸發器決定何時切換到新的狀態。
-
狀態驅動的狀態轉換: 在這種變體中,State 對象自身確定狀態轉換。每個 State 對象知道在接收到當前輸入時應過渡到下一個哪個狀態。
-
層次化狀態機: 在複雜的系統中,可以使用層次化狀態機來表示狀態和狀態轉換。這允許基於對象的內部狀態對行爲進行更細粒度的控制。
這裏有一些 Golang 中的 FSM 庫:
-
https://github.com/qmuntal/stateless
-
https://github.com/looplab/fsm
與其他模式關係:
狀態模式可以與以下其他設計模式相關聯:
-
策略模式(Strategy Pattern):狀態模式類似於策略模式,兩者都涉及封裝行爲並允許其動態變化。然而,狀態模式專注於根據內部狀態封裝行爲,而策略模式則側重於封裝可互換的算法。
-
狀態模式和單例模式(Singleton Pattern):在某些情況下,可以將狀態模式與單例模式結合使用,以確保每個狀態對象只有一個實例存在。當狀態對象沒有特定的狀態數據並且可以在多個上下文對象之間共享時,這會非常有用。
-
狀態模式和裝飾器模式(Decorator Pattern):狀態模式可以與裝飾器模式結合使用,根據當前狀態向 Context 對象添加額外的行爲或功能。裝飾器可以包裝 Context 對象並動態地修改其行爲。
這些組合可以根據具體需求提供更靈活且可擴展的設計解決方案。
本文總結:
State Pattern(狀態模式)允許對象根據其內部狀態地改變,動態地改變行爲。它包括一個上下文(Context)對象,該對象維護對當前狀態對象的引用,並將行爲委託給它。每個狀態對象封裝了與其特定狀態相關的行爲。通過這種設計,當需要添加新的狀態時,可以不必或少許修改現有代碼,從而提供了靈活性和可擴展性。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/gp1vSjPuWWOTM8XnnDrhQA