Back to blog

Observer, Command, and State Patterns Explained

oopbehavioral-patternsdesign-patternstypescriptpythonjava
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.

PatternOne-Line Summary
ObserverNotifies many objects automatically when one object's state changes
CommandEncapsulates a request as an object — enabling undo/redo, queues, and logs
StateLets 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


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:

  1. WeatherStation is coupled to every display — it must know each one by name
  2. You can't add/remove displays at runtime — the list is hardcoded
  3. Unit testing requires instantiating all displays even if you only care about one
  4. 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 updates

Example 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.00

Python 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 one

Part 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() and undo()
  • 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 undone

Example 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 withdrawal

Java 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 creation

Part 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 delivered

State 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 draft

Python 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();       // 👋 Closing

All 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

DimensionObserverCommandState
IntentNotify many on changeEncapsulate actionVary behavior by state
CouplingDecouples publisher from subscribersDecouples invoker from receiverDecouples context from state logic
Key benefitAdd/remove subscribers at runtimeUndo/redo, queuingNo conditionals on state
Stores stateNoYes (for undo)Yes (per-state behavior)
DirectionOne-to-many broadcastSingle action → receiverObject delegates to state
Real-worldEvent systems, React stateUI actions, transactionsWorkflows, 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)
  • Reduxstore.subscribe(listener) is classic Observer
  • RxJSObservable.subscribe() is Observer on steroids
  • Node.js EventEmitteremitter.on("event", callback) is Observer
  • DOM EventsaddEventListener is Observer

Command in the Wild

  • Git — every commit is a Command; git revert is undo
  • Browser historyhistory.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
  • Promisespending → fulfilled | rejected
  • React hooksuseState manages 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.

📬 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.