設計模式 in Go: State

行爲模式旨在解決對象間通信和交互相關的問題,專注於定義那些複雜到無法靜態設計的協作策略,這些協作策略使得程序可以在運行時動態地進行職責派遣以實現更好的擴展。

今天我們開始第 4 個行爲模式的學習 —— State Pattern(狀態模式)。

問題背景:

當一個對象的行爲需要根據其內部狀態的變化而變化時,通常的做法是使用 if-else 語句來處理不同的情況。然而,隨着狀態和行爲數量的增加,這種方法變得繁瑣,並導致代碼難以維護和擴展。

添加大量的 if-else 語句會導致代碼難以閱讀、理解和修改。這違反了開閉原則(Open-Closed Principle),因爲每個新的狀態或行爲都需要修改現有的代碼,可能會引入錯誤並使代碼基礎更加脆弱。

狀態模式允許對象通過改變其內部狀態動態地更改行爲,而無需修改其類或修改使用該對象的代碼。

解決方案:

狀態模式提供了一個更加優雅和易於維護的解決方案。通過將每個狀態的行爲封裝在單獨的狀態對象中,該模式消除了對大量 if-else 語句的需求。相反,上下文對象將行爲委託給當前的狀態對象,該狀態對象封裝了與該狀態相關的行爲。

這種方法遵循單一職責原則(Single Responsibility Principle),因爲每個狀態對象只負責自己的行爲。此外,它還促進了上下文與狀態對象之間的松耦合,因爲上下文只需要瞭解狀態接口,而不需要關心具體的實現細節。

狀態模式相比使用if-else語句提供了多項優勢:

通過使用狀態模式,代碼庫變得更加靈活、模塊化和易於維護。它促進了更好的職責分離,並允許更容易地添加新的狀態或修改現有的狀態。總體而言,與使用大量的 if-else 語句相比,狀態模式提供了一個更乾淨和更具擴展性的解決方案。

狀態模式主要包括兩個主要組件:

通過這兩個組件的協同工作,狀態模式使得對象可以根據其內部狀態的變化動態地改變行爲,而無需修改代碼或增加複雜的條件判斷語句。

上下文對象可以通過設置新的狀態對象來改變其內部狀態。這使得上下文可以根據當前狀態動態地更改其行爲。上下文將方法的執行委託給當前的狀態對象,該狀態對象封裝了與該狀態相關的行爲。通過這種方式,上下文可以在運行時根據不同的條件靈活地切換狀態,從而保持代碼的清晰和模塊化。

這是一個模擬自動售貨機工作方式的例子: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) 方法控制:

狀態模式的變體:

根據系統的具體需求,可以使用幾種狀態模式的變體:

這裏有一些 Golang 中的 FSM 庫:

與其他模式關係:

狀態模式可以與以下其他設計模式相關聯:

這些組合可以根據具體需求提供更靈活且可擴展的設計解決方案。

本文總結:

State Pattern(狀態模式)允許對象根據其內部狀態地改變,動態地改變行爲。它包括一個上下文(Context)對象,該對象維護對當前狀態對象的引用,並將行爲委託給它。每個狀態對象封裝了與其特定狀態相關的行爲。通過這種設計,當需要添加新的狀態時,可以不必或少許修改現有代碼,從而提供了靈活性和可擴展性。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/gp1vSjPuWWOTM8XnnDrhQA