Wiosenna WYPRZEDAŻ
Stan

Stan w języku Go

Stan to behawioralny wzorzec projektowy pozwalający zmieniać zachowanie obiektu w odpowiedzi na zmianę jego wewnętrznego stanu.

Wzorzec Stan zakłada ekstrakcję zachowań odnoszących się do stanu obiektu do osobnych klas odpowiadających jego poszczególnym stanom. Pierwotny obiekt wówczas deleguje pracę instancjom tych klas, zamiast wykonywać ją samodzielnie.

Przykład koncepcyjny

Zastosujmy wzorzec projektowy Stan w kontekście automatu do sprzedaży. Upraszczając, załóżmy że maszyna zawiera tylko jeden typ produktu i że może znajdować się w jednym z czterech stanów:

  • hasItem (maProdukt)
  • noItem (brakProduktu)
  • itemRequested (zażądanoProdukt)
  • hasMoney (maPieniądze)

Maszyna do sprzedaży będzie też w stanie wykonywać różne czynności. Znów upraszczając, zredukujmy ich liczbę do czterech:

  • Wybór produktu
  • Dodanie produktu
  • Przyjęcie pieniędzy
  • Wydanie produktu

Wzorzec projektowy Stan powinien być stosowany gdy obiekt może znajdować się w różnych stanach, a przejścia między nimi odbywają się na podstawie otrzymanego żądania.

W naszym przykładzie automat do sprzedaży może znajdować się w którymś ze stanów pomiędzy którymi będzie się ciągle przełączać. Powiedzmy, że maszyna jest w stanie ItemRequested. Jak tylko wykonana zostanie czynność “przyjęcie pieniędzy” maszyna przejdzie w stan hasMoney.

Zależnie od bieżącego stanu maszyna może zachowywać się w różny sposób w odpowiedzi na te same żądania. Przykładowo, jeśli użytkownik chce zakupić przedmiot, maszyna przejdzie dalej jeśli jest w stanie hasItemState lub odmówi działania jeśli jest w stanie noItemState.

Kod automatu do sprzedaży nie ulega zaśmieceniu tą logiką, gdyż znajduje się ona wyłącznie w odpowiednich implementacjach stanów.

vendingMachine.go: Kontekst

package main

import "fmt"

type VendingMachine struct {
	hasItem       State
	itemRequested State
	hasMoney      State
	noItem        State

	currentState State

	itemCount int
	itemPrice int
}

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
}

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
}

state.go: Interfejs stanu

package main

type State interface {
	addItem(int) error
	requestItem() error
	insertMoney(money int) error
	dispenseItem() error
}

noItemState.go: Konkretny stan

package main

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")
}

hasItemState.go: Konkretny stan

package main

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")
}

itemRequestedState.go: Konkretny stan

package main

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 {
		return 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")
}

hasMoneyState.go: Konkretny stan

package main

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
}

main.go: Kod klienta

package main

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())
	}
}

output.txt: Wynik działania

Item requestd
Money entered is ok
Dispensing Item

Adding 2 items

Item requestd
Money entered is ok
Dispensing Item
Na podstawie: Golang By Example

Stan w innych językach

Stan w języku C# Stan w języku C++ Stan w języku Java Stan w języku PHP Stan w języku Python Stan w języku Ruby Stan w języku Rust Stan w języku Swift Stan w języku TypeScript