Design Patterns in Go: State

ZhangJie (Kn)
6 min readNov 10, 2024

--

State Pattern

Behavioral Patterns target issues related to communication and interaction between objects. They focus on defining protocols for collaboration among objects that are often too complex to design statically, enabling dynamic delegation of responsibilities at runtime.

OK, Let’s go through the description of the 4th behavioral pattern — State Pattern.

Context:

When an object’s behavior needs to change based on its internal state, a common approach is to use if-else statements to handle different cases. However, as the number of states and behaviors increases, this approach becomes cumbersome and leads to code that is difficult to maintain and extend.

Adding numerous if-else statements can result in code that is hard to read, understand, and modify. It violates the Open-Closed Principle, as each new state or behavior requires modifying the existing code, potentially introducing bugs and making the codebase more fragile.

State Pattern allows an object to alter its behavior dynamically by changing its internal state, without changing its class or the code that uses it.

Solution:

The State Pattern offers a more elegant and maintainable solution. By encapsulating each state’s behavior in separate State objects, the pattern eliminates the need for extensive if-else statements. Instead, the Context object delegates the behavior to the current State object, which encapsulates the behavior associated with that state.

This approach adheres to the Single Responsibility Principle, as each State object is responsible for its own behavior. It also promotes loose coupling between the Context and the State objects, as the Context only needs to know about the State interface, not the specific implementations.

The State Pattern provides several benefits over using if-else statements:

  • Improved readability: The State pattern makes the code more readable and understandable by separating the behavior of each state into its own class. This improves code organization and makes it easier to comprehend the logic of each state.
  • Enhanced maintainability: With the State pattern, adding new states or modifying existing ones becomes easier. New states can be added by creating new State classes without modifying the existing code. This promotes code reuse and makes the system more maintainable and extensible.
  • Reduced complexity: The State pattern simplifies the code by removing the need for complex if-else statements. Each state’s behavior is encapsulated within its own class, making the codebase more modular and easier to manage.
  • Improved testability: The State pattern facilitates unit testing by allowing each state’s behavior to be tested independently. This isolation of behavior makes it easier to write focused and targeted tests for each state.

By using the State pattern, the codebase becomes more flexible, modular, and maintainable. It promotes better separation of concerns and allows for easier addition of new states or modifications to existing ones. Overall, the State pattern provides a cleaner and more scalable solution compared to using extensive if-else statements.

The State pattern involves two main components:
the Context: it is the object whose behavior is influenced by its internal state. It maintains a reference to the current State object and delegates the behavior to that State object.
the State: the State interface defines a set of methods that encapsulate the behavior associated with each state. Each concrete State class implements these methods to provide the specific behavior for that state.

The Context object can change its internal state by setting a new State object. This allows the Context to change its behavior dynamically based on its current state. The Context delegates the execution of methods to the current State object, which encapsulates the behavior associated with that state.

Here’s an example that models how the vending machine works: 1) we choose an idle vending machine, 2) we insert the coins 3) select the product and wait until dispensed. The VendingMachine contains a reference to a State object, which can be changed by the VendingMachine. Each State object defines its own behavior.

State Pattern

Source:

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 SoldState struct{}

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

Here’s the test:

package state_test

import (
"fmt"
"state"
"testing"
)

func TestState(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.
}

ps: Here we put all source code into same codeblock to keep the article layout clean. If you want to run the testing code, please clone the repo go-patterns.

In this example, the vending machine changes its state by externally calling `vendingMachine.SetState(state)`. Actually, State Pattern doesn’t require that where the transition controlled:

  • control it via the Context.SetState(nextState) method;
  • or control it in the concrete State class methods, for example, do `vendingMachine.State = hasSelectionState` before `noSelectionState.SelectItem(…)` returns;

Variants:

There are a few variants of the State Pattern that can be used depending on the specific requirements of the system:

  • Context-driven state transition: In this variant, the Context object drives the state transitions based on its internal logic or external events. It determines when to switch to a new state based on certain conditions or triggers.
  • State-driven state transition: In this variant, the State objects themselves determine the state transitions. Each State object knows which state to transition to next based on the current state and the input it receives.
  • Hierarchical state machine: In complex systems, a hierarchical state machine can be used to represent states and state transitions. This allows for more granular control over the behavior of the object based on its internal state.
    Here’s some FSM libraries in golang:
    - https://github.com/qmuntal/stateless
    - https://github.com/looplab/fsm

Relation:

The State pattern can be related to other patterns in the following ways:

  • Strategy Pattern: The State pattern is similar to the Strategy pattern in that both involve encapsulating behavior and allowing it to be changed dynamically. However, the State pattern focuses on encapsulating behavior based on internal state, while the Strategy pattern focuses on encapsulating interchangeable algorithms.
  • State and Singleton: In some cases, the State pattern can be combined with the Singleton pattern to ensure that only one instance of each State object exists. This can be useful when the State objects don’t have any state-specific data and can be shared across multiple Context objects.
  • State and Decorator: The State pattern can be combined with the Decorator pattern to add additional behavior or functionality to the Context object based on its current state. The Decorator can wrap the Context object and modify its behavior dynamically.

In summary, the State pattern allows an object to change its behavior dynamically based on its internal state. It involves a Context object that maintains a reference to the current State object and delegates behavior to it. The State objects encapsulate the behavior associated with each state. The pattern provides flexibility and extensibility by allowing new states to be added without modifying existing code.

--

--

ZhangJie (Kn)
ZhangJie (Kn)

No responses yet