Observer, Command, and State Patterns Explained

Introduction
In the previous post you mastered two behavioral patterns for managing algorithms: Strategy for runtime algorithm selection and Template Method for algorithm skeletons. This is the series finale — we cover the last three behavioral patterns that power real-world event-driven and stateful systems.
| Pattern | One-Line Summary |
|---|---|
| Observer | Notifies many objects automatically when one object's state changes |
| Command | Encapsulates a request as an object — enabling undo/redo, queues, and logs |
| State | Lets an object change its behavior when its internal state changes |
Together, these three patterns are the foundation for UI frameworks, game engines, workflow systems, and every event-driven architecture you'll encounter.
What You'll Learn
✅ Build loosely-coupled event systems with the Observer pattern
✅ Implement a generic event bus used in real frameworks
✅ Encapsulate actions as objects with Command — and get undo/redo for free
✅ Build command queues and macro commands
✅ Model complex state machines elegantly with the State pattern
✅ Eliminate multi-level conditional logic from stateful objects
✅ Know when to use each pattern and how they combine
Prerequisites
- Completed Strategy and Template Method Patterns
- Familiar with Polymorphism and Interfaces
- Comfortable with SOLID Principles
Part 1: The Observer Pattern
The Problem: Tight Coupling on State Change
You're building a weather station. Multiple displays (current conditions, statistics, forecast) all need to update when sensor data changes:
// ❌ Before Observer — WeatherStation knows about every display
class WeatherStation {
private temperature: number = 0;
private currentDisplay: CurrentConditionsDisplay;
private statsDisplay: StatisticsDisplay;
private forecastDisplay: ForecastDisplay;
// Adding a new display means modifying this class ❌
update(temperature: number): void {
this.temperature = temperature;
this.currentDisplay.update(temperature); // tight coupling
this.statsDisplay.update(temperature); // tight coupling
this.forecastDisplay.update(temperature); // tight coupling
// New display = change this method ❌
}
}Problems:
- WeatherStation is coupled to every display — it must know each one by name
- You can't add/remove displays at runtime — the list is hardcoded
- Unit testing requires instantiating all displays even if you only care about one
- Violates Open/Closed Principle — adding a display requires modifying the station
The Observer pattern solves this by inverting the dependency: displays subscribe to the station; the station broadcasts without knowing who is listening.
Pattern Structure
Participants:
- Subject (Publisher) — maintains a list of observers; notifies them on state change
- Observer (Subscriber) — the interface all listeners must implement
- ConcreteSubject — the real object whose state others care about
- ConcreteObserver — reacts to subject state changes
Hollywood Principle: "Don't call us, we'll call you." Observers don't poll the subject — the subject calls them.
TypeScript Implementation
Example 1: Weather Station
// Observer interface — what every subscriber must implement
interface WeatherObserver {
update(temperature: number, humidity: number, pressure: number): void;
}
// Subject interface — what every publisher must implement
interface WeatherSubject {
attach(observer: WeatherObserver): void;
detach(observer: WeatherObserver): void;
notify(): void;
}
// Concrete Subject — the publisher
class WeatherStation implements WeatherSubject {
private observers: WeatherObserver[] = [];
private temperature: number = 0;
private humidity: number = 0;
private pressure: number = 0;
attach(observer: WeatherObserver): void {
this.observers.push(observer);
console.log("📡 Observer subscribed");
}
detach(observer: WeatherObserver): void {
const idx = this.observers.indexOf(observer);
if (idx !== -1) {
this.observers.splice(idx, 1);
console.log("📡 Observer unsubscribed");
}
}
notify(): void {
for (const observer of this.observers) {
observer.update(this.temperature, this.humidity, this.pressure);
}
}
// Business method — triggers notification automatically
setMeasurements(temp: number, humidity: number, pressure: number): void {
this.temperature = temp;
this.humidity = humidity;
this.pressure = pressure;
this.notify(); // broadcast to all subscribers
}
}
// Concrete Observers — each reacts differently
class CurrentConditionsDisplay implements WeatherObserver {
update(temperature: number, humidity: number): void {
console.log(`🌡️ Current: ${temperature}°C, ${humidity}% humidity`);
}
}
class StatisticsDisplay implements WeatherObserver {
private temps: number[] = [];
update(temperature: number): void {
this.temps.push(temperature);
const avg = this.temps.reduce((s, t) => s + t, 0) / this.temps.length;
const max = Math.max(...this.temps);
const min = Math.min(...this.temps);
console.log(`📊 Stats: Avg ${avg.toFixed(1)}°C, Max ${max}°C, Min ${min}°C`);
}
}
class ForecastDisplay implements WeatherObserver {
private lastPressure: number = 0;
update(_temp: number, _hum: number, pressure: number): void {
if (pressure > this.lastPressure) {
console.log("⛅ Forecast: Improving weather on the way!");
} else if (pressure < this.lastPressure) {
console.log("🌧️ Forecast: Watch out for cooler, rainy weather");
} else {
console.log("🌤️ Forecast: More of the same");
}
this.lastPressure = pressure;
}
}
// Usage — observers subscribe/unsubscribe at runtime
const station = new WeatherStation();
const current = new CurrentConditionsDisplay();
const stats = new StatisticsDisplay();
const forecast = new ForecastDisplay();
station.attach(current);
station.attach(stats);
station.attach(forecast);
station.setMeasurements(26, 65, 1013);
// 🌡️ Current: 26°C, 65% humidity
// 📊 Stats: Avg 26.0°C, Max 26°C, Min 26°C
// 🌤️ Forecast: More of the same
station.setMeasurements(29, 70, 1009);
// 🌡️ Current: 29°C, 70% humidity
// 📊 Stats: Avg 27.5°C, Max 29°C, Min 26°C
// 🌧️ Forecast: Watch out for cooler, rainy weather
// Remove a display at runtime — zero code changes to WeatherStation
station.detach(forecast);
station.setMeasurements(27, 60, 1012);
// ForecastDisplay no longer receives updatesExample 2: Generic Event Bus
In modern applications, the Observer pattern is usually implemented as a typed event bus:
type EventCallback<T = any> = (data: T) => void;
class EventBus {
private listeners: Map<string, EventCallback[]> = new Map();
on<T>(event: string, callback: EventCallback<T>): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event)!.push(callback as EventCallback);
// Returns an unsubscribe function (like React useEffect cleanup)
return () => this.off(event, callback as EventCallback);
}
off(event: string, callback: EventCallback): void {
const cbs = this.listeners.get(event);
if (cbs) {
this.listeners.set(event, cbs.filter(cb => cb !== callback));
}
}
emit<T>(event: string, data: T): void {
this.listeners.get(event)?.forEach(cb => cb(data));
}
}
// Usage
const bus = new EventBus();
// Subscribe
const unsubscribeA = bus.on<{ username: string }>("user:login", ({ username }) => {
console.log(`📧 Send welcome email to ${username}`);
});
bus.on<{ username: string }>("user:login", ({ username }) => {
console.log(`📝 Audit log: ${username} logged in`);
});
bus.on<{ orderId: string; total: number }>("order:placed", ({ orderId, total }) => {
console.log(`📦 Processing order ${orderId} — $${total}`);
});
// Emit events — all subscribers notified
bus.emit("user:login", { username: "alice" });
// 📧 Send welcome email to alice
// 📝 Audit log: alice logged in
bus.emit("order:placed", { orderId: "ORD-001", total: 149.99 });
// 📦 Processing order ORD-001 — $149.99
// Unsubscribe the first listener
unsubscribeA();
bus.emit("user:login", { username: "bob" });
// 📝 Audit log: bob logged in (first listener removed)Example 3: Stock Price Tracker
interface StockObserver {
onPriceChange(symbol: string, price: number, change: number): void;
}
class StockMarket {
private observers: Map<string, StockObserver[]> = new Map();
private prices: Map<string, number> = new Map();
subscribe(symbol: string, observer: StockObserver): void {
if (!this.observers.has(symbol)) {
this.observers.set(symbol, []);
}
this.observers.get(symbol)!.push(observer);
}
updatePrice(symbol: string, newPrice: number): void {
const oldPrice = this.prices.get(symbol) ?? newPrice;
const change = ((newPrice - oldPrice) / oldPrice) * 100;
this.prices.set(symbol, newPrice);
this.observers.get(symbol)?.forEach(obs =>
obs.onPriceChange(symbol, newPrice, change)
);
}
}
class AlertSystem implements StockObserver {
constructor(private threshold: number) {}
onPriceChange(symbol: string, price: number, change: number): void {
if (Math.abs(change) >= this.threshold) {
console.log(`🚨 ALERT: ${symbol} moved ${change.toFixed(2)}% → $${price}`);
}
}
}
class Portfolio implements StockObserver {
private holdings: Map<string, number>;
constructor(holdings: Record<string, number>) {
this.holdings = new Map(Object.entries(holdings));
}
onPriceChange(symbol: string, price: number): void {
const shares = this.holdings.get(symbol) ?? 0;
const value = shares * price;
console.log(`💼 Portfolio: ${symbol} × ${shares} shares = $${value.toFixed(2)}`);
}
}
const market = new StockMarket();
const alert = new AlertSystem(5); // alert on 5%+ moves
const portfolio = new Portfolio({ AAPL: 10, GOOG: 5 });
market.subscribe("AAPL", alert);
market.subscribe("AAPL", portfolio);
market.subscribe("GOOG", portfolio);
market.updatePrice("AAPL", 180);
market.updatePrice("AAPL", 192); // 6.67% jump — triggers alert
// 🚨 ALERT: AAPL moved 6.67% → $192
// 💼 Portfolio: AAPL × 10 shares = $1920.00Python Implementation
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Callable, Generic, TypeVar
T = TypeVar("T")
# --- Class-based Observer ---
class Observer(ABC):
@abstractmethod
def update(self, event: str, data: dict) -> None:
pass
class Subject:
def __init__(self) -> None:
self._observers: list[Observer] = []
def attach(self, observer: Observer) -> None:
self._observers.append(observer)
def detach(self, observer: Observer) -> None:
self._observers.remove(observer)
def notify(self, event: str, data: dict) -> None:
for obs in self._observers:
obs.update(event, data)
class ECommerceStore(Subject):
def place_order(self, order_id: str, total: float) -> None:
print(f"\n🛒 Order #{order_id} placed — ${total:.2f}")
self.notify("order_placed", {"order_id": order_id, "total": total})
def cancel_order(self, order_id: str) -> None:
print(f"\n❌ Order #{order_id} cancelled")
self.notify("order_cancelled", {"order_id": order_id})
class EmailNotifier(Observer):
def update(self, event: str, data: dict) -> None:
if event == "order_placed":
print(f"📧 Email: Your order #{data['order_id']} confirmed!")
elif event == "order_cancelled":
print(f"📧 Email: Order #{data['order_id']} has been cancelled.")
class InventoryManager(Observer):
def update(self, event: str, data: dict) -> None:
if event == "order_placed":
print(f"📦 Inventory: Reserving stock for order #{data['order_id']}")
elif event == "order_cancelled":
print(f"📦 Inventory: Releasing reserved stock for order #{data['order_id']}")
class AnalyticsTracker(Observer):
def update(self, event: str, data: dict) -> None:
print(f"📊 Analytics: Logged event '{event}' → {data}")
# Usage
store = ECommerceStore()
store.attach(EmailNotifier())
store.attach(InventoryManager())
store.attach(AnalyticsTracker())
store.place_order("ORD-001", 149.99)
store.cancel_order("ORD-001")Java Implementation
import java.util.ArrayList;
import java.util.List;
// Observer Pattern — Newsletter subscription system
public interface NewsletterObserver {
void onNewArticle(String title, String category);
}
public class NewsletterPublisher {
private final List<NewsletterObserver> subscribers = new ArrayList<>();
private String latestTitle;
private String latestCategory;
public void subscribe(NewsletterObserver observer) {
subscribers.add(observer);
System.out.println("✅ New subscriber added");
}
public void unsubscribe(NewsletterObserver observer) {
subscribers.remove(observer);
System.out.println("👋 Subscriber removed");
}
public void publishArticle(String title, String category) {
this.latestTitle = title;
this.latestCategory = category;
System.out.println("\n📰 Published: " + title + " [" + category + "]");
notifySubscribers();
}
private void notifySubscribers() {
for (NewsletterObserver observer : subscribers) {
observer.onNewArticle(latestTitle, latestCategory);
}
}
}
public class TechReader implements NewsletterObserver {
private final String name;
public TechReader(String name) {
this.name = name;
}
@Override
public void onNewArticle(String title, String category) {
if ("Tech".equals(category) || "Programming".equals(category)) {
System.out.println("👨💻 " + name + " reading: " + title);
}
}
}
public class AllTopicsReader implements NewsletterObserver {
private final String name;
public AllTopicsReader(String name) {
this.name = name;
}
@Override
public void onNewArticle(String title, String category) {
System.out.println("📚 " + name + " added '" + title + "' to reading list");
}
}
// Main
NewsletterPublisher publisher = new NewsletterPublisher();
TechReader alice = new TechReader("Alice");
AllTopicsReader bob = new AllTopicsReader("Bob");
publisher.subscribe(alice);
publisher.subscribe(bob);
publisher.publishArticle("Strategy Pattern Explained", "Programming");
publisher.publishArticle("Morning Yoga Routine", "Health");
publisher.unsubscribe(alice);
publisher.publishArticle("Observer Pattern Deep Dive", "Tech");
// Only Bob receives this onePart 2: The Command Pattern
The Problem: Tightly Coupled Actions
You're building a GUI application with buttons, menus, and keyboard shortcuts. Each UI element triggers an action directly:
// ❌ Before Command — buttons directly call business objects
class SaveButton {
private document: Document;
onClick(): void {
this.document.save(); // tight coupling to Document
}
}
class CopyMenuItem {
private editor: TextEditor;
onSelect(): void {
this.editor.copy(); // tight coupling to TextEditor
}
}
// Problems:
// 1. Each UI element is tightly coupled to the specific receiver
// 2. No undo/redo — actions are fire-and-forget
// 3. Can't log, queue, or replay actions
// 4. Can't assign the same action to multiple triggers (button + menu + shortcut)The Command pattern solves this by turning actions into objects. Each command object knows how to execute an action and how to undo it.
Pattern Structure
Participants:
- Command — the interface:
execute()andundo() - ConcreteCommand — wraps a receiver + stores state needed for undo
- Receiver — the object that does the actual work (TextEditor, Light, DB)
- Invoker — triggers commands; maintains command history for undo/redo; doesn't know what the command does
Key insight: The Invoker doesn't call the receiver directly. It just calls command.execute(). This decouples the trigger (button press, menu click) from the action (what actually runs).
TypeScript Implementation
Example 1: Text Editor with Undo/Redo
// Command interface
interface Command {
execute(): void;
undo(): void;
}
// Receiver — the object that actually manipulates text
class TextDocument {
private content: string = "";
write(text: string): void {
this.content += text;
}
delete(length: number): void {
this.content = this.content.slice(0, -length);
}
replace(from: string, to: string): void {
this.content = this.content.replace(from, to);
}
getContent(): string {
return this.content;
}
}
// Concrete Commands — each encapsulates one reversible action
class WriteCommand implements Command {
constructor(private doc: TextDocument, private text: string) {}
execute(): void {
this.doc.write(this.text);
}
undo(): void {
this.doc.delete(this.text.length); // reverse: delete what was written
}
}
class ReplaceCommand implements Command {
private originalContent: string = "";
constructor(
private doc: TextDocument,
private from: string,
private to: string
) {}
execute(): void {
this.originalContent = this.doc.getContent(); // snapshot before change
this.doc.replace(this.from, this.to);
}
undo(): void {
this.doc.replace(this.to, this.from); // simple reverse
// For complex undo, restore the snapshot
}
}
// Invoker — manages command history for undo/redo
class TextEditor {
private history: Command[] = [];
private undoneStack: Command[] = [];
private doc: TextDocument = new TextDocument();
execute(command: Command): void {
command.execute();
this.history.push(command);
this.undoneStack = []; // clear redo stack on new action
console.log(`📝 After execute: "${this.doc.getContent()}"`);
}
undo(): void {
const command = this.history.pop();
if (command) {
command.undo();
this.undoneStack.push(command);
console.log(`↩️ After undo: "${this.doc.getContent()}"`);
} else {
console.log("Nothing to undo.");
}
}
redo(): void {
const command = this.undoneStack.pop();
if (command) {
command.execute();
this.history.push(command);
console.log(`↪️ After redo: "${this.doc.getContent()}"`);
} else {
console.log("Nothing to redo.");
}
}
getDocument(): TextDocument {
return this.doc;
}
}
// Usage
const editor = new TextEditor();
const doc = editor.getDocument();
editor.execute(new WriteCommand(doc, "Hello"));
// 📝 After execute: "Hello"
editor.execute(new WriteCommand(doc, ", World"));
// 📝 After execute: "Hello, World"
editor.execute(new ReplaceCommand(doc, "World", "TypeScript"));
// 📝 After execute: "Hello, TypeScript"
editor.undo();
// ↩️ After undo: "Hello, World"
editor.undo();
// ↩️ After undo: "Hello"
editor.redo();
// ↪️ After redo: "Hello, World"Example 2: Smart Home Remote Control
// Receivers
class Light {
constructor(private location: string) {}
on(): void { console.log(`💡 ${this.location} light ON`); }
off(): void { console.log(`💡 ${this.location} light OFF`); }
}
class Thermostat {
private temperature: number = 20;
setTemperature(temp: number): void {
console.log(`🌡️ Thermostat: ${this.temperature}°C → ${temp}°C`);
this.temperature = temp;
}
getTemperature(): number {
return this.temperature;
}
}
class MusicPlayer {
private volume: number = 30;
play(song: string): void { console.log(`🎵 Playing: ${song}`); }
stop(): void { console.log("⏹️ Music stopped"); }
setVolume(vol: number): void {
console.log(`🔊 Volume: ${this.volume} → ${vol}`);
this.volume = vol;
}
getVolume(): number { return this.volume; }
}
// Concrete Commands
class LightOnCommand implements Command {
constructor(private light: Light) {}
execute(): void { this.light.on(); }
undo(): void { this.light.off(); }
}
class LightOffCommand implements Command {
constructor(private light: Light) {}
execute(): void { this.light.off(); }
undo(): void { this.light.on(); }
}
class SetTemperatureCommand implements Command {
private previousTemp: number;
constructor(private thermostat: Thermostat, private newTemp: number) {
this.previousTemp = thermostat.getTemperature();
}
execute(): void { this.thermostat.setTemperature(this.newTemp); }
undo(): void { this.thermostat.setTemperature(this.previousTemp); }
}
// Macro Command — executes multiple commands as one
class MacroCommand implements Command {
constructor(private commands: Command[]) {}
execute(): void {
this.commands.forEach(cmd => cmd.execute());
}
undo(): void {
// Undo in reverse order
[...this.commands].reverse().forEach(cmd => cmd.undo());
}
}
// Invoker — the remote with history
class SmartRemote {
private history: Command[] = [];
press(command: Command): void {
command.execute();
this.history.push(command);
}
undoLast(): void {
const cmd = this.history.pop();
if (cmd) {
cmd.undo();
console.log("↩️ Last action undone");
}
}
}
// Usage
const livingRoomLight = new Light("Living Room");
const bedroomLight = new Light("Bedroom");
const thermostat = new Thermostat();
const player = new MusicPlayer();
const remote = new SmartRemote();
// Individual commands
remote.press(new LightOnCommand(livingRoomLight)); // 💡 Living Room light ON
remote.press(new SetTemperatureCommand(thermostat, 23)); // 🌡️ 20°C → 23°C
// Macro — "Movie Night" scene: dim all lights + warm room
const movieNight = new MacroCommand([
new LightOffCommand(livingRoomLight),
new LightOffCommand(bedroomLight),
new SetTemperatureCommand(thermostat, 22),
]);
remote.press(movieNight);
// 💡 Living Room light OFF
// 💡 Bedroom light OFF
// 🌡️ 23°C → 22°C
remote.undoLast(); // Undo the entire macro in reverse
// 🌡️ 22°C → 23°C
// 💡 Bedroom light ON
// 💡 Living Room light ON
// ↩️ Last action undoneExample 3: Command Queue (Task Scheduling)
interface AsyncCommand {
execute(): Promise<void>;
getName(): string;
}
class CommandQueue {
private queue: AsyncCommand[] = [];
private running: boolean = false;
enqueue(command: AsyncCommand): void {
this.queue.push(command);
console.log(`📥 Queued: ${command.getName()} (queue size: ${this.queue.length})`);
}
async process(): Promise<void> {
if (this.running) return;
this.running = true;
while (this.queue.length > 0) {
const command = this.queue.shift()!;
console.log(`⚙️ Executing: ${command.getName()}`);
await command.execute();
}
this.running = false;
console.log("✅ All commands processed");
}
}
class SendEmailCommand implements AsyncCommand {
constructor(private to: string, private subject: string) {}
getName(): string { return `SendEmail(${this.to})`; }
async execute(): Promise<void> {
await new Promise(r => setTimeout(r, 100)); // simulate async email send
console.log(`📧 Email sent to ${this.to}: "${this.subject}"`);
}
}
class ResizeImageCommand implements AsyncCommand {
constructor(private imagePath: string, private width: number) {}
getName(): string { return `ResizeImage(${this.imagePath})`; }
async execute(): Promise<void> {
await new Promise(r => setTimeout(r, 200)); // simulate image processing
console.log(`🖼️ Resized ${this.imagePath} to ${this.width}px`);
}
}
// Usage
const queue = new CommandQueue();
queue.enqueue(new SendEmailCommand("alice@example.com", "Welcome!"));
queue.enqueue(new ResizeImageCommand("profile.jpg", 200));
queue.enqueue(new SendEmailCommand("bob@example.com", "Your order shipped"));
queue.process();Python Implementation
from abc import ABC, abstractmethod
from collections import deque
from typing import Optional
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
pass
# Receiver
class BankAccount:
def __init__(self, owner: str, balance: float = 0):
self.owner = owner
self._balance = balance
def deposit(self, amount: float) -> None:
self._balance += amount
print(f" 💰 Deposited ${amount:.2f} → Balance: ${self._balance:.2f}")
def withdraw(self, amount: float) -> bool:
if amount > self._balance:
print(f" ❌ Insufficient funds (balance: ${self._balance:.2f})")
return False
self._balance -= amount
print(f" 💸 Withdrew ${amount:.2f} → Balance: ${self._balance:.2f}")
return True
@property
def balance(self) -> float:
return self._balance
# Concrete Commands
class DepositCommand(Command):
def __init__(self, account: BankAccount, amount: float):
self._account = account
self._amount = amount
def execute(self) -> None:
self._account.deposit(self._amount)
def undo(self) -> None:
self._account.withdraw(self._amount)
class WithdrawCommand(Command):
def __init__(self, account: BankAccount, amount: float):
self._account = account
self._amount = amount
self._success = False
def execute(self) -> None:
self._success = self._account.withdraw(self._amount)
def undo(self) -> None:
if self._success:
self._account.deposit(self._amount)
# Invoker with full undo/redo
class TransactionManager:
def __init__(self):
self._history: list[Command] = []
self._undone: list[Command] = []
def execute(self, command: Command) -> None:
command.execute()
self._history.append(command)
self._undone.clear()
def undo(self) -> None:
if self._history:
cmd = self._history.pop()
cmd.undo()
self._undone.append(cmd)
print(" ↩️ Undone")
else:
print(" Nothing to undo")
def redo(self) -> None:
if self._undone:
cmd = self._undone.pop()
cmd.execute()
self._history.append(cmd)
print(" ↪️ Redone")
else:
print(" Nothing to redo")
# Usage
account = BankAccount("Alice", balance=1000)
manager = TransactionManager()
print("=== Transactions ===")
manager.execute(DepositCommand(account, 500))
manager.execute(WithdrawCommand(account, 200))
manager.execute(WithdrawCommand(account, 1500)) # fails
print("\n=== Undo/Redo ===")
manager.undo() # undo the failed withdraw (no-op since it failed)
manager.undo() # undo the 200 withdrawal
manager.redo() # redo the 200 withdrawalJava Implementation
import java.util.ArrayDeque;
import java.util.Deque;
// Command Pattern — File system operations with undo
public interface FileCommand {
void execute();
void undo();
String getDescription();
}
// Receiver
public class FileSystem {
public void createFile(String path) {
System.out.println("📄 Created: " + path);
}
public void deleteFile(String path) {
System.out.println("🗑️ Deleted: " + path);
}
public void moveFile(String from, String to) {
System.out.println("📁 Moved: " + from + " → " + to);
}
}
// Concrete Commands
public class CreateFileCommand implements FileCommand {
private final FileSystem fs;
private final String path;
public CreateFileCommand(FileSystem fs, String path) {
this.fs = fs;
this.path = path;
}
@Override public void execute() { fs.createFile(path); }
@Override public void undo() { fs.deleteFile(path); }
@Override public String getDescription() { return "Create " + path; }
}
public class MoveFileCommand implements FileCommand {
private final FileSystem fs;
private final String from;
private final String to;
public MoveFileCommand(FileSystem fs, String from, String to) {
this.fs = fs;
this.from = from;
this.to = to;
}
@Override public void execute() { fs.moveFile(from, to); }
@Override public void undo() { fs.moveFile(to, from); }
@Override public String getDescription() { return "Move " + from + " → " + to; }
}
// Invoker
public class FileManager {
private final Deque<FileCommand> history = new ArrayDeque<>();
public void execute(FileCommand command) {
command.execute();
history.push(command);
}
public void undo() {
if (!history.isEmpty()) {
FileCommand cmd = history.pop();
cmd.undo();
System.out.println("↩️ Undid: " + cmd.getDescription());
}
}
}
// Main
FileSystem fs = new FileSystem();
FileManager manager = new FileManager();
manager.execute(new CreateFileCommand(fs, "/docs/report.txt"));
manager.execute(new CreateFileCommand(fs, "/docs/notes.txt"));
manager.execute(new MoveFileCommand(fs, "/docs/report.txt", "/archive/report.txt"));
System.out.println("\n--- Undo operations ---");
manager.undo(); // undo move
manager.undo(); // undo notes.txt creationPart 3: The State Pattern
The Problem: Conditional Logic That Grows With State
You're building a vending machine. Its behavior depends on its current state: waiting for coins, has enough money, out of stock. Without State pattern:
// ❌ Before State — giant switch/if blocks everywhere
class VendingMachine {
private state: "idle" | "hasMoney" | "dispensing" | "outOfStock" = "idle";
insertCoin(amount: number): void {
if (this.state === "idle") {
console.log("Coin accepted");
this.state = "hasMoney";
} else if (this.state === "hasMoney") {
console.log("Adding more coins");
} else if (this.state === "dispensing") {
console.log("Please wait...");
} else if (this.state === "outOfStock") {
console.log("Out of stock — returning coin");
}
}
selectProduct(): void {
if (this.state === "idle") {
console.log("Please insert coins first");
} else if (this.state === "hasMoney") {
console.log("Dispensing...");
this.state = "dispensing";
}
// ... more branches
}
// Every method has a giant switch. Adding a new state = touch every method ❌
}The State pattern fixes this: each state gets its own class. The object delegates to the current state class instead of running conditionals. Adding a new state doesn't require touching existing state classes.
Pattern Structure
Participants:
- Context — the object whose behavior changes; delegates to the current state object; exposes
setState()for state transitions - State — the interface all state classes implement
- ConcreteState — encapsulates behavior for one specific state; triggers transitions by calling
context.setState()
Key insight: State objects transition the context by calling context.setState(newState). No conditionals — the state itself decides what comes next.
TypeScript Implementation
Example 1: Order Lifecycle
// Context (forward declaration for circular reference)
class Order {
private state: OrderState;
private orderId: string;
constructor(orderId: string) {
this.orderId = orderId;
this.state = new PendingState();
console.log(`\n📋 Order #${orderId} created (Pending)`);
}
setState(state: OrderState): void {
this.state = state;
}
pay(): void { this.state.pay(this); }
ship(): void { this.state.ship(this); }
deliver(): void { this.state.deliver(this); }
cancel(): void { this.state.cancel(this); }
getOrderId(): string { return this.orderId; }
}
// State interface
interface OrderState {
pay(order: Order): void;
ship(order: Order): void;
deliver(order: Order): void;
cancel(order: Order): void;
}
// Concrete States — each handles only what makes sense in that state
class PendingState implements OrderState {
pay(order: Order): void {
console.log(`💳 Order #${order.getOrderId()} paid! → Processing`);
order.setState(new ProcessingState());
}
ship(order: Order): void {
console.log("❌ Cannot ship — order not paid yet");
}
deliver(order: Order): void {
console.log("❌ Cannot deliver — order not shipped");
}
cancel(order: Order): void {
console.log(`🚫 Order #${order.getOrderId()} cancelled (before payment)`);
order.setState(new CancelledState());
}
}
class ProcessingState implements OrderState {
pay(order: Order): void {
console.log("ℹ️ Already paid");
}
ship(order: Order): void {
console.log(`📦 Order #${order.getOrderId()} shipped! → Shipped`);
order.setState(new ShippedState());
}
deliver(order: Order): void {
console.log("❌ Cannot deliver — not shipped yet");
}
cancel(order: Order): void {
console.log(`🚫 Order #${order.getOrderId()} cancelled (refund issued)`);
order.setState(new CancelledState());
}
}
class ShippedState implements OrderState {
pay(order: Order): void { console.log("ℹ️ Already paid"); }
ship(order: Order): void { console.log("ℹ️ Already shipped"); }
deliver(order: Order): void {
console.log(`🎉 Order #${order.getOrderId()} delivered! → Delivered`);
order.setState(new DeliveredState());
}
cancel(order: Order): void {
console.log("❌ Cannot cancel — order already shipped");
}
}
class DeliveredState implements OrderState {
pay(order: Order): void { console.log("ℹ️ Already paid"); }
ship(order: Order): void { console.log("ℹ️ Already shipped"); }
deliver(order: Order): void { console.log("ℹ️ Already delivered"); }
cancel(order: Order): void { console.log("❌ Cannot cancel — order delivered"); }
}
class CancelledState implements OrderState {
pay(order: Order): void { console.log("❌ Order is cancelled"); }
ship(order: Order): void { console.log("❌ Order is cancelled"); }
deliver(order: Order): void { console.log("❌ Order is cancelled"); }
cancel(order: Order): void { console.log("ℹ️ Already cancelled"); }
}
// Usage — state transitions
const order = new Order("ORD-2026-001");
order.ship(); // ❌ Cannot ship — order not paid yet
order.pay(); // 💳 paid! → Processing
order.ship(); // 📦 shipped! → Shipped
order.cancel(); // ❌ Cannot cancel — already shipped
order.deliver(); // 🎉 delivered!
order.cancel(); // ❌ Cannot cancel — order deliveredState transition diagram:
Example 2: Traffic Light
interface TrafficLightState {
next(light: TrafficLight): void;
getColor(): string;
getDuration(): number; // seconds
}
class TrafficLight {
private state: TrafficLightState = new GreenState();
transition(): void {
console.log(`🚦 ${this.state.getColor()} (${this.state.getDuration()}s) → switching...`);
this.state.next(this);
}
setState(state: TrafficLightState): void {
this.state = state;
console.log(` → ${this.state.getColor()}`);
}
}
class GreenState implements TrafficLightState {
next(light: TrafficLight): void { light.setState(new YellowState()); }
getColor(): string { return "🟢 Green"; }
getDuration(): number { return 30; }
}
class YellowState implements TrafficLightState {
next(light: TrafficLight): void { light.setState(new RedState()); }
getColor(): string { return "🟡 Yellow"; }
getDuration(): number { return 5; }
}
class RedState implements TrafficLightState {
next(light: TrafficLight): void { light.setState(new GreenState()); }
getColor(): string { return "🔴 Red"; }
getDuration(): number { return 25; }
}
const light = new TrafficLight();
for (let i = 0; i < 5; i++) {
light.transition();
}Example 3: Document Approval Workflow
class Document {
private state: DocumentState = new DraftState();
content: string = "";
setState(state: DocumentState): void {
this.state = state;
}
submit(): void { this.state.submit(this); }
approve(): void { this.state.approve(this); }
reject(): void { this.state.reject(this); }
publish(): void { this.state.publish(this); }
getStateName(): string { return this.state.getName(); }
}
interface DocumentState {
submit(doc: Document): void;
approve(doc: Document): void;
reject(doc: Document): void;
publish(doc: Document): void;
getName(): string;
}
class DraftState implements DocumentState {
submit(doc: Document): void {
console.log("📤 Document submitted for review");
doc.setState(new ReviewState());
}
approve(doc: Document): void { console.log("❌ Draft must be submitted first"); }
reject(doc: Document): void { console.log("❌ Draft must be submitted first"); }
publish(doc: Document): void { console.log("❌ Draft must be approved first"); }
getName(): string { return "Draft"; }
}
class ReviewState implements DocumentState {
submit(doc: Document): void { console.log("ℹ️ Already under review"); }
approve(doc: Document): void {
console.log("✅ Document approved");
doc.setState(new ApprovedState());
}
reject(doc: Document): void {
console.log("❌ Document rejected — back to draft");
doc.setState(new DraftState());
}
publish(doc: Document): void { console.log("❌ Must be approved first"); }
getName(): string { return "Under Review"; }
}
class ApprovedState implements DocumentState {
submit(doc: Document): void { console.log("ℹ️ Already approved"); }
approve(doc: Document): void { console.log("ℹ️ Already approved"); }
reject(doc: Document): void {
console.log("📝 Approval revoked — back to draft");
doc.setState(new DraftState());
}
publish(doc: Document): void {
console.log("🌐 Document published!");
doc.setState(new PublishedState());
}
getName(): string { return "Approved"; }
}
class PublishedState implements DocumentState {
submit(doc: Document): void { console.log("ℹ️ Already published"); }
approve(doc: Document): void { console.log("ℹ️ Already published"); }
reject(doc: Document): void {
console.log("⬇️ Document unpublished — back to draft");
doc.setState(new DraftState());
}
publish(doc: Document): void { console.log("ℹ️ Already published"); }
getName(): string { return "Published"; }
}
// Usage
const doc = new Document();
console.log(`State: ${doc.getStateName()}`); // Draft
doc.approve(); // ❌ Draft must be submitted first
doc.submit(); // 📤 submitted for review
doc.reject(); // ❌ rejected — back to draft
doc.submit(); // 📤 submitted again
doc.approve(); // ✅ approved
doc.publish(); // 🌐 published!
doc.reject(); // ⬇️ unpublished — back to draftPython Implementation
from abc import ABC, abstractmethod
class VendingMachineState(ABC):
@abstractmethod
def insert_coin(self, machine: "VendingMachine", amount: float) -> None:
pass
@abstractmethod
def select_product(self, machine: "VendingMachine", product: str, price: float) -> None:
pass
@abstractmethod
def dispense(self, machine: "VendingMachine") -> None:
pass
@abstractmethod
def cancel(self, machine: "VendingMachine") -> None:
pass
class VendingMachine:
def __init__(self) -> None:
self._state: VendingMachineState = IdleState()
self._balance: float = 0
self._selected_product: str | None = None
self._selected_price: float = 0
def set_state(self, state: VendingMachineState) -> None:
self._state = state
def add_balance(self, amount: float) -> None:
self._balance += amount
def set_product(self, product: str, price: float) -> None:
self._selected_product = product
self._selected_price = price
def get_balance(self) -> float:
return self._balance
def get_selected_price(self) -> float:
return self._selected_price
def reset(self) -> None:
self._balance = 0
self._selected_product = None
self._selected_price = 0
# Delegate to current state
def insert_coin(self, amount: float) -> None:
self._state.insert_coin(self, amount)
def select_product(self, product: str, price: float) -> None:
self._state.select_product(self, product, price)
def dispense(self) -> None:
self._state.dispense(self)
def cancel(self) -> None:
self._state.cancel(self)
class IdleState(VendingMachineState):
def insert_coin(self, machine: VendingMachine, amount: float) -> None:
machine.add_balance(amount)
print(f"💰 Accepted ${amount:.2f} — Balance: ${machine.get_balance():.2f}")
machine.set_state(HasMoneyState())
def select_product(self, machine: VendingMachine, product: str, price: float) -> None:
print("❌ Insert coins first")
def dispense(self, machine: VendingMachine) -> None:
print("❌ Insert coins and select a product first")
def cancel(self, machine: VendingMachine) -> None:
print("ℹ️ Nothing to cancel")
class HasMoneyState(VendingMachineState):
def insert_coin(self, machine: VendingMachine, amount: float) -> None:
machine.add_balance(amount)
print(f"💰 Added ${amount:.2f} — Balance: ${machine.get_balance():.2f}")
def select_product(self, machine: VendingMachine, product: str, price: float) -> None:
if machine.get_balance() >= price:
machine.set_product(product, price)
print(f"🎯 Selected: {product} (${price:.2f}) — dispensing...")
machine.set_state(DispensingState())
else:
shortage = price - machine.get_balance()
print(f"❌ Insufficient — need ${shortage:.2f} more for {product}")
def dispense(self, machine: VendingMachine) -> None:
print("❌ Select a product first")
def cancel(self, machine: VendingMachine) -> None:
change = machine.get_balance()
machine.reset()
machine.set_state(IdleState())
print(f"↩️ Cancelled — returning ${change:.2f}")
class DispensingState(VendingMachineState):
def insert_coin(self, machine: VendingMachine, amount: float) -> None:
print("⏳ Dispensing — please wait")
def select_product(self, machine: VendingMachine, product: str, price: float) -> None:
print("⏳ Dispensing — please wait")
def dispense(self, machine: VendingMachine) -> None:
change = machine.get_balance() - machine.get_selected_price()
machine.reset()
machine.set_state(IdleState())
if change > 0:
print(f"🎁 Dispensed! Change: ${change:.2f}")
else:
print("🎁 Dispensed! Enjoy!")
def cancel(self, machine: VendingMachine) -> None:
print("❌ Already dispensing — cannot cancel")
# Usage
vm = VendingMachine()
vm.select_product("Cola", 1.50) # ❌ Insert coins first
vm.insert_coin(1.00) # 💰 Accepted
vm.select_product("Cola", 1.50) # ❌ Not enough
vm.insert_coin(0.50) # 💰 Added
vm.select_product("Cola", 1.50) # ✅ Dispensing
vm.dispense() # 🎁 Dispensed!Java Implementation
// State Pattern — TCP Connection states
public interface TCPState {
void open(TCPConnection conn);
void close(TCPConnection conn);
void acknowledge(TCPConnection conn);
String getStateName();
}
public class TCPConnection {
private TCPState state = new ClosedState();
public void setState(TCPState state) {
System.out.println(" → Transitioning to: " + state.getStateName());
this.state = state;
}
public void open() { state.open(this); }
public void close() { state.close(this); }
public void acknowledge() { state.acknowledge(this); }
public String getState() { return state.getStateName(); }
}
public class ClosedState implements TCPState {
@Override public void open(TCPConnection conn) {
System.out.println("🔌 Opening connection (SYN sent)");
conn.setState(new ListenState());
}
@Override public void close(TCPConnection conn) {
System.out.println("ℹ️ Already closed");
}
@Override public void acknowledge(TCPConnection conn) {
System.out.println("❌ Cannot ACK — connection not open");
}
@Override public String getStateName() { return "Closed"; }
}
public class ListenState implements TCPState {
@Override public void open(TCPConnection conn) {
System.out.println("ℹ️ Already listening");
}
@Override public void close(TCPConnection conn) {
System.out.println("🔌 Closing connection");
conn.setState(new ClosedState());
}
@Override public void acknowledge(TCPConnection conn) {
System.out.println("✅ SYN-ACK received — connection established");
conn.setState(new EstablishedState());
}
@Override public String getStateName() { return "Listen"; }
}
public class EstablishedState implements TCPState {
@Override public void open(TCPConnection conn) {
System.out.println("ℹ️ Already established");
}
@Override public void close(TCPConnection conn) {
System.out.println("👋 Sending FIN — closing");
conn.setState(new ClosedState());
}
@Override public void acknowledge(TCPConnection conn) {
System.out.println("📨 Data acknowledged");
}
@Override public String getStateName() { return "Established"; }
}
// Main
TCPConnection conn = new TCPConnection();
System.out.println("State: " + conn.getState()); // Closed
conn.acknowledge(); // ❌ Cannot ACK
conn.open(); // 🔌 Opening
conn.acknowledge(); // ✅ Established
conn.acknowledge(); // 📨 Data ACK
conn.close(); // 👋 ClosingAll Three Together: A Real-World Example
Let's see how Observer, Command, and State work together in a document editing system:
// State — tracks the document lifecycle
// Command — every edit is undoable
// Observer — UI components react to state + content changes
class DocumentEditor {
private eventBus = new EventBus();
private commandManager = new TextEditor();
private doc: TextDocument;
private workflowState: DocumentState = new DraftState();
constructor() {
this.doc = this.commandManager.getDocument();
// Observers watching the editor
this.eventBus.on("content:changed", (content: string) => {
console.log(`📊 Word count: ${content.split(" ").filter(Boolean).length}`);
});
this.eventBus.on("state:changed", (state: string) => {
console.log(`🔔 Toolbar updated for state: ${state}`);
});
}
type(text: string): void {
this.commandManager.execute(new WriteCommand(this.doc, text));
this.eventBus.emit("content:changed", this.doc.getContent());
}
undoTyping(): void {
this.commandManager.undo();
this.eventBus.emit("content:changed", this.doc.getContent());
}
submitForReview(): void {
this.workflowState.submit({ setState: (s) => {
this.workflowState = s;
this.eventBus.emit("state:changed", s.getName());
}} as any);
}
}
const editor = new DocumentEditor();
editor.type("Hello");
editor.type(", Design Patterns!");
editor.undoTyping();
editor.submitForReview();Comparing the Three Patterns
| Dimension | Observer | Command | State |
|---|---|---|---|
| Intent | Notify many on change | Encapsulate action | Vary behavior by state |
| Coupling | Decouples publisher from subscribers | Decouples invoker from receiver | Decouples context from state logic |
| Key benefit | Add/remove subscribers at runtime | Undo/redo, queuing | No conditionals on state |
| Stores state | No | Yes (for undo) | Yes (per-state behavior) |
| Direction | One-to-many broadcast | Single action → receiver | Object delegates to state |
| Real-world | Event systems, React state | UI actions, transactions | Workflows, state machines |
Real-World Frameworks Using These Patterns
Observer in the Wild
- React / Vue / Angular — component re-renders are Observer: state changes notify components (subscribers)
- Redux —
store.subscribe(listener)is classic Observer - RxJS —
Observable.subscribe()is Observer on steroids - Node.js EventEmitter —
emitter.on("event", callback)is Observer - DOM Events —
addEventListeneris Observer
Command in the Wild
- Git — every commit is a Command;
git revertis undo - Browser history —
history.pushState()/history.back()is Command - Database transactions — COMMIT/ROLLBACK encapsulates command + undo
- Redux actions — dispatched actions are Commands; reducers are receivers
- Text editors (VS Code, IntelliJ) — ctrl+Z is Command.undo()
State in the Wild
- TCP/IP protocol — CLOSED → LISTEN → ESTABLISHED → TIME_WAIT
- Promises —
pending → fulfilled | rejected - React hooks —
useStatemanages state transitions - Order management systems — Pending → Processing → Shipped → Delivered
- Video player — Stopped → Playing → Paused → Buffering
Summary and Key Takeaways
This post completes the OOP & Design Patterns series. Here's what each final pattern brings:
Observer Pattern — Publish/Subscribe:
✅ Defines a one-to-many dependency: one subject, many observers
✅ Observers subscribe/unsubscribe at runtime — zero changes to subject
✅ Loose coupling: subject knows only the Observer interface, not concrete classes
✅ Foundation for all event-driven architectures
Command Pattern — Encapsulated Actions:
✅ Turns requests into objects — store, queue, log, delay, or replay them
✅ Enables undo/redo by storing reverse operations
✅ Decouples the trigger (button) from the action (receiver)
✅ Macro commands compose multiple commands into one
State Pattern — State Machines Without Conditionals:
✅ Each state is a class — behavior lives where it belongs
✅ State objects handle their own transitions — no central dispatcher
✅ Adding a new state doesn't require touching existing states
✅ The context's methods stay lean — they just delegate
🎉 Series Complete!
You've finished the OOP & Design Patterns series — all 15 posts covering:
- Phase 1: Four pillars, classes, encapsulation, inheritance, polymorphism
- Phase 2: SOLID principles
- Phase 3: Creational patterns — Singleton, Factory, Builder, Prototype
- Phase 4: Structural patterns — Adapter, Facade, Decorator, Proxy
- Phase 5: Behavioral patterns — Strategy, Template Method, Observer, Command, State
You now have a solid foundation in object-oriented design. The patterns you've learned appear in every major framework and codebase — you'll recognize them everywhere from here.
Recommended Next Series
- Software Architecture Patterns — scale from design patterns to full system architecture
- Spring Boot Roadmap — these patterns appear everywhere in Spring
- TypeScript Full-Stack Roadmap — apply patterns in a real full-stack project
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.