State Pattern
State is a behavioral design pattern that allows an object to change the behavior when its internal state changes.
The pattern extracts state-related behaviors into separate state classes and forces the original object to delegate the work to an instance of these classes, instead of acting on its own.
Golang Implementation
Example: Vending Machine
Let’s apply the State Design Pattern in the context of vending machines. For simplicity, let’s assume that vending machine only has one type of item or product. Also for simplicity lets assume that a vending machine can be in 4 different states:
- hasItem
- noItem
- itemRequested
- hasMoney
A vending machine will also have different actions. Again for simplicity lets assume that there are only four actions:
- Select the item
- Add the item
- Insert money
- Dispense item
The State design pattern should be used when the object can be in many different states and depending upon incoming request the object needs to change its current state.
In our example, a vending machine can be in many different states and these states will constantly switch from one to another. Let’s say vending machine is in itemRequested. Once the action “Insert Money” occurs, the machine moves to hasMoney state.
Depending on its current state, the machine can behave differently to the same requests. For example, if user wants to purchase an item then the machine will proceed if it’s in hasItemState or it will reject if it’s in noItemState.
The code of the vending machine is not polluted with this logics, all the state-dependent code lives in respective state implementations.
vendingMachine.go
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
package main
type State interface {
addItem(int) error
requestItem() error
insertMoney(money int) error
dispenseItem() error
}
noItemState.go
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
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
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
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
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())
}
}
// Item requestd
// Money entered is ok
// Dispensing Item
// Adding 2 items
// Item requestd
// Money entered is ok
// Dispensing Item
Rust Implementation
The State pattern is related to a finite-state machine (FSM) concept, however, instead of implementing a lot of conditional statements, each state is represented by a separate type that implements a common state trait. Transitions between states depend on the particular trait implementation for each state type.
Example: Music Player
Let’s build a music player with the following state transitions:
player.rs
/// A music track.
pub struct Track {
pub title: String,
pub duration: u32,
cursor: u32,
}
impl Track {
pub fn new(title: &'static str, duration: u32) -> Self {
Self {
title: title.into(),
duration,
cursor: 0,
}
}
}
/// A music player holds a playlist and it can do basic operations over it.
pub struct Player {
playlist: Vec<Track>,
current_track: usize,
_volume: u8,
}
impl Default for Player {
fn default() -> Self {
Self {
playlist: vec![
Track::new("Track 1", 180),
Track::new("Track 2", 165),
Track::new("Track 3", 197),
Track::new("Track 4", 205),
],
current_track: 0,
_volume: 25,
}
}
}
impl Player {
pub fn next_track(&mut self) {
self.current_track = (self.current_track + 1) % self.playlist.len();
}
pub fn prev_track(&mut self) {
self.current_track = (self.playlist.len() + self.current_track - 1) % self.playlist.len();
}
pub fn play(&mut self) {
self.track_mut().cursor = 10; // Playback imitation.
}
pub fn pause(&mut self) {
self.track_mut().cursor = 43; // Paused at some moment.
}
pub fn rewind(&mut self) {
self.track_mut().cursor = 0;
}
pub fn track(&self) -> &Track {
&self.playlist[self.current_track]
}
fn track_mut(&mut self) -> &mut Track {
&mut self.playlist[self.current_track]
}
}
state.rs
use cursive::views::TextView;
use crate::player::Player;
pub struct StoppedState;
pub struct PausedState;
pub struct PlayingState;
/// There is a base `State` trait with methods `play` and `stop` which make
/// state transitions. There are also `next` and `prev` methods in a separate
/// `impl dyn State` block below, those are default implementations
/// that cannot be overridden.
///
/// What is the `self: Box<Self>` notation? We use the state as follows:
/// ```rust
/// let prev_state = Box::new(PlayingState);
/// let next_state = prev_state.play(&mut player);
/// ```
/// A method `play` receives a whole `Box<PlayingState>` object,
/// and not just `PlayingState`. The previous state "disappears" in the method,
/// in turn, it returns a new `Box<PausedState>` state object.
pub trait State {
fn play(self: Box<Self>, player: &mut Player) -> Box<dyn State>;
fn stop(self: Box<Self>, player: &mut Player) -> Box<dyn State>;
fn render(&self, player: &Player, view: &mut TextView);
}
impl State for StoppedState {
fn play(self: Box<Self>, player: &mut Player) -> Box<dyn State> {
player.play();
// Stopped -> Playing.
Box::new(PlayingState)
}
fn stop(self: Box<Self>, _: &mut Player) -> Box<dyn State> {
// Change no state.
self
}
fn render(&self, _: &Player, view: &mut TextView) {
view.set_content("[Stopped] Press 'Play'")
}
}
impl State for PausedState {
fn play(self: Box<Self>, player: &mut Player) -> Box<dyn State> {
player.pause();
// Paused -> Playing.
Box::new(PlayingState)
}
fn stop(self: Box<Self>, player: &mut Player) -> Box<dyn State> {
player.pause();
player.rewind();
// Paused -> Stopped.
Box::new(StoppedState)
}
fn render(&self, player: &Player, view: &mut TextView) {
view.set_content(format!(
"[Paused] {} - {} sec",
player.track().title,
player.track().duration
))
}
}
impl State for PlayingState {
fn play(self: Box<Self>, player: &mut Player) -> Box<dyn State> {
player.pause();
// Playing -> Paused.
Box::new(PausedState)
}
fn stop(self: Box<Self>, player: &mut Player) -> Box<dyn State> {
player.pause();
player.rewind();
// Playing -> Stopped.
Box::new(StoppedState)
}
fn render(&self, player: &Player, view: &mut TextView) {
view.set_content(format!(
"[Playing] {} - {} sec",
player.track().title,
player.track().duration
))
}
}
// Default "next" and "prev" implementations for the trait.
impl dyn State {
pub fn next(self: Box<Self>, player: &mut Player) -> Box<dyn State> {
player.next_track();
// Change no state.
self
}
pub fn prev(self: Box<Self>, player: &mut Player) -> Box<dyn State> {
player.prev_track();
// Change no state.
self
}
}
main.rs
mod player;
mod state;
use cursive::{
event::Key,
view::Nameable,
views::{Dialog, TextView},
Cursive,
};
use player::Player;
use state::{State, StoppedState};
// Application context: a music player and a state.
struct PlayerApplication {
player: Player,
state: Box<dyn State>,
}
fn main() {
let mut app = cursive::default();
app.set_user_data(PlayerApplication {
player: Player::default(),
state: Box::new(StoppedState),
});
app.add_layer(
Dialog::around(TextView::new("Press Play").with_name("Player Status"))
.title("Music Player")
.button("Play", |s| execute(s, "Play"))
.button("Stop", |s| execute(s, "Stop"))
.button("Prev", |s| execute(s, "Prev"))
.button("Next", |s| execute(s, "Next")),
);
app.add_global_callback(Key::Esc, |s| s.quit());
app.run();
}
fn execute(s: &mut Cursive, button: &'static str) {
let PlayerApplication {
mut player,
mut state,
} = s.take_user_data().unwrap();
let mut view = s.find_name::<TextView>("Player Status").unwrap();
// Here is how state mechanics work: the previous state
// executes an action and returns a new state.
// Each state has all 4 operations but reacts differently.
state = match button {
"Play" => state.play(&mut player),
"Stop" => state.stop(&mut player),
"Prev" => state.prev(&mut player),
"Next" => state.next(&mut player),
_ => unreachable!(),
};
state.render(&player, &mut view);
s.set_user_data(PlayerApplication { player, state });
}