State Pattern
Part of the Object Oriented Programming section of Coddy's Swift journey. Lesson 49 of 57.
The state pattern lets an object change its behaviour when its internal state changes. Each state is its own type that decides what happens and which state comes next:
protocol LightState {
var name: String { get }
func next() -> any LightState
}
struct Red: LightState { var name: String { "red" }; func next() -> any LightState { Green() } }
struct Green: LightState { var name: String { "green" }; func next() -> any LightState { Yellow() } }
struct Yellow: LightState { var name: String { "yellow" }; func next() -> any LightState { Red() } }
var light: any LightState = Red()
for _ in 1...4 {
print(light.name)
light = light.next()
}Output:
red
green
yellow
redThe object that owns the state forwards requests to its current state. Each state handles an event in its own way, and illegal actions are refused by the states that do not allow them:
protocol OrderState {
var name: String { get }
func pay() -> (any OrderState)?
func ship() -> (any OrderState)?
}
struct New: OrderState { var name: String { "new" }; func pay() -> (any OrderState)? { Paid() }; func ship() -> (any OrderState)? { nil } }
struct Paid: OrderState { var name: String { "paid" }; func pay() -> (any OrderState)? { nil }; func ship() -> (any OrderState)? { Shipped() } }
struct Shipped: OrderState { var name: String { "shipped" }; func pay() -> (any OrderState)? { nil }; func ship() -> (any OrderState)? { nil } }
final class Order {
private(set) var state: any OrderState = New()
func handle(_ event: String) -> String {
guard let next = event == "pay" ? state.pay() : state.ship() else { return "cannot \(event) when \(state.name)" }
state = next
return "now \(state.name)"
}
}
let order = Order()
for e in ["ship", "pay", "pay", "ship"] { print(order.handle(e)) }Output:
cannot ship when new
now paid
cannot pay when paid
now shippedFor a small number of states without their own data, an enum with a switch often expresses the same idea more compactly, with the compiler checking that every state is handled:
enum Door {
case open, closed, locked
func handle(_ action: String) -> Door? {
switch (self, action) {
case (.closed, "open"): return .open
case (.open, "close"): return .closed
case (.closed, "lock"): return .locked
case (.locked, "unlock"): return .closed
default: return nil
}
}
}
var door = Door.closed
for a in ["lock", "open", "unlock", "open"] {
if let next = door.handle(a) { door = next; print("\(a) -> \(door)") } else { print("cannot \(a)") }
}Output:
lock -> locked
cannot open
unlock -> closed
open -> openState types pay off when each state has several behaviours or its own data. Either way, the rules for each state live in one place instead of if checks spread through the object.
// scattered: every method checks the state
func ship() { if status == "paid" { ... } else if status == "new" { ... } }
// state pattern: each state answers for itself
state = state.ship() ?? stateChallenge
EasyModel a vending machine with an enum state Vending with the cases idle, hasCoin(Int) and soldOut. handle(_ event:) returns the next state or nil for an event that is not allowed: coin <n> in idle goes to hasCoin(n), and in hasCoin adds to the amount; buy in hasCoin with at least 150 goes to idle (a successful sale); refund in hasCoin goes to idle; empty in any state goes to soldOut; refill in soldOut goes to idle.
The supplied code applies the events in order and prints the state after each (idle, coin 100, sold out) or not allowed: <event>.
Your code goes in Vending.swift. main.swift holds the supplied input/output code and cannot be edited.
Try it yourself
// Supplied input/output code: keep it as it is
var input: [String] = []
while let line = readLine() { input.append(line) }
var machine = Vending.idle
for event in input {
if let next = machine.handle(event) {
machine = next
print(machine.label)
} else {
print("not allowed: \(event)")
}
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Classes and Objects
Working With FilesDefining ClassesMethods and selfInitializersClasses Are ReferencesRecap - Library Card4Encapsulation
Access ControlRead-Only From OutsideGuarding StateFailable InitializersRecap - Bank Account7Polymorphism and Generics
PolymorphismType CastingGeneric FunctionsGeneric TypesRecap - Shape Calculator5Inheritance
SubclassesOverriding MethodsInitializers and superfinal and HierarchiesRecap - Employee Hierarchy11Project: Library Management
Books and MembersBorrowing BooksPractice on your own: Swift playground