Behaviour that depends on what has already happened: where to keep the rules of an order's life, and when a table beats a class per state.
Place a food order and tap Cancel. If the restaurant hasn't accepted yet, you get all your money back. If the kitchen has started, you get half. Once the rider has picked it up, the button does nothing at all. It is the same button and the same method call, and what happens depends on what has already happened to the order.
That is the State pattern: when an object's behaviour depends on its current state, put each state's behaviour in one place, and let the object hand every request to whichever state it is in. You have already seen it as a status enum in the library, the hotel, ride sharing and food delivery, and as full state classes in the vending machine. Nine of the 22 rounds use some form of it.
This lesson builds one order's life twice, once as a class per state and once as an enum with a transition table, and prints the same thing from both. The interesting question is not how, but which: who owns the transitions, and when each design wins.
A lift's door-open button is State: the same press reopens closing doors, holds open ones longer, and does nothing while the lift is moving.
The first version: a status string and an if in every method#
The first version keeps a status string, and every method starts by checking it:
# order_v1.py: a status string, and an if in every method
class Order:
def __init__(self, paid: int) -> None:
self.paid = paid # rupees
self.status = "placed"
def accept(self) -> None:
if self.status == "placed":
self.status = "accepted"
else:
raise ValueError(f"cannot accept an order that is {self.status}")
def pick_up(self) -> None:
if self.status == "accepted":
self.status = "picked_up"
else:
raise ValueError(f"cannot pick up an order that is {self.status}")
def deliver(self) -> None:
if self.status == "picked_up":
self.status = "delivered"
else:
raise ValueError(f"cannot deliver an order that is {self.status}")
def cancel(self) -> int:
if self.status == "placed":
refund = self.paid
elif self.status == "accepted":
refund = self.paid // 2 # the kitchen has started
else:
raise ValueError(f"cannot cancel an order that is {self.status}")
self.status = "cancelled"
return refund
def label(self) -> str:
if self.status == "placed":
return "Waiting for the restaurant"
elif self.status == "accepted":
return "Being prepared"
elif self.status == "picked_up":
return "On its way"
elif self.status == "delivered":
return "Delivered"
else:
return "Cancelled"
order = Order(300)
print(order.label())
order.accept()
print(order.label())
order.pick_up()
print(order.label())
order.deliver()
print(order.label())
try:
order.cancel()
except ValueError as err:
print("refused:", err)// OrderV1.java: a status string, and an if in every method
class OrderV1 {
static class Order {
final int paid; // rupees
String status = "placed";
Order(int paid) { this.paid = paid; }
void accept() {
if (status.equals("placed")) {
status = "accepted";
} else {
throw new IllegalStateException("cannot accept an order that is " + status);
}
}
void pickUp() {
if (status.equals("accepted")) {
status = "picked_up";
} else {
throw new IllegalStateException("cannot pick up an order that is " + status);
}
}
void deliver() {
if (status.equals("picked_up")) {
status = "delivered";
} else {
throw new IllegalStateException("cannot deliver an order that is " + status);
}
}
int cancel() {
int refund;
if (status.equals("placed")) {
refund = paid;
} else if (status.equals("accepted")) {
refund = paid / 2; // the kitchen has started
} else {
throw new IllegalStateException("cannot cancel an order that is " + status);
}
status = "cancelled";
return refund;
}
String label() {
if (status.equals("placed")) {
return "Waiting for the restaurant";
} else if (status.equals("accepted")) {
return "Being prepared";
} else if (status.equals("picked_up")) {
return "On its way";
} else if (status.equals("delivered")) {
return "Delivered";
} else {
return "Cancelled";
}
}
}
public static void main(String[] args) {
Order order = new Order(300);
System.out.println(order.label());
order.accept();
System.out.println(order.label());
order.pickUp();
System.out.println(order.label());
order.deliver();
System.out.println(order.label());
try {
order.cancel();
} catch (IllegalStateException err) {
System.out.println("refused: " + err.getMessage());
}
}
}// order_v1.cpp: a status string, and an if in every method
#include <iostream>
#include <stdexcept>
#include <string>
class Order {
int paid_; // rupees
std::string status_ = "placed";
public:
explicit Order(int paid) : paid_(paid) {}
void accept() {
if (status_ == "placed") {
status_ = "accepted";
} else {
throw std::logic_error("cannot accept an order that is " + status_);
}
}
void pickUp() {
if (status_ == "accepted") {
status_ = "picked_up";
} else {
throw std::logic_error("cannot pick up an order that is " + status_);
}
}
void deliver() {
if (status_ == "picked_up") {
status_ = "delivered";
} else {
throw std::logic_error("cannot deliver an order that is " + status_);
}
}
int cancel() {
int refund;
if (status_ == "placed") {
refund = paid_;
} else if (status_ == "accepted") {
refund = paid_ / 2; // the kitchen has started
} else {
throw std::logic_error("cannot cancel an order that is " + status_);
}
status_ = "cancelled";
return refund;
}
std::string label() const {
if (status_ == "placed") {
return "Waiting for the restaurant";
} else if (status_ == "accepted") {
return "Being prepared";
} else if (status_ == "picked_up") {
return "On its way";
} else if (status_ == "delivered") {
return "Delivered";
} else {
return "Cancelled";
}
}
};
int main() {
Order order(300);
std::cout << order.label() << "\n";
order.accept();
std::cout << order.label() << "\n";
order.pickUp();
std::cout << order.label() << "\n";
order.deliver();
std::cout << order.label() << "\n";
try {
order.cancel();
} catch (const std::logic_error& err) {
std::cout << "refused: " << err.what() << "\n";
}
}// orderV1.js: a status string, and an if in every method
class Order {
constructor(paid) {
this.paid = paid; // rupees
this.status = "placed";
}
accept() {
if (this.status === "placed") {
this.status = "accepted";
} else {
throw new Error(`cannot accept an order that is ${this.status}`);
}
}
pickUp() {
if (this.status === "accepted") {
this.status = "picked_up";
} else {
throw new Error(`cannot pick up an order that is ${this.status}`);
}
}
deliver() {
if (this.status === "picked_up") {
this.status = "delivered";
} else {
throw new Error(`cannot deliver an order that is ${this.status}`);
}
}
cancel() {
let refund;
if (this.status === "placed") {
refund = this.paid;
} else if (this.status === "accepted") {
refund = Math.floor(this.paid / 2); // the kitchen has started
} else {
throw new Error(`cannot cancel an order that is ${this.status}`);
}
this.status = "cancelled";
return refund;
}
label() {
if (this.status === "placed") {
return "Waiting for the restaurant";
} else if (this.status === "accepted") {
return "Being prepared";
} else if (this.status === "picked_up") {
return "On its way";
} else if (this.status === "delivered") {
return "Delivered";
} else {
return "Cancelled";
}
}
}
const order = new Order(300);
console.log(order.label());
order.accept();
console.log(order.label());
order.pickUp();
console.log(order.label());
order.deliver();
console.log(order.label());
try {
order.cancel();
} catch (err) {
console.log("refused:", err.message);
}Waiting for the restaurant
Being prepared
On its way
Delivered
refused: cannot cancel an order that is deliveredIt runs, and the five states are easy to read today. The rules it hides are the problem:
- The state machine is nowhere. Which events are legal in which state is spread over five methods. To answer "what can happen to a picked-up order?" you read them all.
- A new state edits every method. A ready for pickup step means a new branch in
accept,pick_up,deliver,cancelandlabel. - A typo is a silent bug.
"pickedup"instead of"picked_up"compiles and runs; the order is simply stuck. - Behaviour that varies by state is mixed with the checks. The refund percentage sits inside an
ifthat also decides whether the cancel is allowed. - Nothing can print or test the whole machine. There is no list of states and events to loop over.
Count the cells
There are five states and four events (accept, pick_up, deliver, cancel). How many state-and-event pairs are there, how many are legal, and where does the first version write them down?
One strong answer
Twenty pairs, five legal, and nowhere. Placed accepts and cancels; accepted picks up and cancels; picked up delivers. Every other pair (15 of them) must be refused. The first version never lists the five: they are implied by if branches in four different methods. The refactor puts them in one place.
The pattern#
The book that named the pattern (Design Patterns, 1994, by Gamma, Helm, Johnson and Vlissides) states its intent in two sentences: "Allow an object to alter its behavior when its internal state changes. The object will appear to change its class."
In the order app that gives three roles:
| Role | In the order app | Its one job |
|---|---|---|
| Context | Order |
Hold the current state, hand each request to it, and commit the state's answer |
| State (the interface) | OrderState with accept, pick_up, deliver, cancel |
Say what every state can be asked; the default answer is "not now" |
| Concrete states | Placed, Accepted, PickedUp, Delivered, Cancelled |
Each answers the events it allows, and refuses the rest |
%% title: State: an order's life
classDiagram
class Order {
+paid int
-state OrderState
+status() str
+label() str
+accept()
+pick_up()
+deliver()
+cancel() int
}
class OrderState {
<<interface>>
+name str
+label str
+accept() OrderState
+pick_up() OrderState
+deliver() OrderState
+cancel() OrderState
+refund(paid) int
}
class Placed
class Accepted
class PickedUp
class Delivered
class Cancelled
Order --> OrderState : asks
OrderState <|.. Placed
OrderState <|.. Accepted
OrderState <|.. PickedUp
OrderState <|.. Delivered
OrderState <|.. Cancelled
Placed ..> Accepted : next
Placed ..> Cancelled : next
Accepted ..> PickedUp : next
Accepted ..> Cancelled : next
PickedUp ..> Delivered : nextRead the arrows. Order points at the interface, as in Strategy, so it holds a state without knowing which. The difference is the dotted "next" arrows between the states: each state names the state that follows it. That is the machine, and it is the part that Strategy has no equivalent of: the objects behind the interface know each other.
An order has five states and four events, and only five of the 20 pairs are legal. Each state names the state that follows it.
The code: a class per state#
Each state class answers the events it allows by returning the state that comes next. Everything else falls to the base class, which refuses. Order asks its current state, and commits the answer. The states hold no per-order data (the paid amount lives on the order), so one instance of each is shared by every order.
# order_states.py: one class per state; each state says what it allows and what comes next
from typing import List
class InvalidTransition(Exception):
pass
class OrderState:
"""What an order can do in one state. The default answer to every event is 'not now'."""
name = "?"
label = "?"
def accept(self) -> "OrderState":
raise self._refusal("accept")
def pick_up(self) -> "OrderState":
raise self._refusal("pick up")
def deliver(self) -> "OrderState":
raise self._refusal("deliver")
def cancel(self) -> "OrderState":
raise self._refusal("cancel")
def refund(self, paid: int) -> int:
return 0
def _refusal(self, event: str) -> InvalidTransition:
return InvalidTransition(f"cannot {event} an order that is {self.name}")
class Placed(OrderState):
name, label = "placed", "Waiting for the restaurant"
def accept(self) -> OrderState:
return ACCEPTED
def cancel(self) -> OrderState:
return CANCELLED
def refund(self, paid: int) -> int:
return paid # the kitchen has not started
class Accepted(OrderState):
name, label = "accepted", "Being prepared"
def pick_up(self) -> OrderState:
return PICKED_UP
def cancel(self) -> OrderState:
return CANCELLED
def refund(self, paid: int) -> int:
return paid // 2 # the kitchen has started
class PickedUp(OrderState):
name, label = "picked_up", "On its way"
def deliver(self) -> OrderState:
return DELIVERED
class Delivered(OrderState):
name, label = "delivered", "Delivered"
class Cancelled(OrderState):
name, label = "cancelled", "Cancelled"
# The states hold no per-order data, so one instance of each serves every order.
PLACED, ACCEPTED, PICKED_UP, DELIVERED, CANCELLED = Placed(), Accepted(), PickedUp(), Delivered(), Cancelled()
class Order:
"""The context: it holds the current state, asks it, and commits the answer. Nothing else sets the state."""
def __init__(self, paid: int) -> None:
self.paid = paid # rupees
self._state: OrderState = PLACED
@property
def status(self) -> str:
return self._state.name
def label(self) -> str:
return self._state.label
def accept(self) -> None:
self._state = self._state.accept()
def pick_up(self) -> None:
self._state = self._state.pick_up()
def deliver(self) -> None:
self._state = self._state.deliver()
def cancel(self) -> int:
nxt = self._state.cancel() # raises if cancelling is not allowed here
refund = self._state.refund(self.paid)
self._state = nxt
return refund
EVENTS = ["accept", "pick_up", "deliver", "cancel"]
PATHS = { # how to reach each state, using only legal events
"placed": [],
"accepted": ["accept"],
"picked_up": ["accept", "pick_up"],
"delivered": ["accept", "pick_up", "deliver"],
"cancelled": ["cancel"],
}
def order_in(state_name: str) -> Order:
order = Order(300)
for event in PATHS[state_name]:
getattr(order, event)()
return order
def answer(state_name: str, event: str) -> str:
try:
getattr(order_in(state_name), event)()
return "ok"
except InvalidTransition:
return "no"
order = Order(300)
for step in [None, "accept", "pick_up", "deliver"]:
if step:
getattr(order, step)()
print(f"{order.status}: {order.label()}")
try:
order.cancel()
except InvalidTransition as err:
print(f"cancel after delivery: refused ({err})")
print("cancel when placed: refund", Order(300).cancel())
accepted = order_in("accepted")
print("cancel when accepted: refund", accepted.cancel())
print("-- every state against every event")
for name in PATHS:
cells: List[str] = [f"{event}:{answer(name, event)}" for event in EVENTS]
print(f"{name:<10} " + " ".join(cells))// OrderStates.java: one class per state; each state says what it allows and what comes next
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
class OrderStates {
static final class InvalidTransition extends RuntimeException {
InvalidTransition(String message) { super(message); }
}
/** What an order can do in one state. The default answer to every event is "not now". */
abstract static class OrderState {
final String name, label;
OrderState(String name, String label) { this.name = name; this.label = label; }
OrderState accept() { throw refusal("accept"); }
OrderState pickUp() { throw refusal("pick up"); }
OrderState deliver() { throw refusal("deliver"); }
OrderState cancel() { throw refusal("cancel"); }
int refund(int paid) { return 0; }
private InvalidTransition refusal(String event) {
return new InvalidTransition("cannot " + event + " an order that is " + name);
}
}
static final class Placed extends OrderState {
Placed() { super("placed", "Waiting for the restaurant"); }
OrderState accept() { return ACCEPTED; }
OrderState cancel() { return CANCELLED; }
int refund(int paid) { return paid; } // the kitchen has not started
}
static final class Accepted extends OrderState {
Accepted() { super("accepted", "Being prepared"); }
OrderState pickUp() { return PICKED_UP; }
OrderState cancel() { return CANCELLED; }
int refund(int paid) { return paid / 2; } // the kitchen has started
}
static final class PickedUp extends OrderState {
PickedUp() { super("picked_up", "On its way"); }
OrderState deliver() { return DELIVERED; }
}
static final class Delivered extends OrderState {
Delivered() { super("delivered", "Delivered"); }
}
static final class Cancelled extends OrderState {
Cancelled() { super("cancelled", "Cancelled"); }
}
// The states hold no per-order data, so one instance of each serves every order.
static final OrderState PLACED = new Placed(), ACCEPTED = new Accepted(), PICKED_UP = new PickedUp(),
DELIVERED = new Delivered(), CANCELLED = new Cancelled();
/** The context: it holds the current state, asks it, and commits the answer. Nothing else sets the state. */
static final class Order {
final int paid; // rupees
private OrderState state = PLACED;
Order(int paid) { this.paid = paid; }
String status() { return state.name; }
String label() { return state.label; }
void accept() { state = state.accept(); }
void pickUp() { state = state.pickUp(); }
void deliver() { state = state.deliver(); }
int cancel() {
OrderState next = state.cancel(); // throws if cancelling is not allowed here
int refund = state.refund(paid);
state = next;
return refund;
}
}
static final Map<String, Consumer<Order>> EVENTS = new LinkedHashMap<>();
static final String[][] PATHS = { // how to reach each state, using only legal events
{"placed"}, {"accepted", "accept"}, {"picked_up", "accept", "pick_up"},
{"delivered", "accept", "pick_up", "deliver"}, {"cancelled", "cancel"}};
static {
EVENTS.put("accept", Order::accept);
EVENTS.put("pick_up", Order::pickUp);
EVENTS.put("deliver", Order::deliver);
EVENTS.put("cancel", Order::cancel);
}
static Order orderIn(String[] path) {
Order order = new Order(300);
for (int i = 1; i < path.length; i++) EVENTS.get(path[i]).accept(order);
return order;
}
static String answer(String[] path, String event) {
try {
EVENTS.get(event).accept(orderIn(path));
return "ok";
} catch (InvalidTransition err) {
return "no";
}
}
public static void main(String[] args) {
Order order = new Order(300);
System.out.println(order.status() + ": " + order.label());
order.accept();
System.out.println(order.status() + ": " + order.label());
order.pickUp();
System.out.println(order.status() + ": " + order.label());
order.deliver();
System.out.println(order.status() + ": " + order.label());
try {
order.cancel();
} catch (InvalidTransition err) {
System.out.println("cancel after delivery: refused (" + err.getMessage() + ")");
}
System.out.println("cancel when placed: refund " + new Order(300).cancel());
Order accepted = orderIn(PATHS[1]);
System.out.println("cancel when accepted: refund " + accepted.cancel());
System.out.println("-- every state against every event");
for (String[] path : PATHS) {
StringBuilder line = new StringBuilder(String.format("%-10s", path[0]));
for (String event : EVENTS.keySet()) line.append(" ").append(event).append(":").append(answer(path, event));
System.out.println(line);
}
}
}// order_states.cpp: one class per state; each state says what it allows and what comes next
#include <functional>
#include <iomanip>
#include <iostream>
#include <map>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
struct InvalidTransition : std::logic_error {
using std::logic_error::logic_error;
};
// What an order can do in one state. The default answer to every event is "not now".
class OrderState {
public:
virtual ~OrderState() = default;
virtual std::string name() const = 0;
virtual std::string label() const = 0;
virtual const OrderState& accept() const { throw refusal("accept"); }
virtual const OrderState& pickUp() const { throw refusal("pick up"); }
virtual const OrderState& deliver() const { throw refusal("deliver"); }
virtual const OrderState& cancel() const { throw refusal("cancel"); }
virtual int refund(int) const { return 0; }
protected:
InvalidTransition refusal(const std::string& event) const {
return InvalidTransition("cannot " + event + " an order that is " + name());
}
};
// The states hold no per-order data, so one instance of each serves every order.
const OrderState& placed();
const OrderState& accepted();
const OrderState& pickedUp();
const OrderState& delivered();
const OrderState& cancelled();
class Placed : public OrderState {
public:
std::string name() const override { return "placed"; }
std::string label() const override { return "Waiting for the restaurant"; }
const OrderState& accept() const override { return accepted(); }
const OrderState& cancel() const override { return cancelled(); }
int refund(int paid) const override { return paid; } // the kitchen has not started
};
class Accepted : public OrderState {
public:
std::string name() const override { return "accepted"; }
std::string label() const override { return "Being prepared"; }
const OrderState& pickUp() const override { return pickedUp(); }
const OrderState& cancel() const override { return cancelled(); }
int refund(int paid) const override { return paid / 2; } // the kitchen has started
};
class PickedUp : public OrderState {
public:
std::string name() const override { return "picked_up"; }
std::string label() const override { return "On its way"; }
const OrderState& deliver() const override { return delivered(); }
};
class Delivered : public OrderState {
public:
std::string name() const override { return "delivered"; }
std::string label() const override { return "Delivered"; }
};
class Cancelled : public OrderState {
public:
std::string name() const override { return "cancelled"; }
std::string label() const override { return "Cancelled"; }
};
const OrderState& placed() { static const Placed s; return s; }
const OrderState& accepted() { static const Accepted s; return s; }
const OrderState& pickedUp() { static const PickedUp s; return s; }
const OrderState& delivered() { static const Delivered s; return s; }
const OrderState& cancelled() { static const Cancelled s; return s; }
// The context: it holds the current state, asks it, and commits the answer. Nothing else sets the state.
class Order {
int paid_; // rupees
const OrderState* state_ = &placed();
public:
explicit Order(int paid) : paid_(paid) {}
std::string status() const { return state_->name(); }
std::string label() const { return state_->label(); }
void accept() { state_ = &state_->accept(); }
void pickUp() { state_ = &state_->pickUp(); }
void deliver() { state_ = &state_->deliver(); }
int cancel() {
const OrderState& next = state_->cancel(); // throws if cancelling is not allowed here
int refund = state_->refund(paid_);
state_ = &next;
return refund;
}
};
using Event = std::function<void(Order&)>;
const std::vector<std::pair<std::string, Event>> EVENTS = {
{"accept", [](Order& o) { o.accept(); }},
{"pick_up", [](Order& o) { o.pickUp(); }},
{"deliver", [](Order& o) { o.deliver(); }},
{"cancel", [](Order& o) { o.cancel(); }},
};
// how to reach each state, using only legal events
const std::vector<std::vector<std::string>> PATHS = {
{"placed"}, {"accepted", "accept"}, {"picked_up", "accept", "pick_up"},
{"delivered", "accept", "pick_up", "deliver"}, {"cancelled", "cancel"}};
const Event& eventNamed(const std::string& name) {
for (const auto& e : EVENTS) if (e.first == name) return e.second;
throw std::invalid_argument("no event " + name);
}
Order orderIn(const std::vector<std::string>& path) {
Order order(300);
for (size_t i = 1; i < path.size(); i++) eventNamed(path[i])(order);
return order;
}
std::string answer(const std::vector<std::string>& path, const Event& event) {
try {
Order order = orderIn(path);
event(order);
return "ok";
} catch (const InvalidTransition&) {
return "no";
}
}
int main() {
Order order(300);
std::cout << order.status() << ": " << order.label() << "\n";
order.accept();
std::cout << order.status() << ": " << order.label() << "\n";
order.pickUp();
std::cout << order.status() << ": " << order.label() << "\n";
order.deliver();
std::cout << order.status() << ": " << order.label() << "\n";
try {
order.cancel();
} catch (const InvalidTransition& err) {
std::cout << "cancel after delivery: refused (" << err.what() << ")\n";
}
Order fresh(300);
std::cout << "cancel when placed: refund " << fresh.cancel() << "\n";
Order accepted = orderIn(PATHS[1]);
std::cout << "cancel when accepted: refund " << accepted.cancel() << "\n";
std::cout << "-- every state against every event\n";
for (const auto& path : PATHS) {
std::ostringstream line;
line << std::left << std::setw(10) << path[0];
for (const auto& e : EVENTS) line << " " << e.first << ":" << answer(path, e.second);
std::cout << line.str() << "\n";
}
}// orderStates.js: one class per state; each state says what it allows and what comes next
class InvalidTransition extends Error {}
/** What an order can do in one state. The default answer to every event is "not now". */
class OrderState {
constructor(name, label) {
Object.assign(this, { name, label });
}
accept() {
throw this.refusal("accept");
}
pickUp() {
throw this.refusal("pick up");
}
deliver() {
throw this.refusal("deliver");
}
cancel() {
throw this.refusal("cancel");
}
refund(paid) {
return 0;
}
refusal(event) {
return new InvalidTransition(`cannot ${event} an order that is ${this.name}`);
}
}
class Placed extends OrderState {
constructor() {
super("placed", "Waiting for the restaurant");
}
accept() {
return ACCEPTED;
}
cancel() {
return CANCELLED;
}
refund(paid) {
return paid; // the kitchen has not started
}
}
class Accepted extends OrderState {
constructor() {
super("accepted", "Being prepared");
}
pickUp() {
return PICKED_UP;
}
cancel() {
return CANCELLED;
}
refund(paid) {
return Math.floor(paid / 2); // the kitchen has started
}
}
class PickedUp extends OrderState {
constructor() {
super("picked_up", "On its way");
}
deliver() {
return DELIVERED;
}
}
class Delivered extends OrderState {
constructor() {
super("delivered", "Delivered");
}
}
class Cancelled extends OrderState {
constructor() {
super("cancelled", "Cancelled");
}
}
// The states hold no per-order data, so one instance of each serves every order.
const PLACED = new Placed();
const ACCEPTED = new Accepted();
const PICKED_UP = new PickedUp();
const DELIVERED = new Delivered();
const CANCELLED = new Cancelled();
/** The context: it holds the current state, asks it, and commits the answer. Nothing else sets the state. */
class Order {
#state = PLACED;
constructor(paid) {
this.paid = paid; // rupees
}
get status() {
return this.#state.name;
}
label() {
return this.#state.label;
}
accept() {
this.#state = this.#state.accept();
}
pickUp() {
this.#state = this.#state.pickUp();
}
deliver() {
this.#state = this.#state.deliver();
}
cancel() {
const next = this.#state.cancel(); // throws if cancelling is not allowed here
const refund = this.#state.refund(this.paid);
this.#state = next;
return refund;
}
}
const EVENTS = [["accept", "accept"], ["pick_up", "pickUp"], ["deliver", "deliver"], ["cancel", "cancel"]];
const PATHS = { // how to reach each state, using only legal events
placed: [],
accepted: ["accept"],
picked_up: ["accept", "pickUp"],
delivered: ["accept", "pickUp", "deliver"],
cancelled: ["cancel"],
};
function orderIn(stateName) {
const order = new Order(300);
for (const method of PATHS[stateName]) order[method]();
return order;
}
function answer(stateName, method) {
try {
orderIn(stateName)[method]();
return "ok";
} catch (err) {
if (err instanceof InvalidTransition) return "no";
throw err;
}
}
const order = new Order(300);
for (const step of [null, "accept", "pickUp", "deliver"]) {
if (step) order[step]();
console.log(`${order.status}: ${order.label()}`);
}
try {
order.cancel();
} catch (err) {
console.log(`cancel after delivery: refused (${err.message})`);
}
console.log("cancel when placed: refund", new Order(300).cancel());
console.log("cancel when accepted: refund", orderIn("accepted").cancel());
console.log("-- every state against every event");
for (const name of Object.keys(PATHS)) {
const cells = EVENTS.map(([event, method]) => `${event}:${answer(name, method)}`);
console.log(`${name.padEnd(10)} ${cells.join(" ")}`);
}All four print the same thing:
placed: Waiting for the restaurant
accepted: Being prepared
picked_up: On its way
delivered: Delivered
cancel after delivery: refused (cannot cancel an order that is delivered)
cancel when placed: refund 300
cancel when accepted: refund 150
-- every state against every event
placed accept:ok pick_up:no deliver:no cancel:ok
accepted accept:no pick_up:ok deliver:no cancel:ok
picked_up accept:no pick_up:no deliver:ok cancel:no
delivered accept:no pick_up:no deliver:no cancel:no
cancelled accept:no pick_up:no deliver:no cancel:noCheck by hand. The grid has 20 cells and five oks: exactly the five legal pairs from the checkpoint. An order that has paid 300 is refunded 300 when cancelled while placed, and 150 (half, rounded down) once accepted. The program reaches each state by legal events only, because the order is the only code that commits a state: the field is private in C++ and JavaScript (a # field), which the compiler or runtime enforces. Java enforces it once Order is in its own file (here every class shares one outer class, and Java lets nested classes see each other's private members), and Python only by convention (a leading underscore).
What each language brings to the same design:
- Python: class attributes for
nameandlabel, and a base class whose default methods raise. A subclass overrides only what it allows. Python won't stop code from assigningorder._state; the underscore is a convention. - Java: an abstract base class with default methods that throw; each concrete state overrides what it allows. The state field in
Orderis private; putOrderin its own file, as in a real project, and onlyOrdercan commit a transition. - C++: virtual functions on a base class, returning
const OrderState&; each state is a function-localstatic, so one instance is shared and nothing is allocated per order. - JavaScript: classes with default methods that throw. The state lives in a
#stateprivate field, so a transition can only be committed insideOrder.
Who owns the transitions?#
In the class version the states own the transitions: each one knows what follows it. The other design keeps the whole machine in a single table that the context owns, and the states become plain data:
# order_table.py: an enum and a transition table; the context owns every transition
from enum import Enum
from typing import Dict, List, Tuple
class InvalidTransition(Exception):
pass
class Status(Enum):
PLACED = "placed"
ACCEPTED = "accepted"
PICKED_UP = "picked_up"
DELIVERED = "delivered"
CANCELLED = "cancelled"
# The whole machine, in one place: (status, event) -> next status. Anything missing is refused.
TRANSITIONS: Dict[Tuple[Status, str], Status] = {
(Status.PLACED, "accept"): Status.ACCEPTED,
(Status.PLACED, "cancel"): Status.CANCELLED,
(Status.ACCEPTED, "pick_up"): Status.PICKED_UP,
(Status.ACCEPTED, "cancel"): Status.CANCELLED,
(Status.PICKED_UP, "deliver"): Status.DELIVERED,
}
LABELS = {
Status.PLACED: "Waiting for the restaurant",
Status.ACCEPTED: "Being prepared",
Status.PICKED_UP: "On its way",
Status.DELIVERED: "Delivered",
Status.CANCELLED: "Cancelled",
}
REFUND_PERCENT = {Status.PLACED: 100, Status.ACCEPTED: 50} # the kitchen has started at ACCEPTED
class Order:
"""The context: it owns the status and applies the table. Nothing else sets the status."""
def __init__(self, paid: int) -> None:
self.paid = paid # rupees
self._status = Status.PLACED
@property
def status(self) -> str:
return self._status.value
def label(self) -> str:
return LABELS[self._status]
def _fire(self, event: str) -> None:
nxt = TRANSITIONS.get((self._status, event))
if nxt is None:
raise InvalidTransition(f"cannot {event.replace('_', ' ')} an order that is {self._status.value}")
self._status = nxt
def accept(self) -> None:
self._fire("accept")
def pick_up(self) -> None:
self._fire("pick_up")
def deliver(self) -> None:
self._fire("deliver")
def cancel(self) -> int:
refund = self.paid * REFUND_PERCENT.get(self._status, 0) // 100
self._fire("cancel") # raises if cancelling is not allowed here
return refund
EVENTS = ["accept", "pick_up", "deliver", "cancel"]
PATHS = { # how to reach each state, using only legal events
"placed": [],
"accepted": ["accept"],
"picked_up": ["accept", "pick_up"],
"delivered": ["accept", "pick_up", "deliver"],
"cancelled": ["cancel"],
}
def order_in(state_name: str) -> Order:
order = Order(300)
for event in PATHS[state_name]:
getattr(order, event)()
return order
def answer(state_name: str, event: str) -> str:
try:
getattr(order_in(state_name), event)()
return "ok"
except InvalidTransition:
return "no"
order = Order(300)
for step in [None, "accept", "pick_up", "deliver"]:
if step:
getattr(order, step)()
print(f"{order.status}: {order.label()}")
try:
order.cancel()
except InvalidTransition as err:
print(f"cancel after delivery: refused ({err})")
print("cancel when placed: refund", Order(300).cancel())
accepted = order_in("accepted")
print("cancel when accepted: refund", accepted.cancel())
print("-- every state against every event")
for name in PATHS:
cells: List[str] = [f"{event}:{answer(name, event)}" for event in EVENTS]
print(f"{name:<10} " + " ".join(cells))// OrderTable.java: an enum and a transition table; the context owns every transition
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
class OrderTable {
static final class InvalidTransition extends RuntimeException {
InvalidTransition(String message) { super(message); }
}
enum Status {
PLACED("placed"), ACCEPTED("accepted"), PICKED_UP("picked_up"), DELIVERED("delivered"), CANCELLED("cancelled");
final String value;
Status(String value) { this.value = value; }
}
// The whole machine, in one place: status -> event -> next status. Anything missing is refused.
static final Map<Status, Map<String, Status>> TRANSITIONS = new EnumMap<>(Status.class);
static final Map<Status, String> LABELS = new EnumMap<>(Status.class);
static final Map<Status, Integer> REFUND_PERCENT = new EnumMap<>(Status.class); // the kitchen has started at ACCEPTED
static {
TRANSITIONS.put(Status.PLACED, Map.of("accept", Status.ACCEPTED, "cancel", Status.CANCELLED));
TRANSITIONS.put(Status.ACCEPTED, Map.of("pick_up", Status.PICKED_UP, "cancel", Status.CANCELLED));
TRANSITIONS.put(Status.PICKED_UP, Map.of("deliver", Status.DELIVERED));
LABELS.put(Status.PLACED, "Waiting for the restaurant");
LABELS.put(Status.ACCEPTED, "Being prepared");
LABELS.put(Status.PICKED_UP, "On its way");
LABELS.put(Status.DELIVERED, "Delivered");
LABELS.put(Status.CANCELLED, "Cancelled");
REFUND_PERCENT.put(Status.PLACED, 100);
REFUND_PERCENT.put(Status.ACCEPTED, 50);
}
/** The context: it owns the status and applies the table. Nothing else sets the status. */
static final class Order {
final int paid; // rupees
private Status status = Status.PLACED;
Order(int paid) { this.paid = paid; }
String status() { return status.value; }
String label() { return LABELS.get(status); }
private void fire(String event) {
Status next = TRANSITIONS.getOrDefault(status, Map.of()).get(event);
if (next == null) {
throw new InvalidTransition("cannot " + event.replace('_', ' ') + " an order that is " + status.value);
}
status = next;
}
void accept() { fire("accept"); }
void pickUp() { fire("pick_up"); }
void deliver() { fire("deliver"); }
int cancel() {
int refund = paid * REFUND_PERCENT.getOrDefault(status, 0) / 100;
fire("cancel"); // throws if cancelling is not allowed here
return refund;
}
}
static final Map<String, Consumer<Order>> EVENTS = new LinkedHashMap<>();
static final String[][] PATHS = { // how to reach each state, using only legal events
{"placed"}, {"accepted", "accept"}, {"picked_up", "accept", "pick_up"},
{"delivered", "accept", "pick_up", "deliver"}, {"cancelled", "cancel"}};
static {
EVENTS.put("accept", Order::accept);
EVENTS.put("pick_up", Order::pickUp);
EVENTS.put("deliver", Order::deliver);
EVENTS.put("cancel", Order::cancel);
}
static Order orderIn(String[] path) {
Order order = new Order(300);
for (int i = 1; i < path.length; i++) EVENTS.get(path[i]).accept(order);
return order;
}
static String answer(String[] path, String event) {
try {
EVENTS.get(event).accept(orderIn(path));
return "ok";
} catch (InvalidTransition err) {
return "no";
}
}
public static void main(String[] args) {
Order order = new Order(300);
System.out.println(order.status() + ": " + order.label());
order.accept();
System.out.println(order.status() + ": " + order.label());
order.pickUp();
System.out.println(order.status() + ": " + order.label());
order.deliver();
System.out.println(order.status() + ": " + order.label());
try {
order.cancel();
} catch (InvalidTransition err) {
System.out.println("cancel after delivery: refused (" + err.getMessage() + ")");
}
System.out.println("cancel when placed: refund " + new Order(300).cancel());
Order accepted = orderIn(PATHS[1]);
System.out.println("cancel when accepted: refund " + accepted.cancel());
System.out.println("-- every state against every event");
for (String[] path : PATHS) {
StringBuilder line = new StringBuilder(String.format("%-10s", path[0]));
for (String event : EVENTS.keySet()) line.append(" ").append(event).append(":").append(answer(path, event));
System.out.println(line);
}
}
}// order_table.cpp: an enum and a transition table; the context owns every transition
#include <functional>
#include <iomanip>
#include <iostream>
#include <map>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
struct InvalidTransition : std::logic_error {
using std::logic_error::logic_error;
};
enum class Status { Placed, Accepted, PickedUp, Delivered, Cancelled };
std::string valueOf(Status s) {
switch (s) {
case Status::Placed: return "placed";
case Status::Accepted: return "accepted";
case Status::PickedUp: return "picked_up";
case Status::Delivered: return "delivered";
case Status::Cancelled: return "cancelled";
}
return "?";
}
// The whole machine, in one place: (status, event) -> next status. Anything missing is refused.
const std::map<std::pair<Status, std::string>, Status> TRANSITIONS = {
{{Status::Placed, "accept"}, Status::Accepted},
{{Status::Placed, "cancel"}, Status::Cancelled},
{{Status::Accepted, "pick_up"}, Status::PickedUp},
{{Status::Accepted, "cancel"}, Status::Cancelled},
{{Status::PickedUp, "deliver"}, Status::Delivered},
};
const std::map<Status, std::string> LABELS = {
{Status::Placed, "Waiting for the restaurant"}, {Status::Accepted, "Being prepared"},
{Status::PickedUp, "On its way"}, {Status::Delivered, "Delivered"}, {Status::Cancelled, "Cancelled"}};
const std::map<Status, int> REFUND_PERCENT = {{Status::Placed, 100}, {Status::Accepted, 50}}; // the kitchen has started at Accepted
// The context: it owns the status and applies the table. Nothing else sets the status.
class Order {
int paid_; // rupees
Status status_ = Status::Placed;
void fire(const std::string& event) {
auto it = TRANSITIONS.find({status_, event});
if (it == TRANSITIONS.end()) {
std::string spaced = event;
for (char& c : spaced) if (c == '_') c = ' ';
throw InvalidTransition("cannot " + spaced + " an order that is " + valueOf(status_));
}
status_ = it->second;
}
public:
explicit Order(int paid) : paid_(paid) {}
std::string status() const { return valueOf(status_); }
std::string label() const { return LABELS.at(status_); }
void accept() { fire("accept"); }
void pickUp() { fire("pick_up"); }
void deliver() { fire("deliver"); }
int cancel() {
auto it = REFUND_PERCENT.find(status_);
int refund = paid_ * (it == REFUND_PERCENT.end() ? 0 : it->second) / 100;
fire("cancel"); // throws if cancelling is not allowed here
return refund;
}
};
using Event = std::function<void(Order&)>;
const std::vector<std::pair<std::string, Event>> EVENTS = {
{"accept", [](Order& o) { o.accept(); }},
{"pick_up", [](Order& o) { o.pickUp(); }},
{"deliver", [](Order& o) { o.deliver(); }},
{"cancel", [](Order& o) { o.cancel(); }},
};
// how to reach each state, using only legal events
const std::vector<std::vector<std::string>> PATHS = {
{"placed"}, {"accepted", "accept"}, {"picked_up", "accept", "pick_up"},
{"delivered", "accept", "pick_up", "deliver"}, {"cancelled", "cancel"}};
const Event& eventNamed(const std::string& name) {
for (const auto& e : EVENTS) if (e.first == name) return e.second;
throw std::invalid_argument("no event " + name);
}
Order orderIn(const std::vector<std::string>& path) {
Order order(300);
for (size_t i = 1; i < path.size(); i++) eventNamed(path[i])(order);
return order;
}
std::string answer(const std::vector<std::string>& path, const Event& event) {
try {
Order order = orderIn(path);
event(order);
return "ok";
} catch (const InvalidTransition&) {
return "no";
}
}
int main() {
Order order(300);
std::cout << order.status() << ": " << order.label() << "\n";
order.accept();
std::cout << order.status() << ": " << order.label() << "\n";
order.pickUp();
std::cout << order.status() << ": " << order.label() << "\n";
order.deliver();
std::cout << order.status() << ": " << order.label() << "\n";
try {
order.cancel();
} catch (const InvalidTransition& err) {
std::cout << "cancel after delivery: refused (" << err.what() << ")\n";
}
Order fresh(300);
std::cout << "cancel when placed: refund " << fresh.cancel() << "\n";
Order accepted = orderIn(PATHS[1]);
std::cout << "cancel when accepted: refund " << accepted.cancel() << "\n";
std::cout << "-- every state against every event\n";
for (const auto& path : PATHS) {
std::ostringstream line;
line << std::left << std::setw(10) << path[0];
for (const auto& e : EVENTS) line << " " << e.first << ":" << answer(path, e.second);
std::cout << line.str() << "\n";
}
}// orderTable.js: an enum and a transition table; the context owns every transition
class InvalidTransition extends Error {}
const Status = Object.freeze({
PLACED: "placed",
ACCEPTED: "accepted",
PICKED_UP: "picked_up",
DELIVERED: "delivered",
CANCELLED: "cancelled",
});
// The whole machine, in one place: "status:event" -> next status. Anything missing is refused.
const TRANSITIONS = new Map([
[`${Status.PLACED}:accept`, Status.ACCEPTED],
[`${Status.PLACED}:cancel`, Status.CANCELLED],
[`${Status.ACCEPTED}:pick_up`, Status.PICKED_UP],
[`${Status.ACCEPTED}:cancel`, Status.CANCELLED],
[`${Status.PICKED_UP}:deliver`, Status.DELIVERED],
]);
const LABELS = {
[Status.PLACED]: "Waiting for the restaurant",
[Status.ACCEPTED]: "Being prepared",
[Status.PICKED_UP]: "On its way",
[Status.DELIVERED]: "Delivered",
[Status.CANCELLED]: "Cancelled",
};
const REFUND_PERCENT = { [Status.PLACED]: 100, [Status.ACCEPTED]: 50 }; // the kitchen has started at ACCEPTED
/** The context: it owns the status and applies the table. Nothing else sets the status. */
class Order {
#status = Status.PLACED;
constructor(paid) {
this.paid = paid; // rupees
}
get status() {
return this.#status;
}
label() {
return LABELS[this.#status];
}
#fire(event) {
const next = TRANSITIONS.get(`${this.#status}:${event}`);
if (next === undefined) throw new InvalidTransition(`cannot ${event.replace("_", " ")} an order that is ${this.#status}`);
this.#status = next;
}
accept() {
this.#fire("accept");
}
pickUp() {
this.#fire("pick_up");
}
deliver() {
this.#fire("deliver");
}
cancel() {
const refund = Math.floor((this.paid * (REFUND_PERCENT[this.#status] ?? 0)) / 100);
this.#fire("cancel"); // throws if cancelling is not allowed here
return refund;
}
}
const EVENTS = [["accept", "accept"], ["pick_up", "pickUp"], ["deliver", "deliver"], ["cancel", "cancel"]];
const PATHS = { // how to reach each state, using only legal events
placed: [],
accepted: ["accept"],
picked_up: ["accept", "pickUp"],
delivered: ["accept", "pickUp", "deliver"],
cancelled: ["cancel"],
};
function orderIn(stateName) {
const order = new Order(300);
for (const method of PATHS[stateName]) order[method]();
return order;
}
function answer(stateName, method) {
try {
orderIn(stateName)[method]();
return "ok";
} catch (err) {
if (err instanceof InvalidTransition) return "no";
throw err;
}
}
const order = new Order(300);
for (const step of [null, "accept", "pickUp", "deliver"]) {
if (step) order[step]();
console.log(`${order.status}: ${order.label()}`);
}
try {
order.cancel();
} catch (err) {
console.log(`cancel after delivery: refused (${err.message})`);
}
console.log("cancel when placed: refund", new Order(300).cancel());
console.log("cancel when accepted: refund", orderIn("accepted").cancel());
console.log("-- every state against every event");
for (const name of Object.keys(PATHS)) {
const cells = EVENTS.map(([event, method]) => `${event}:${answer(name, method)}`);
console.log(`${name.padEnd(10)} ${cells.join(" ")}`);
}The output is identical to the class version's:
placed: Waiting for the restaurant
accepted: Being prepared
picked_up: On its way
delivered: Delivered
cancel after delivery: refused (cannot cancel an order that is delivered)
cancel when placed: refund 300
cancel when accepted: refund 150
-- every state against every event
placed accept:ok pick_up:no deliver:no cancel:ok
accepted accept:no pick_up:ok deliver:no cancel:ok
picked_up accept:no pick_up:no deliver:ok cancel:no
delivered accept:no pick_up:no deliver:no cancel:no
cancelled accept:no pick_up:no deliver:no cancel:noThe two designs make different things easy:
States own it: a class per state
- Each state knows its successors: you read one class to learn what can happen from here
- A state can hold real code that differs from the others
- The whole machine is scattered over the classes: no single place to read or print it
The context owns it: an enum and a table
- All 5 legal transitions are 5 lines you can review, print and test
- Labels and refund percentages are data in the same style
- A cell can't hold behaviour that is more than a value; anything richer needs code
So which one wins? Look at what a cell contains. In this order, the states differ in data: which events are allowed, a label and a refund percentage. That is a table's job, and it is why ride sharing (TripStatus with an ALLOWED table) and food delivery (OrderStatus with an ALLOWED table) use one. In the vending machine the states differ in code: HasMoney.select checks the slot, checks the balance and works out the change before it commits, and Idle.select just says "insert money first". Four states with four events is sixteen different answers, and a class per state keeps each answer next to its state. A good rule is to start with an enum and a table, and move to classes when a cell wants more than a value.
A transition may also depend on a condition. The ATM guards every button by state, and keeps the card after too many wrong PINs. A table can hold a guard function beside the next state; once guards multiply, that is another sign that classes would read better.
A new state
Operations adds a ready for pickup state between accepted and picked up. What changes in the class-per-state design, and what in the table design, compared with the first version?
One strong answer
First version: every method gets a new branch. Class per state: a new Ready class, a new ready event on the interface and the order, and Accepted now leads to Ready instead of PickedUp. Table design: a new enum value, a few table lines (accepted -> ready, ready -> pick_up, ready -> cancel), a label and a refund percentage, and the new ready event on the order. In both new designs the existing states keep working and the change is local to where the machine is described. Food delivery has exactly this extra ready state.
Either the states own the transitions, one class each, or the context owns a table of five legal lines. Look at what a cell contains: data, or code.
State and Strategy: the same shape, a different intent#
State and Strategy draw almost the same diagram: a context holding an object behind an interface, and the context delegating to it. The question that tells them apart is who changes the object, and why.
| Strategy (dp-16) | State | |
|---|---|---|
| Who switches it | Something outside: config, the user's plan, the chooser. | The object itself, as events arrive. |
| Why it varies | The business wants a different way to do one job. | The object's history changed what it can do. |
| Do the objects know each other? | No. A fee rule never mentions another fee rule. | Yes. Placed names Accepted and Cancelled. |
| Are they interchangeable? | Yes: any strategy can replace any other, and the context behaves correctly with each. | No: each state allows different events, and which one is current is decided by what has happened, not picked by a caller. |
| Example in the course | the parking lot's pricing, set once in the constructor | the vending machine moving from Idle to HasMoney when a coin goes in |
If the answer to "could I swap this object for another at construction time, and nothing else would notice?" is yes, it is Strategy. If the object moves itself to its neighbour when something happens, it is State. The two combine happily: a state's behaviour (the refund in Placed) can itself be a strategy, as the refund policy is in food delivery.
State and Strategy draw the same diagram. A strategy is chosen from outside and never swaps itself; a state moves the object on as events arrive.
State and its look-alikes#
| Pattern | Looks like State because | The difference |
|---|---|---|
| Strategy | a context delegates to an object behind an interface | See the table above: Strategy is chosen from outside; State moves itself. |
Boolean flags (is_accepted, is_cancelled) |
they also record where the object is | Flags can hold combinations nobody meant, such as delivered and cancelled. One status can only be one thing. |
| Observer (dp-17) | something reacts when the order changes | Transitions are the natural place to announce a change. State decides what is legal; Observer tells the listeners afterwards. |
| A plain enum with an if | it has the same states | Fine for two or three states with few events (the elevator's Direction). It is the first version of this lesson, and it stops scaling when states and events multiply. |
Boolean flags such as is accepted and is cancelled can hold combinations nobody meant, like delivered and cancelled. A single status can only be one thing.
When not to use it#
- There are two states. A boolean is the honest design:
is_open. - There is one event. An enum with a guard at the top of the method is simpler than any machine.
- The states differ only in data. Use an enum and a table, not five classes that return constants.
- The lifecycle lives in a database across services. A long-lived workflow stores its status and rebuilds the object from it on each request; the machine is then a table (or a workflow engine), and a class per state adds little.
The thread-safety caveat applies to every design here. A real order is changed by several threads (the customer cancels while the restaurant accepts), so "check the state, apply the transition and work out the refund" must be one atomic step, under one lock, as in food delivery. This lesson's code is single-threaded by design, to keep the machine in view.
Where it shows up in the case studies#
State appears in nine of the 22 rounds, mostly in the cheap forms, and in one as the full pattern:
| Case study | The state | Form |
|---|---|---|
| Elevator | Direction and doors_open |
an enum, enough for four states; a fire-alarm follow-up is where it would grow into the pattern |
| Movie booking | BookingStatus |
an enum: every operation asks which state the booking is in |
| Vending machine | State, Idle, HasMoney, Dispensing, SoldOut |
the full pattern: a class per state, each answering every event |
| ATM | AtmState and _require() |
an enum with guards: each button is legal in one or two states |
| Library | CopyStatus |
an enum checked by each operation |
| Hotel | BookingStatus |
an enum: only HELD can be confirmed; HELD or CONFIRMED can be cancelled |
| Ride sharing | TripStatus and ALLOWED |
a transition table: five trip states, every other transition refused |
| Food delivery | OrderStatus and ALLOWED |
a transition table: this lesson's order, plus a ready state |
| Auction | AuctionState and _advance |
an enum and one function that moves the state forward by the clock |
Notice that the case studies mostly choose the enum or the table, and use full state classes once. That matches the rule above: reach for classes when the states have behaviour of their own.
In the interview#
Say the pattern with the reason, in one breath: "An order's behaviour depends on its status, so I'll model it as a state machine: five states, four events and a table of the legal transitions, with everything else refused. If the states grow behaviour of their own, I'll move to a class per state." Saying which of the two you chose, and why, is what scores.
Then expect the follow-ups:
- "Add a ready-for-pickup state." A new enum value and a few table lines, or a new state class. The existing states and their tests don't change.
- "Cancel and accept arrive at the same moment." The check, the transition and the refund must be one atomic step under one lock, so exactly one of the two wins and the other is told the transition isn't allowed.
- "The restaurant can cancel too, and refunds in full." The refund depends on the state and on who cancelled: keep the state machine as it is and make the refund a strategy.
- "How do you test it?" Loop over every state and every event, and assert which are legal, as the grid does: 20 cells. Then test each refund.
- "How do you save an order?" Store the status string, and rebuild the state object from it on load; a table or a registry makes that a lookup.
- "State or Strategy?" Who switches it: from outside means Strategy, the object itself means State.
Common mistakes#
- Boolean flags instead of a status, which allow combinations nobody meant.
- Setting the state from outside. Let only the context commit a transition.
- Silently ignoring an illegal event. Refuse it, with the state and the event in the message.
- Forgetting the terminal states. Say what is legal from
deliveredandcancelled: nothing. - Sharing a state object that holds per-order data. A shared state may hold settings, not a retry count.
- A class per state when the states differ only by a number. That is a table.
- No lock around check-then-transition, so two threads both pass the check.
- Status strings with typos. Use an enum: a misspelt name fails at once (at compile time in Java and C++) instead of leaving an order stuck.
State
Q1/4 An order in the delivered state is asked to cancel. In both designs of this lesson, what happens?
Answer & explanation
Answer: C
Delivered and cancelled are terminal: nothing is legal from them. The design refuses the request with the state and the event in the message, instead of ignoring it.
Q2/4 The order's states differ only in which events they allow, a label and a refund percentage. Which design fits best?
Answer & explanation
Answer: B
When cells hold data, a table is enough and the whole machine can be read, printed and tested in one place. Classes pay off when states have behaviour of their own, like the vending machine's HasMoney.
Q3/4 What most clearly separates State from Strategy, which share the same diagram?
Answer & explanation
Answer: C
The shape is the same. The difference is who changes the behaviour: a strategy is set from outside and never swaps itself, while states name their successors and move the object along.
Q4/4 A single Placed state object is shared by every order. Which of these may it safely hold?
Answer & explanation
Answer: C
A shared instance serves every order at once, so it may only hold settings that never change. Anything per order, such as the paid amount or an attempt count, belongs on the order, or in a new state object per transition.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


