GoLang設計模式14 - 狀態模式

robin·張發表於2021-11-13

狀態模式,顧名思義,是一種基於有限狀態機制的設計模式。在這種設計模式中,行為是由相應的狀態來決定的。接下來我們會用一個售賣機的例子來說明下狀態模式。為了便於說明,我們把場景簡化一下,假設有一臺售賣機只賣一種商品,且只有如下四種狀態:

  1. 有商品
  2. 無商品
  3. 商品請求中
  4. 已收款

一臺售賣機也應該會有多種功能,我們同樣做一下簡化,假設這臺售賣機只有四個功能:

  1. 選擇商品
  2. 補充商品
  3. 投幣
  4. 吐出商品

什麼時候使用狀態模式

  • 在一個物件有多種不同狀態的時候。物件需要根據當前的請求來改變它的狀態
    • 在前面提到的例子中,售賣機將會受到行為的影響從一種狀態切換到另一種狀態。比如,當“投幣”行為結束後,售貨機將會從“商品請求中”狀態切換到“已收款”狀態。
  • 在一個物件需要根據它當前的狀態對同一個請求做出不同響應的時候。這個時候使用狀態模式可以避免大量的條件宣告。
    • 仍然以售賣機為例,當使用者想購買商品時,如果售賣機的狀態為“有商品”,它就會繼續處理,如果售賣機狀態為“無商品”,它就會拒絕處理。請注意,這裡售賣機根據它“有商品”和“無商品”的狀態,對購買商品的請求作出了兩種不同的響應。

UML類圖

類圖如下:

程式碼

看下程式碼:

state.go:

type state interface {
	addItem(int) error

	requestItem() error

	insertMoney(money int) error

	dispenseItem() error
}

這裡簡單解釋下:

在程式碼中我們定義了一個State介面,這個介面中有四個函式分別表示了售賣機的四種行為,如下:

  1. 購買商品:addItem(int) error
  2. 請求商品:requestItem() error
  3. 投幣:insertMoney(money int) error
  4. 吐出商品:dispenseItem() error

每個具體的狀態實現都實現了以上四個函式,並對每種行為發生時該切換到哪種狀態,以及如何響應做了處理

每個具體的狀態也都嵌入了一個指向當前售賣機的指標,這樣以確保狀態的切換是發生在這臺售賣機上。

vendingMachine.go:

import "fmt"

type vendingMachine struct {
	hasItem       state
	itemRequested state
	hasMoney      state
	noItem        state

	currentState state

	itemCount int
	itemPrice int
}

func (v *vendingMachine) requestItem() error {
	return v.currentState.requestItem()
}

func (v *vendingMachine) addItem(count int) error {
	return v.currentState.addItem(count)
}

func (v *vendingMachine) insertMoney(money int) error {
	return v.currentState.insertMoney(money)
}

func (v *vendingMachine) dispenseItem() error {
	return v.currentState.dispenseItem()
}

func (v *vendingMachine) setState(s state) {
	v.currentState = s
}

func (v *vendingMachine) incrementItemCount(count int) {
	fmt.Printf("Adding %d items\n", count)
	v.itemCount = v.itemCount + count
}

注意這段程式碼,這裡面沒有任何條件表示式,所有邏輯處理均由相應的狀態實現完成。

下面是具體的狀態實現。

hasItemState.go:

import "fmt"

type hasItemState struct {
	vendingMachine *vendingMachine
}

func (i *hasItemState) requestItem() error {
	if i.vendingMachine.itemCount == 0 {
		i.vendingMachine.setState(i.vendingMachine.noItem)
		return fmt.Errorf("No item present")
	}
	fmt.Printf("Item requestd\n")
	i.vendingMachine.setState(i.vendingMachine.itemRequested)
	return nil
}

func (i *hasItemState) addItem(count int) error {
	fmt.Printf("%d items added\n", count)
	i.vendingMachine.incrementItemCount(count)
	return nil
}

func (i *hasItemState) insertMoney(money int) error {
	return fmt.Errorf("Please select item first")
}
func (i *hasItemState) dispenseItem() error {
	return fmt.Errorf("Please select item first")
}

hasMoneyState.go:

import "fmt"

type hasMoneyState struct {
	vendingMachine *vendingMachine
}

func (i *hasMoneyState) requestItem() error {
	return fmt.Errorf("Item dispense in progress")
}

func (i *hasMoneyState) addItem(count int) error {
	return fmt.Errorf("Item dispense in progress")
}

func (i *hasMoneyState) insertMoney(money int) error {
	return fmt.Errorf("Item out of stock")
}

func (i *hasMoneyState) dispenseItem() error {
	fmt.Println("Dispensing Item")
	i.vendingMachine.itemCount = i.vendingMachine.itemCount - 1
	if i.vendingMachine.itemCount == 0 {
		i.vendingMachine.setState(i.vendingMachine.noItem)
	} else {
		i.vendingMachine.setState(i.vendingMachine.hasItem)
	}
	return nil
}

itemRequestedState.go:

import "fmt"

type itemRequestedState struct {
	vendingMachine *vendingMachine
}

func (i *itemRequestedState) requestItem() error {
	return fmt.Errorf("Item already requested")
}

func (i *itemRequestedState) addItem(count int) error {
	return fmt.Errorf("Item Dispense in progress")
}

func (i *itemRequestedState) insertMoney(money int) error {
	if money < i.vendingMachine.itemPrice {
		fmt.Errorf("Inserted money is less. Please insert %d", i.vendingMachine.itemPrice)
	}
	fmt.Println("Money entered is ok")
	i.vendingMachine.setState(i.vendingMachine.hasMoney)
	return nil
}

func (i *itemRequestedState) dispenseItem() error {
	return fmt.Errorf("Please insert money first")
}

noItemState.go:

import "fmt"

type noItemState struct {
	vendingMachine *vendingMachine
}

func (i *noItemState) requestItem() error {
	return fmt.Errorf("Item out of stock")
}

func (i *noItemState) addItem(count int) error {
	i.vendingMachine.incrementItemCount(count)
	i.vendingMachine.setState(i.vendingMachine.hasItem)
	return nil
}

func (i *noItemState) insertMoney(money int) error {
	return fmt.Errorf("Item out of stock")
}

func (i *noItemState) dispenseItem() error {
	return fmt.Errorf("Item out of stock")
}

下面是場景實現main.go:

import (
	"fmt"
	"log"
)

func main() {
	vendingMachine := newVendingMachine(1, 10)
	err := vendingMachine.requestItem()
	if err != nil {
		log.Fatalf(err.Error())
	}
	err = vendingMachine.insertMoney(10)
	if err != nil {
		log.Fatalf(err.Error())
	}
	err = vendingMachine.dispenseItem()
	if err != nil {
		log.Fatalf(err.Error())
	}

	fmt.Println()
	err = vendingMachine.addItem(2)
	if err != nil {
		log.Fatalf(err.Error())
	}

	fmt.Println()

	err = vendingMachine.requestItem()
	if err != nil {
		log.Fatalf(err.Error())
	}

	err = vendingMachine.insertMoney(10)
	if err != nil {
		log.Fatalf(err.Error())
	}

	err = vendingMachine.dispenseItem()
	if err != nil {
		log.Fatalf(err.Error())
	}
}

func newVendingMachine(itemCount, itemPrice int) *vendingMachine {
	v := &vendingMachine{
		itemCount: itemCount,
		itemPrice: itemPrice,
	}
	hasItemState := &hasItemState{
		vendingMachine: v,
	}
	itemRequestedState := &itemRequestedState{
		vendingMachine: v,
	}
	hasMoneyState := &hasMoneyState{
		vendingMachine: v,
	}
	noItemState := &noItemState{
		vendingMachine: v,
	}

	v.setState(hasItemState)
	v.hasItem = hasItemState
	v.itemRequested = itemRequestedState
	v.hasMoney = hasMoneyState
	v.noItem = noItemState
	return v
}

執行後輸出為:

Item requestd
Money entered is ok
Dispensing Item

Adding 2 items

Item requestd
Money entered is ok
Dispensing Item

程式碼已上傳至GitHub: zhyea / go-patterns / state-pattern

End!!

相關文章