Francesco Sannicola

Machine Learning | Software Engineering

./articles/ Software Engineering Essentials: Behavioral Design Patterns

Last modified:

Behavioral design patterns are one of the three major categories of design patterns, alongside Creational and Structural. While Creational patterns deal with object creation and Structural patterns handle the composition of classes and objects, Behavioral patterns focus on how objects communicate and collaborate with one another . They define the responsibilities of objects and the protocols through which they interact, making complex control flows easier to understand, extend, and maintain.

This guide examines ten Behavioral Design Patterns through their intent, participants, trade-offs, and runnable examples. The first six patterns, plus Memento and Visitor, include Python and Java implementations; State and Mediator use Python. The selection is not the complete behavioral-pattern catalog.

For the other GoF categories, explore Creational Design Patterns, focused on object creation, and Structural Design Patterns, focused on composition.

Explore the source implementations of the behavioral patterns in GitHub: OOP Design Patterns Handbook. The examples below remain self-contained, so you can run each pattern independently while reading.

Run each Python example independently with Python 3.10 or later. Each Java example is a separate source file: save it using the filename in its first comment, then compile and run its public class, for example javac -encoding UTF-8 WeatherStation.java and java WeatherStation . The Java code targets Java 8 or later and uses only JDK APIs. Do not combine all snippets into one file. The output blocks show the demo runs; Java numeric formatting uses a fixed locale.

Quick Reference

Pattern Core Intent Key Benefit
Iterator Traverse a collection without exposing its internals Decouples traversal from data structure
Command Encapsulate a request as an object Enables undo/redo, queuing, and logging
Strategy Define a family of interchangeable algorithms Swap behavior at runtime without changing the client
Observer Notify dependents automatically when state changes Loose coupling between publisher and subscribers
Chain of Responsibility Pass a request along a chain of handlers Decouples sender from receiver
Template Method Define the skeleton of an algorithm in a base class Reuse common steps, vary specific ones
State Alter object behavior when its internal state changes Eliminates large state-based conditionals
Mediator Centralize complex communication between objects Reduces tight coupling among collaborators
Memento Capture and restore an object's state Keeps checkpoint storage separate from state representation
Visitor Separate operations from a stable element hierarchy Adds operations without editing each element class

Each section moves from the design problem to the implementation and its output. The comparison at the end focuses on selection criteria and common mistakes, rather than repeating the definitions.

1. Iterator

Intent

The Iterator pattern provides a way to sequentially access the elements of a collection without exposing its underlying representation. Whether the collection is a list, a tree, a graph, or a custom data structure, the client always interacts with it through the same uniform interface.

The Problem It Solves

Imagine you have a BookCollection class that stores books internally as a list. A client that wants to iterate over the books would need to know that the collection uses a list, access it directly, and write a for loop against the list's index. Now suppose you later change the internal representation to a tree or a hash map: you would have to update every client that iterates over it. The Iterator pattern breaks this dependency: the collection exposes an iterator object, and the client only talks to that object.

Structure

The pattern has four participants:

Python Implementation

from abc import ABC, abstractmethod
from typing import Any


# Iterator interface
class Iterator(ABC):
    def __iter__(self):
        return self

    def __next__(self) -> Any:
        return self.next()

    @abstractmethod
    def has_next(self) -> bool:
        pass

    @abstractmethod
    def next(self) -> Any:
        pass


# Iterable interface
class IterableCollection(ABC):
    def __iter__(self) -> Iterator:
        return self.create_iterator()

    @abstractmethod
    def create_iterator(self) -> Iterator:
        pass


# Concrete Iterator
class BookIterator(Iterator):
    def __init__(self, books: list[str]):
        self._books = tuple(books)  # stable traversal snapshot
        self._index = 0

    def has_next(self) -> bool:
        return self._index < len(self._books)

    def next(self) -> str:
        if not self.has_next():
            raise StopIteration
        book = self._books[self._index]
        self._index += 1
        return book


# Concrete Collection
class BookCollection(IterableCollection):
    def __init__(self):
        self._books: list[str] = []

    def add_book(self, book: str) -> None:
        self._books.append(book)

    def create_iterator(self) -> Iterator:
        return BookIterator(self._books)


# Example usage
if __name__ == "__main__":
    library = BookCollection()
    library.add_book("The Pragmatic Programmer")
    library.add_book("Clean Code")
    library.add_book("Design Patterns")
    library.add_book("Refactoring")

    iterator = library.create_iterator()

    print("Books in library:")
    while iterator.has_next():
        print(f"  - {iterator.next()}")

Java Implementation

// BookCollection.java

import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;


// Iterator interface
interface Iterator<T> extends java.util.Iterator<T> {
    boolean hasNext();
    T next();
}


// Iterable interface
interface IterableCollection<T> extends Iterable<T> {
    Iterator<T> createIterator();

    @Override
    default Iterator<T> iterator() {
        return createIterator();
    }
}


// Concrete Iterator
class BookIterator implements Iterator<String> {
    private final List<String> books;
    private int index = 0;

    public BookIterator(List<String> books) {
        this.books = new ArrayList<>(books); // stable traversal snapshot
    }

    @Override
    public boolean hasNext() {
        return index < books.size();
    }

    @Override
    public String next() {
        if (!hasNext()) {
            throw new NoSuchElementException("Iterator exhausted");
        }
        return books.get(index++);
    }
}


// Concrete Collection
public class BookCollection implements IterableCollection<String> {
    private final List<String> books = new ArrayList<>();

    public void addBook(String book) {
        books.add(book);
    }

    @Override
    public Iterator<String> createIterator() {
        return new BookIterator(books);
    }

    public static void main(String[] args) {
        BookCollection library = new BookCollection();
        library.addBook("The Pragmatic Programmer");
        library.addBook("Clean Code");
        library.addBook("Design Patterns");
        library.addBook("Refactoring");

        Iterator<String> iterator = library.createIterator();

        System.out.println("Books in library:");
        while (iterator.hasNext()) {
            System.out.println("  - " + iterator.next());
        }
    }
}

Output

Books in library:
  - The Pragmatic Programmer
  - Clean Code
  - Design Patterns
  - Refactoring

When to Use It

The teaching interfaces also implement the native protocols: Python supports iter() , next() , and for ; Java supports enhanced for through Iterable . Exhaustion raises StopIteration in Python and NoSuchElementException in Java. Each iterator takes a shallow snapshot at creation: later additions do not affect an active traversal. This costs O(n) time and references per iterator, while preserving independent positions; the referenced elements themselves are not deep-copied.

2. Command

Intent

The Command pattern encapsulates a request as a standalone object , which contains all the information needed to perform the action: the receiver, the method to call, and the arguments. This decouples the object that sends the request (the invoker ) from the object that actually performs it (the receiver ), and it enables powerful features like undo/redo, request queuing, and operation logging.

The Problem It Solves

Consider a smart home remote control. Calling every device directly couples the remote to device-specific operations. Command moves those operations behind a common interface. For this example, each button press also records the receiver’s previous state, so the remote can undo actions without knowing how a light works. Undo is an extension used here, not a requirement of the pattern.

Structure

The pattern has four participants:

Python Implementation

from abc import ABC, abstractmethod
from typing import List


# Command interface
class Command(ABC):
    @abstractmethod
    def execute(self) -> None:
        pass

    @abstractmethod
    def undo(self) -> None:
        pass


# Receiver
class Light:
    def __init__(self, location: str):
        self.location = location
        self._is_on = False

    @property
    def is_on(self) -> bool:
        return self._is_on

    def turn_on(self) -> None:
        self._is_on = True
        print(f"{self.location} light turned ON")

    def turn_off(self) -> None:
        self._is_on = False
        print(f"{self.location} light turned OFF")


# Concrete Commands
class TurnOnCommand(Command):
    def __init__(self, light: Light):
        self._light = light
        self._previous_states: list[bool] = []

    def execute(self) -> None:
        previous = self._light.is_on
        self._light.turn_on()
        self._previous_states.append(previous)

    def undo(self) -> None:
        if not self._previous_states:
            raise RuntimeError("Command has no execution to undo")
        previous = self._previous_states[-1]
        if previous:
            self._light.turn_on()
        else:
            self._light.turn_off()
        self._previous_states.pop()

class TurnOffCommand(Command):
    def __init__(self, light: Light):
        self._light = light
        self._previous_states: list[bool] = []

    def execute(self) -> None:
        previous = self._light.is_on
        self._light.turn_off()
        self._previous_states.append(previous)

    def undo(self) -> None:
        if not self._previous_states:
            raise RuntimeError("Command has no execution to undo")
        previous = self._previous_states[-1]
        if previous:
            self._light.turn_on()
        else:
            self._light.turn_off()
        self._previous_states.pop()

# Invoker
class RemoteControl:
    def __init__(self):
        self._history: List[Command] = []

    def press(self, command: Command) -> None:
        command.execute()
        self._history.append(command)

    def press_undo(self) -> None:
        if self._history:
            command = self._history[-1]
            command.undo()
            self._history.pop()
        else:
            print("Nothing to undo")


# Example usage
if __name__ == "__main__":
    remote = RemoteControl()

    living_room = Light("Living room")
    bedroom = Light("Bedroom")

    remote.press(TurnOnCommand(living_room))
    remote.press(TurnOnCommand(bedroom))
    remote.press(TurnOffCommand(living_room))

    print("\n-- Undo last 2 commands --")
    remote.press_undo()
    remote.press_undo()

Java Implementation

// RemoteControl.java

import java.util.ArrayDeque;
import java.util.Deque;


// Command interface
interface Command {
    void execute();
    void undo();
}


// Receiver
class Light {
    private final String location;
    private boolean isOn = false;

    public Light(String location) {
        this.location = location;
    }

    public boolean isOn() {
        return isOn;
    }

    public void turnOn() {
        isOn = true;
        System.out.println(location + " light turned ON");
    }

    public void turnOff() {
        isOn = false;
        System.out.println(location + " light turned OFF");
    }
}


// Concrete Commands
class TurnOnCommand implements Command {
    private final Light light;
    private final Deque<Boolean> previousStates = new ArrayDeque<>();

    public TurnOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        boolean previous = light.isOn();
        light.turnOn();
        previousStates.push(previous);
    }

    @Override
    public void undo() {
        if (previousStates.isEmpty()) {
            throw new IllegalStateException("Command has no execution to undo");
        }
        if (previousStates.peek()) {
            light.turnOn();
        } else {
            light.turnOff();
        }
        previousStates.pop();
    }
}


class TurnOffCommand implements Command {
    private final Light light;
    private final Deque<Boolean> previousStates = new ArrayDeque<>();

    public TurnOffCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        boolean previous = light.isOn();
        light.turnOff();
        previousStates.push(previous);
    }

    @Override
    public void undo() {
        if (previousStates.isEmpty()) {
            throw new IllegalStateException("Command has no execution to undo");
        }
        if (previousStates.peek()) {
            light.turnOn();
        } else {
            light.turnOff();
        }
        previousStates.pop();
    }
}


// Invoker
public class RemoteControl {
    private final Deque<Command> history = new ArrayDeque<>();

    public void press(Command command) {
        command.execute();
        history.push(command);
    }

    public void pressUndo() {
        if (!history.isEmpty()) {
            history.peek().undo();
            history.pop();
        } else {
            System.out.println("Nothing to undo");
        }
    }

    public static void main(String[] args) {
        RemoteControl remote = new RemoteControl();

        Light livingRoom = new Light("Living room");
        Light bedroom    = new Light("Bedroom");

        remote.press(new TurnOnCommand(livingRoom));
        remote.press(new TurnOnCommand(bedroom));
        remote.press(new TurnOffCommand(livingRoom));

        System.out.println("\n-- Undo last 2 commands --");
        remote.pressUndo();
        remote.pressUndo();
    }
}

Output

Living room light turned ON
Bedroom light turned ON
Living room light turned OFF

-- Undo last 2 commands --
Living room light turned ON
Bedroom light turned OFF
Execution and Undo: the State That Must Be Saved
  1. Read the light’s current state, execute the action, then store the successful execution.
  2. Undo restores that saved state; simply calling the opposite action is insufficient when the light was already on or off.
  3. Remove history entries only after undo succeeds. Per-command state stacks allow the same command instance to be reused in one LIFO history.

The example is synchronous and assumes actions on a receiver are undone in reverse execution order through one invoker, without out-of-band changes. It implements undo, not redo; external side effects may require compensation instead of exact reversal.

When to Use It

3. Strategy

Intent

The Strategy pattern defines a family of algorithms, encapsulates each one in its own class, and makes them interchangeable at runtime . The client delegates the algorithm to a strategy object rather than implementing it directly, which means the algorithm can change without altering the client.

The Problem It Solves

Imagine an e-commerce application that calculates shipping costs. The initial implementation uses standard shipping, but you later need to add express shipping, overnight shipping, and in-store pickup, each with its own pricing formula. Without the Strategy pattern, you would end up with a large if/elif/else block or a switch statement inside the shipping calculator, growing with every new option. The Strategy pattern lets you extract each algorithm into its own class and swap them in without touching the calculator itself.

Structure

Python Implementation

from abc import ABC, abstractmethod
from math import isfinite


# Strategy interface
class ShippingStrategy(ABC):
    @abstractmethod
    def calculate(self, order_weight: float) -> float:
        pass


# Concrete Strategies
class StandardShipping(ShippingStrategy):
    def calculate(self, order_weight: float) -> float:
        return order_weight * 1.5

class ExpressShipping(ShippingStrategy):
    def calculate(self, order_weight: float) -> float:
        return order_weight * 3.0 + 5.0

class OvernightShipping(ShippingStrategy):
    def calculate(self, order_weight: float) -> float:
        return order_weight * 5.0 + 15.0


# Context
class ShippingCalculator:
    def __init__(self, strategy: ShippingStrategy):
        self._strategy = strategy

    def set_strategy(self, strategy: ShippingStrategy) -> None:
        self._strategy = strategy

    def get_cost(self, order_weight: float) -> float:
        if not isfinite(order_weight) or order_weight < 0:
            raise ValueError("Order weight must be finite and non-negative")
        return self._strategy.calculate(order_weight)


# Example usage
if __name__ == "__main__":
    calculator = ShippingCalculator(StandardShipping())
    print(f"Standard:  ${calculator.get_cost(10):.2f}")

    calculator.set_strategy(ExpressShipping())
    print(f"Express:   ${calculator.get_cost(10):.2f}")

    calculator.set_strategy(OvernightShipping())
    print(f"Overnight: ${calculator.get_cost(10):.2f}")

Java Implementation

// ShippingCalculator.java

import java.util.Locale;
import java.util.Objects;


// Strategy interface
interface ShippingStrategy {
    double calculate(double orderWeight);
}


// Concrete Strategies
class StandardShipping implements ShippingStrategy {
    @Override
    public double calculate(double orderWeight) {
        return orderWeight * 1.5;
    }
}

class ExpressShipping implements ShippingStrategy {
    @Override
    public double calculate(double orderWeight) {
        return orderWeight * 3.0 + 5.0;
    }
}

class OvernightShipping implements ShippingStrategy {
    @Override
    public double calculate(double orderWeight) {
        return orderWeight * 5.0 + 15.0;
    }
}


// Context
public class ShippingCalculator {
    private ShippingStrategy strategy;

    public ShippingCalculator(ShippingStrategy strategy) {
        this.strategy = Objects.requireNonNull(strategy, "strategy");
    }

    public void setStrategy(ShippingStrategy strategy) {
        this.strategy = Objects.requireNonNull(strategy, "strategy");
    }

    public double getCost(double orderWeight) {
        if (!Double.isFinite(orderWeight) || orderWeight < 0) {
            throw new IllegalArgumentException("Order weight must be finite and non-negative");
        }
        return strategy.calculate(orderWeight);
    }

    public static void main(String[] args) {
        ShippingCalculator calculator = new ShippingCalculator(new StandardShipping());
        System.out.printf(Locale.ROOT, "Standard:  $%.2f%n", calculator.getCost(10));

        calculator.setStrategy(new ExpressShipping());
        System.out.printf(Locale.ROOT, "Express:   $%.2f%n", calculator.getCost(10));

        calculator.setStrategy(new OvernightShipping());
        System.out.printf(Locale.ROOT, "Overnight: $%.2f%n", calculator.getCost(10));
    }
}

Weights are expressed in kilograms, and the rates are illustrative. The context rejects negative and non-finite weights. Floating-point costs keep the example focused on algorithm selection; production money calculations need an explicit decimal or minor-unit representation and rounding policy.

Output

Standard:  $15.00
Express:   $35.00
Overnight: $65.00

Strategy vs. Command

Strategy selects how to perform a calculation; Command packages what action should run. Neither is defined by being stateless or stateful: strategies may hold configuration, and commands do not have to support undo. Choose by the responsibility you need to separate, not by the shape of the interface.

When to Use It

4. Observer

Intent

The Observer pattern defines a one-to-many dependency between objects so that when one object (the subject or publisher ) changes state, all its dependents ( observers or subscribers ) are notified and updated automatically. This is the backbone of event-driven systems.

The Problem It Solves

Suppose you are building a weather station application. The station reads the current temperature and needs to update a display panel, a mobile app, and a logging service every time the temperature changes. Without the Observer pattern, the weather station would need a direct reference to every one of these components and would call their update methods manually. Adding a new subscriber (say, an alert service) would require modifying the station itself. The Observer pattern inverts this: the station simply notifies all registered observers when its state changes, without caring who they are.

Structure

Python Implementation

from abc import ABC, abstractmethod
from typing import List


# Observer interface
class Observer(ABC):
    @abstractmethod
    def update(self, temperature: float) -> None:
        pass


# Subject
class WeatherStation:
    def __init__(self):
        self._observers: List[Observer] = []
        self._temperature: float = 0.0

    def subscribe(self, observer: Observer) -> None:
        if not any(current is observer for current in self._observers):
            self._observers.append(observer)

    def unsubscribe(self, observer: Observer) -> None:
        self._observers[:] = [
            current for current in self._observers if current is not observer
        ]

    def set_temperature(self, temperature: float) -> None:
        print(f"\nWeatherStation: temperature changed to {temperature}°C")
        self._temperature = temperature
        self._notify()

    def _notify(self) -> None:
        temperature = self._temperature
        for observer in tuple(self._observers):
            observer.update(temperature)


# Concrete Observers
class DisplayPanel(Observer):
    def update(self, temperature: float) -> None:
        print(f"  DisplayPanel: showing {temperature}°C")

class MobileApp(Observer):
    def update(self, temperature: float) -> None:
        print(f"  MobileApp: pushing notification: {temperature}°C")

class Logger(Observer):
    def update(self, temperature: float) -> None:
        print(f"  Logger: recorded {temperature}°C")


# Example usage
if __name__ == "__main__":
    station = WeatherStation()

    display = DisplayPanel()
    app     = MobileApp()
    logger  = Logger()

    station.subscribe(display)
    station.subscribe(app)
    station.subscribe(logger)

    station.set_temperature(22.5)
    station.set_temperature(19.0)

    print("\n-- Unsubscribing MobileApp --")
    station.unsubscribe(app)
    station.set_temperature(25.0)

Java Implementation

// WeatherStation.java

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;


// Observer interface
interface Observer {
    void update(double temperature);
}


// Subject
public class WeatherStation {
    private final List<Observer> observers = new ArrayList<>();
    private double temperature;

    public void subscribe(Observer observer) {
        Objects.requireNonNull(observer, "observer");
        if (!observers.contains(observer)) {
            observers.add(observer);
        }
    }

    public void unsubscribe(Observer observer) {
        observers.remove(observer);
    }

    public void setTemperature(double temperature) {
        System.out.printf(Locale.ROOT, "%nWeatherStation: temperature changed to %.1f°C%n", temperature);
        this.temperature = temperature;
        notifyObservers();
    }

    private void notifyObservers() {
        double eventTemperature = temperature;
        for (Observer observer : new ArrayList<>(observers)) {
            observer.update(eventTemperature);
        }
    }

    public static void main(String[] args) {
        WeatherStation station = new WeatherStation();

        DisplayPanel display = new DisplayPanel();
        MobileApp    app     = new MobileApp();
        Logger       logger  = new Logger();

        station.subscribe(display);
        station.subscribe(app);
        station.subscribe(logger);

        station.setTemperature(22.5);
        station.setTemperature(19.0);

        System.out.println("\n-- Unsubscribing MobileApp --");
        station.unsubscribe(app);
        station.setTemperature(25.0);
    }
}


// Concrete Observers
class DisplayPanel implements Observer {
    @Override
    public void update(double temperature) {
        System.out.printf(Locale.ROOT, "  DisplayPanel: showing %.1f°C%n", temperature);
    }
}

class MobileApp implements Observer {
    @Override
    public void update(double temperature) {
        System.out.printf(Locale.ROOT, "  MobileApp: pushing notification: %.1f°C%n", temperature);
    }
}

class Logger implements Observer {
    @Override
    public void update(double temperature) {
        System.out.printf(Locale.ROOT, "  Logger: recorded %.1f°C%n", temperature);
    }
}

Output


WeatherStation: temperature changed to 22.5°C
  DisplayPanel: showing 22.5°C
  MobileApp: pushing notification: 22.5°C
  Logger: recorded 22.5°C

WeatherStation: temperature changed to 19.0°C
  DisplayPanel: showing 19.0°C
  MobileApp: pushing notification: 19.0°C
  Logger: recorded 19.0°C

-- Unsubscribing MobileApp --

WeatherStation: temperature changed to 25.0°C
  DisplayPanel: showing 25.0°C
  Logger: recorded 25.0°C

Notification is synchronous and uses a subscription snapshot plus a captured event value. Duplicate subscriptions are ignored, and removing a missing observer is harmless. Changes made inside a callback affect the next notification: an observer already in the current snapshot still receives the current event. Callback exceptions propagate and stop that notification; delivery is neither asynchronous nor fault-isolated. The example does not coordinate concurrent or reentrant updates.

When to Use It

5. Chain of Responsibility

Intent

The Chain of Responsibility pattern lets you pass a request along a chain of handlers . Each handler decides either to process the request or to pass it to the next handler in the chain. This decouples the sender of a request from its receivers, giving multiple objects a chance to handle it.

The Problem It Solves

Consider a technical support system where user tickets are assigned a severity level. A basic issue can be resolved by a Level 1 agent, a medium issue escalates to Level 2, and a critical issue goes to Level 3. Without this pattern, the dispatcher would contain a nested chain of conditionals, tightly coupling it to every support tier. The Chain of Responsibility lets each handler decide whether to handle or forward the request, and new tiers can be inserted without modifying existing code.

Structure

Python Implementation

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Optional


# Handler interface + base class
class SupportHandler(ABC):
    def __init__(self):
        self._next: Optional[SupportHandler] = None

    def set_next(self, handler: SupportHandler) -> SupportHandler:
        if handler is None:
            raise ValueError("Next handler cannot be None")
        current = handler
        while current is not None:
            if current is self:
                raise ValueError("A handler chain cannot contain a cycle")
            current = current._next
        self._next = handler
        return handler  # allows chaining: h1.set_next(h2).set_next(h3)

    @abstractmethod
    def handle(self, severity: int, description: str) -> None:
        pass

    def _forward(self, severity: int, description: str) -> None:
        if self._next:
            self._next.handle(severity, description)
        else:
            print(f"  No handler available for severity {severity}: {description}")


# Concrete Handlers
class Level1Support(SupportHandler):
    def handle(self, severity: int, description: str) -> None:
        if severity == 1:
            print(f"Level1Support resolved: {description}")
        else:
            self._forward(severity, description)

class Level2Support(SupportHandler):
    def handle(self, severity: int, description: str) -> None:
        if severity == 2:
            print(f"Level2Support resolved: {description}")
        else:
            self._forward(severity, description)

class Level3Support(SupportHandler):
    def handle(self, severity: int, description: str) -> None:
        if severity == 3:
            print(f"Level3Support resolved: {description}")
        else:
            self._forward(severity, description)


# Example usage
if __name__ == "__main__":
    l1 = Level1Support()
    l2 = Level2Support()
    l3 = Level3Support()

    l1.set_next(l2).set_next(l3)

    tickets = [
        (1, "Password reset request"),
        (2, "Application crash on login"),
        (3, "Data breach detected"),
        (4, "Unknown severity issue"),
    ]

    for severity, description in tickets:
        l1.handle(severity, description)

Java Implementation

// SupportChain.java

import java.util.Objects;


// Handler interface
abstract class SupportHandler {
    private SupportHandler next;

    public SupportHandler setNext(SupportHandler handler) {
        Objects.requireNonNull(handler, "handler");
        for (SupportHandler current = handler; current != null; current = current.next) {
            if (current == this) {
                throw new IllegalArgumentException("A handler chain cannot contain a cycle");
            }
        }
        this.next = handler;
        return handler;
    }

    public abstract void handle(int severity, String description);

    protected void forward(int severity, String description) {
        if (next != null) {
            next.handle(severity, description);
        } else {
            System.out.println("  No handler available for severity " + severity + ": " + description);
        }
    }
}


// Concrete Handlers
class Level1Support extends SupportHandler {
    @Override
    public void handle(int severity, String description) {
        if (severity == 1) System.out.println("Level1Support resolved: " + description);
        else forward(severity, description);
    }
}

class Level2Support extends SupportHandler {
    @Override
    public void handle(int severity, String description) {
        if (severity == 2) System.out.println("Level2Support resolved: " + description);
        else forward(severity, description);
    }
}

class Level3Support extends SupportHandler {
    @Override
    public void handle(int severity, String description) {
        if (severity == 3) System.out.println("Level3Support resolved: " + description);
        else forward(severity, description);
    }
}


// Entry point
public class SupportChain {
    public static void main(String[] args) {
        Level1Support l1 = new Level1Support();
        Level2Support l2 = new Level2Support();
        Level3Support l3 = new Level3Support();

        l1.setNext(l2).setNext(l3);

        int[] severities = {1, 2, 3, 4};
        String[] descriptions = {
            "Password reset request",
            "Application crash on login",
            "Data breach detected",
            "Unknown severity issue"
        };

        for (int i = 0; i < descriptions.length; i++) {
            l1.handle(severities[i], descriptions[i]);
        }
    }
}

Output

Level1Support resolved: Password reset request
Level2Support resolved: Application crash on login
Level3Support resolved: Data breach detected
  No handler available for severity 4: Unknown severity issue

This example stops at the first matching handler and reports unhandled severities explicitly. Middleware may instead process a request and continue, including work on the return path. Link creation rejects cycles to prevent infinite forwarding; the chain is configured before requests are processed.

When to Use It

6. Template Method

Intent

The Template Method pattern defines the skeleton of an algorithm in a base class , deferring some steps to subclasses. Subclasses can override specific steps without changing the algorithm's overall structure. This is a classic application of the Hollywood Principle: "Don't call us, we'll call you."

The Problem It Solves

Suppose CSV and XML reports share the same processing sequence: extract the source, parse records, analyze them, and send a report. The variable extraction and parsing steps belong in subclasses; the base class keeps the orchestration in one place. The two implementations below use in-memory fixtures, so they run without external files; path is a source label, not a file that is opened.

Structure

Python Implementation

from abc import ABC, abstractmethod
import csv
from io import StringIO
from typing import final
import xml.etree.ElementTree as ET


# Abstract Class
class DataMiner(ABC):
    @final  # checked by type checkers; not enforced by the Python runtime
    def mine(self, path: str) -> None:
        raw = self.extract_data(path)
        data = self.parse_data(raw)
        analysis = self.analyze_data(data)
        self.send_report(analysis)

    @abstractmethod
    def extract_data(self, path: str) -> str:
        pass

    @abstractmethod
    def parse_data(self, raw: str) -> list:
        pass

    def analyze_data(self, data: list) -> dict:
        return {"total_records": len(data), "sample": data[0] if data else None}

    def send_report(self, analysis: dict) -> None:
        sample = analysis["sample"]
        if isinstance(sample, list):
            sample = ",".join(sample)
        elif sample is None:
            sample = "<empty>"
        print(f"  Report: total_records={analysis['total_records']}, sample={sample}")


# In-memory fixtures keep this example runnable without external files.
class CSVDataMiner(DataMiner):
    def extract_data(self, path: str) -> str:
        print(f"CSV: demo data for {path}")
        return "Alice,30\nBob,25\nCarol,35"

    def parse_data(self, raw: str) -> list:
        return [row for row in csv.reader(StringIO(raw)) if row]


class XMLDataMiner(DataMiner):
    def extract_data(self, path: str) -> str:
        print(f"XML: demo data for {path}")
        return "<users><user>Alice</user><user>Bob</user></users>"

    def parse_data(self, raw: str) -> list:
        root = ET.fromstring(raw)
        if root.tag != "users":
            raise ValueError("Expected a <users> root element")
        return ["".join(user.itertext()).strip() for user in root.findall("user")]


if __name__ == "__main__":
    print("=== CSV Mining ===")
    CSVDataMiner().mine("data.csv")

    print("\n=== XML Mining ===")
    XMLDataMiner().mine("data.xml")

Java Implementation

// DataMiner.java

import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;


// Abstract Class and entry point: only one DataMiner declaration.
public abstract class DataMiner {
    public final void mine(String path) {
        String raw = extractData(path);
        String[] data = parseData(raw);
        String analysis = analyzeData(data);
        sendReport(analysis);
    }

    protected abstract String extractData(String path);
    protected abstract String[] parseData(String raw);

    protected String analyzeData(String[] data) {
        String sample = data.length == 0 ? "<empty>" : data[0];
        return "total_records=" + data.length + ", sample=" + sample;
    }

    protected void sendReport(String analysis) {
        System.out.println("  Report: " + analysis);
    }

    public static void main(String[] args) {
        System.out.println("=== CSV Mining ===");
        new CSVDataMiner().mine("data.csv");

        System.out.println("\n=== XML Mining ===");
        new XMLDataMiner().mine("data.xml");
    }
}


class CSVDataMiner extends DataMiner {
    @Override
    protected String extractData(String path) {
        System.out.println("CSV: demo data for " + path);
        return "Alice,30\nBob,25\nCarol,35";
    }

    @Override
    protected String[] parseData(String raw) {
        // Demo subset: one record per line, without quoted CSV fields.
        // Use a dedicated CSV library for general CSV input.
        if (raw.indexOf('"') >= 0) {
            throw new IllegalArgumentException("Quoted CSV fields are outside this demo's scope");
        }
        List<String> records = new ArrayList<>();
        for (String line : raw.split("\\R")) {
            if (!line.trim().isEmpty()) {
                records.add(line);
            }
        }
        return records.toArray(new String[0]);
    }
}


class XMLDataMiner extends DataMiner {
    @Override
    protected String extractData(String path) {
        System.out.println("XML: demo data for " + path);
        return "<users><user>Alice</user><user>Bob</user></users>";
    }

    @Override
    protected String[] parseData(String raw) {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
            factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
            factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
            Element root = factory.newDocumentBuilder()
                    .parse(new InputSource(new StringReader(raw)))
                    .getDocumentElement();
            if (!"users".equals(root.getTagName())) {
                throw new IllegalArgumentException("Expected a <users> root element");
            }
            List<String> users = new ArrayList<>();
            NodeList children = root.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                Node node = children.item(i);
                if (node.getNodeType() == Node.ELEMENT_NODE
                        && "user".equals(node.getNodeName())) {
                    users.add(node.getTextContent().trim());
                }
            }
            return users.toArray(new String[0]);
        } catch (Exception exception) {
            throw new IllegalArgumentException("Cannot parse users XML", exception);
        }
    }
}

The Python version uses csv.reader and an XML parser, not comma splitting or regular expressions for general structured input. The dependency-free Java CSV example intentionally accepts only unquoted, one-record-per-line input and rejects quoted fields; use a CSV library for general CSV. Java XML parsing disables DTDs and external resources. Both reports handle zero records and produce the same displayed output for the fixtures. These are teaching examples, not a complete ingestion or untrusted-input security layer.

final prevents overriding mine() in Java. Python’s @final documents the same contract for type checkers but does not enforce it at runtime.

Output

=== CSV Mining ===
CSV: demo data for data.csv
  Report: total_records=3, sample=Alice,30

=== XML Mining ===
XML: demo data for data.xml
  Report: total_records=2, sample=Alice

Template Method vs. Strategy

Template Method fixes the orchestration in a base class and varies selected steps through inheritance. Strategy injects a replaceable collaborator. Use the former when subclasses must follow a common sequence; use the latter when clients should switch an algorithm independently of their class hierarchy. Boilerplate depends on the language: Python strategies can also be callables.

When to Use It

7. State

Intent

The State pattern allows an object to alter its behavior when its internal state changes . The object will appear to change its class. Instead of encoding all state-dependent behavior in one monolithic class using if / switch statements, each state is extracted into its own class.

The Problem It Solves

Consider a simplified vending machine with three states: idle, coin inserted, and dispensing after an item is selected. A single class with several flags would mix valid transitions with actions that must be rejected. Here each state owns those decisions, while the machine delegates actions to its current state. Stock, payment amounts, cancellation, and hardware failures are deliberately outside the example’s scope.

Structure

Vending machine state transitions Idle changes to HasCoin on insert_coin. HasCoin changes to Dispensing on select_item. Dispensing returns to Idle on dispense. Other actions only print a message and leave the state unchanged. IdleState HasCoinState DispensingState insert_coin() select_item() dispense()
Only the three successful actions change state; rejected actions preserve it.

Python Implementation

from __future__ import annotations
from abc import ABC, abstractmethod


# State interface
class VendingState(ABC):
    @abstractmethod
    def insert_coin(self, machine: VendingMachine) -> None:
        pass

    @abstractmethod
    def select_item(self, machine: VendingMachine) -> None:
        pass

    @abstractmethod
    def dispense(self, machine: VendingMachine) -> None:
        pass


# Concrete States
class IdleState(VendingState):
    def insert_coin(self, machine: VendingMachine) -> None:
        print("Coin inserted.")
        machine.set_state(HasCoinState())

    def select_item(self, machine: VendingMachine) -> None:
        print("Please insert a coin first.")

    def dispense(self, machine: VendingMachine) -> None:
        print("Please insert a coin first.")


class HasCoinState(VendingState):
    def insert_coin(self, machine: VendingMachine) -> None:
        print("Coin already inserted.")

    def select_item(self, machine: VendingMachine) -> None:
        print("Item selected.")
        machine.set_state(DispensingState())

    def dispense(self, machine: VendingMachine) -> None:
        print("Please select an item first.")


class DispensingState(VendingState):
    def insert_coin(self, machine: VendingMachine) -> None:
        print("Please wait, dispensing in progress.")

    def select_item(self, machine: VendingMachine) -> None:
        print("Please wait, dispensing in progress.")

    def dispense(self, machine: VendingMachine) -> None:
        print("Item dispensed. Enjoy!")
        machine.set_state(IdleState())


# Context
class VendingMachine:
    def __init__(self):
        self._state: VendingState = IdleState()

    def set_state(self, state: VendingState) -> None:
        self._state = state

    def insert_coin(self) -> None:
        self._state.insert_coin(self)

    def select_item(self) -> None:
        self._state.select_item(self)

    def dispense(self) -> None:
        self._state.dispense(self)


# Example usage
if __name__ == "__main__":
    machine = VendingMachine()

    machine.select_item()     # Error: no coin
    machine.insert_coin()     # OK
    machine.insert_coin()     # Error: already inserted
    machine.select_item()     # OK
    machine.dispense()        # OK
    machine.dispense()        # Error: back to idle

Output

Please insert a coin first.
Coin inserted.
Coin already inserted.
Item selected.
Item dispensed. Enjoy!
Please insert a coin first.

When to Use It

8. Mediator

Intent

The Mediator pattern defines an object ( the mediator ) that encapsulates how a set of objects interact . It promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently. Instead of many-to-many communication between objects, all communication is routed through the mediator.

The Problem It Solves

In a chat room application, each user might need to send messages to every other user. Without a mediator, every User object would hold a reference to every other User , creating a tangled web of dependencies. Adding a new user means updating all existing users. The mediator, the ChatRoom , centralizes this: every user sends messages to the chat room, and the chat room decides how to route them. Users only know about the mediator, not about each other.

Structure

Python Implementation

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List, Optional


# Mediator interface
class Mediator(ABC):
    @abstractmethod
    def send_message(self, message: str, sender: User) -> None:
        pass


# Concrete Mediator
class ChatRoom(Mediator):
    def __init__(self):
        self._users: List[User] = []

    def register(self, user: User) -> None:
        if user.mediator is not None and user.mediator is not self:
            raise ValueError("Unregister the user from its current room first")
        if not any(current is user for current in self._users):
            self._users.append(user)
        user.set_mediator(self)

    def unregister(self, user: User) -> None:
        self._users[:] = [current for current in self._users if current is not user]
        if user.mediator is self:
            user.set_mediator(None)

    def send_message(self, message: str, sender: User) -> None:
        if not any(user is sender for user in self._users) or sender.mediator is not self:
            raise ValueError("Sender must be registered in this room")
        for user in tuple(self._users):
            if user is not sender:
                user.receive(message, sender.name)


# Colleague
class User:
    def __init__(self, name: str):
        self.name = name
        self._mediator: Optional[Mediator] = None

    @property
    def mediator(self) -> Optional[Mediator]:
        return self._mediator

    # Called by ChatRoom.register/unregister to keep both sides in sync.
    def set_mediator(self, mediator: Optional[Mediator]) -> None:
        self._mediator = mediator

    def send(self, message: str) -> None:
        if self._mediator is None:
            raise RuntimeError("Register the user before sending messages")
        print(f"{self.name} sends: '{message}'")
        self._mediator.send_message(message, self)

    def receive(self, message: str, sender_name: str) -> None:
        print(f"  {self.name} received from {sender_name}: '{message}'")


# Example usage
if __name__ == "__main__":
    room = ChatRoom()

    alice = User("Alice")
    bob   = User("Bob")
    carol = User("Carol")

    room.register(alice)
    room.register(bob)
    room.register(carol)

    alice.send("Hello everyone!")
    bob.send("Hey Alice!")

Output

Alice sends: 'Hello everyone!'
  Bob received from Alice: 'Hello everyone!'
  Carol received from Alice: 'Hello everyone!'
Bob sends: 'Hey Alice!'
  Alice received from Bob: 'Hey Alice!'
  Carol received from Bob: 'Hey Alice!'

Mediator vs. Observer

Observer expresses a subscription relationship: a subject notifies dependents of an event or state change, using either push data or a pull-style update. Mediator encapsulates coordination rules among colleagues; it need not broadcast. This chat room broadcasts to other members, but a dialog mediator might enable only one control. The two patterns can coexist, so bidirectional observation alone is not a sufficient reason to replace Observer.

Registration is idempotent. Sending without a room raises an explicit error; a room also rejects senders that are not members. A user must leave its current room before joining another, and unregister() clears the association. Registration methods maintain the link on both sides; calling set_mediator() directly is not the membership API. Routing uses a membership snapshot and remains synchronous.

When to Use It

9. Memento

Intent

The Memento pattern captures an object's state so it can be restored later, without requiring the object that stores the snapshot to understand that state.

The Problem It Solves

A text editor must restore both its content and its cursor position when a user returns to a checkpoint. If the history manager copies those fields directly, it becomes coupled to the editor's internal representation. Adding selection or formatting state would then require changes outside the editor. Memento keeps snapshot creation and restoration inside the object that owns the state.

Structure

Saving and restoring an editor snapshot History asks Editor to save its state. Editor creates an immutable memento containing text and cursor. History stores the opaque memento and later passes it back to Editor to restore the saved state. History Caretaker Editor Originator Memento text + cursor 1. save() 2. create 3. Return snapshot to History; store without inspecting it 4. restore(snapshot) on undo Only Editor interprets the saved state.
The caretaker owns the history, not the editor's state representation. Dashed flow shows a later restore request.
From Checkpoint to Undo
  1. Before an edit, backup() asks the editor to create a memento and pushes it onto a stack.
  2. The editor changes independently; the saved checkpoint remains unchanged.
  3. undo() passes the latest memento back to the editor and removes it only after restoration succeeds.

The example saves two checkpoints, then restores them in reverse order. It implements undo only, not redo or automatic checkpoint creation.

Python Implementation

from __future__ import annotations
from abc import ABC
from dataclasses import dataclass


# Narrow interface: the caretaker cannot access state through this API.
class Memento(ABC):
    pass


@dataclass(frozen=True)
class _EditorSnapshot(Memento):
    _owner: object
    _text: str
    _cursor: int


# Originator
class Editor:
    def __init__(self, text: str = "", cursor: int = 0):
        self.__owner = object()
        self.replace(text, cursor)

    @property
    def text(self) -> str:
        return self.__text

    @property
    def cursor(self) -> int:
        return self.__cursor

    def replace(self, text: str, cursor: int) -> None:
        if not 0 <= cursor <= len(text):
            raise ValueError("Cursor must be inside the text")
        self.__text = text
        self.__cursor = cursor

    def save(self) -> Memento:
        return _EditorSnapshot(self.__owner, self.__text, self.__cursor)

    def restore(self, memento: Memento) -> None:
        if not isinstance(memento, _EditorSnapshot):
            raise ValueError("Unsupported memento")
        if memento._owner is not self.__owner:
            raise ValueError("Memento belongs to another editor")
        self.replace(memento._text, memento._cursor)


# Caretaker
class History:
    def __init__(self, editor: Editor):
        self._editor = editor
        self._snapshots: list[Memento] = []

    def backup(self) -> None:
        self._snapshots.append(self._editor.save())

    def undo(self) -> bool:
        if not self._snapshots:
            return False
        self._editor.restore(self._snapshots[-1])
        self._snapshots.pop()  # discard only after a successful restore
        return True


if __name__ == "__main__":
    editor = Editor("Draft", 5)
    history = History(editor)

    history.backup()
    editor.replace("Draft v2", 8)
    history.backup()
    editor.replace("Final", 5)
    print(f"Current: {editor.text} (cursor={editor.cursor})")

    while history.undo():
        print(f"Restored: {editor.text} (cursor={editor.cursor})")
    print("Nothing to undo")

Java Implementation

// MementoDemo.java

import java.util.ArrayDeque;
import java.util.Deque;


// Narrow interface used by the caretaker.
interface Memento {}


// Originator
final class Editor {
    private String text;
    private int cursor;

    private static final class Snapshot implements Memento {
        private final Editor owner;
        private final String text;
        private final int cursor;

        private Snapshot(Editor owner, String text, int cursor) {
            this.owner = owner;
            this.text = text;
            this.cursor = cursor;
        }
    }

    public Editor(String text, int cursor) {
        replace(text, cursor);
    }

    public String getText() { return text; }
    public int getCursor() { return cursor; }

    public void replace(String text, int cursor) {
        if (text == null || cursor < 0 || cursor > text.length()) {
            throw new IllegalArgumentException("Invalid text or cursor");
        }
        this.text = text;
        this.cursor = cursor;
    }

    public Memento save() {
        return new Snapshot(this, text, cursor);
    }

    public void restore(Memento memento) {
        if (!(memento instanceof Snapshot)) {
            throw new IllegalArgumentException("Unsupported memento");
        }
        Snapshot snapshot = (Snapshot) memento;
        if (snapshot.owner != this) {
            throw new IllegalArgumentException("Memento belongs to another editor");
        }
        replace(snapshot.text, snapshot.cursor);
    }
}


// Caretaker
final class History {
    private final Editor editor;
    private final Deque<Memento> snapshots = new ArrayDeque<>();

    public History(Editor editor) {
        this.editor = editor;
    }

    public void backup() {
        snapshots.push(editor.save());
    }

    public boolean undo() {
        if (snapshots.isEmpty()) return false;
        editor.restore(snapshots.peek());
        snapshots.pop();
        return true;
    }
}


public class MementoDemo {
    public static void main(String[] args) {
        Editor editor = new Editor("Draft", 5);
        History history = new History(editor);

        history.backup();
        editor.replace("Draft v2", 8);
        history.backup();
        editor.replace("Final", 5);
        System.out.println("Current: " + editor.getText()
                + " (cursor=" + editor.getCursor() + ")");

        while (history.undo()) {
            System.out.println("Restored: " + editor.getText()
                    + " (cursor=" + editor.getCursor() + ")");
        }
        System.out.println("Nothing to undo");
    }
}

Output

Current: Final (cursor=5)
Restored: Draft v2 (cursor=8)
Restored: Draft (cursor=5)
Nothing to undo

Snapshot isolation matters more than copying every field mechanically. Here, strings and cursor values are immutable, so later edits cannot mutate a saved checkpoint. Mutable lists or nested objects would require defensive copies, immutable structures, or a carefully defined sharing policy. Retaining many versions can consume significant memory, so real histories often impose a size limit.

Java hides the snapshot class inside Editor. Python uses a frozen dataclass and internal attributes by convention; these discourage accidental changes but are not a security boundary. The caretaker in both implementations uses only save() and restore(), never snapshot fields.

Relationship to Command

Command represents an action; Memento represents a saved state. A reversible command can keep a memento instead of learning the receiver's internals. Restoring an in-memory checkpoint does not reverse external effects such as a sent message or a completed payment.

When to Use It

10. Visitor

Intent

The Visitor pattern separates operations from the objects they work on. Once an element hierarchy supports accept(visitor), a new operation can be added as a visitor without changing every element class.

The Problem It Solves

A drawing contains circles and rectangles. Calculating area and generating descriptions are distinct operations, but putting both into every shape class makes that hierarchy grow with each reporting requirement. Moving them into external functions with repeated type checks spreads knowledge of concrete shapes across the application. Visitor groups each operation in one class while preserving type-specific behavior.

Structure

Visitor dispatch for a heterogeneous shape collection The client calls accept on a Circle or Rectangle with an AreaVisitor. Each concrete shape calls its corresponding visit method. AreaVisitor computes the result. A DescriptionVisitor can follow the same route without changing either shape class. Client list of Shape Circle.accept() concrete element Rectangle.accept() concrete element AreaVisitor operation logic 1. accept(visitor) 2. visit_circle(self) 2. visit_rectangle(self) New operation: pass a different visitor through the same accept() methods.
The element selects the type-specific visit method; the concrete visitor supplies its implementation. Java uses overloaded visit methods for the same two routes.
Two Dispatch Steps, One Operation
  1. The client calls shape.accept(visitor); the concrete shape determines which accept() implementation runs.
  2. That method calls the shape-specific visit method on the visitor, passing the shape itself.
  3. The concrete visitor determines the operation to run. The same list can then accept a different visitor.

This is double dispatch: behavior depends on both the element and the visitor. Python names the routes explicitly, such as visit_circle(). Java resolves the overloaded visit(this) signature inside each concrete shape, then dynamically dispatches to the concrete visitor implementation. Calling an overloaded method directly with a variable typed only as Shape would not provide this behavior.

Python Implementation

from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from math import pi
from typing import Generic, TypeVar


R = TypeVar("R")


class ShapeVisitor(ABC, Generic[R]):
    @abstractmethod
    def visit_circle(self, circle: Circle) -> R:
        pass

    @abstractmethod
    def visit_rectangle(self, rectangle: Rectangle) -> R:
        pass


class Shape(ABC):
    @abstractmethod
    def accept(self, visitor: ShapeVisitor[R]) -> R:
        pass


@dataclass(frozen=True)
class Circle(Shape):
    radius: float

    def accept(self, visitor: ShapeVisitor[R]) -> R:
        return visitor.visit_circle(self)


@dataclass(frozen=True)
class Rectangle(Shape):
    width: float
    height: float

    def accept(self, visitor: ShapeVisitor[R]) -> R:
        return visitor.visit_rectangle(self)


class AreaVisitor(ShapeVisitor[float]):
    def visit_circle(self, circle: Circle) -> float:
        return pi * circle.radius ** 2

    def visit_rectangle(self, rectangle: Rectangle) -> float:
        return rectangle.width * rectangle.height


class DescriptionVisitor(ShapeVisitor[str]):
    def visit_circle(self, circle: Circle) -> str:
        return f"Circle(radius={circle.radius:.1f})"

    def visit_rectangle(self, rectangle: Rectangle) -> str:
        return f"Rectangle(width={rectangle.width:.1f}, height={rectangle.height:.1f})"


if __name__ == "__main__":
    shapes: list[Shape] = [Circle(2.0), Rectangle(3.0, 4.0)]
    area = AreaVisitor()
    description = DescriptionVisitor()

    for shape in shapes:
        print(f"{shape.accept(description)}: area={shape.accept(area):.2f}")

Java Implementation

// VisitorDemo.java

import java.util.Arrays;
import java.util.List;
import java.util.Locale;


interface ShapeVisitor<R> {
    R visit(Circle circle);
    R visit(Rectangle rectangle);
}


interface Shape {
    <R> R accept(ShapeVisitor<R> visitor);
}


final class Circle implements Shape {
    private final double radius;

    public Circle(double radius) { this.radius = radius; }
    public double getRadius() { return radius; }

    @Override
    public <R> R accept(ShapeVisitor<R> visitor) {
        return visitor.visit(this);  // this has static type Circle
    }
}


final class Rectangle implements Shape {
    private final double width;
    private final double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double getWidth() { return width; }
    public double getHeight() { return height; }

    @Override
    public <R> R accept(ShapeVisitor<R> visitor) {
        return visitor.visit(this);  // this has static type Rectangle
    }
}


final class AreaVisitor implements ShapeVisitor<Double> {
    @Override
    public Double visit(Circle circle) {
        return Math.PI * circle.getRadius() * circle.getRadius();
    }

    @Override
    public Double visit(Rectangle rectangle) {
        return rectangle.getWidth() * rectangle.getHeight();
    }
}


final class DescriptionVisitor implements ShapeVisitor<String> {
    @Override
    public String visit(Circle circle) {
        return String.format(Locale.ROOT, "Circle(radius=%.1f)", circle.getRadius());
    }

    @Override
    public String visit(Rectangle rectangle) {
        return String.format(Locale.ROOT, "Rectangle(width=%.1f, height=%.1f)",
                rectangle.getWidth(), rectangle.getHeight());
    }
}


public class VisitorDemo {
    public static void main(String[] args) {
        List<Shape> shapes = Arrays.asList(new Circle(2.0), new Rectangle(3.0, 4.0));
        ShapeVisitor<Double> area = new AreaVisitor();
        ShapeVisitor<String> description = new DescriptionVisitor();

        for (Shape shape : shapes) {
            System.out.printf(Locale.ROOT, "%s: area=%.2f%n",
                    shape.accept(description), shape.accept(area));
        }
    }
}

Output

Circle(radius=2.0): area=12.57
Rectangle(width=3.0, height=4.0): area=12.00

The generic result type lets each visitor return its own kind of value without mixing printing into the element classes. Shapes in this demo contain trusted, non-negative dimensions; validation and units are outside its scope. Visitors use public shape data, not private-state access.

Visitor makes new operations inexpensive, but new element types more expensive. Adding a Triangle requires a new visit method and updates to every concrete visitor. Prefer it when the element types are relatively stable and operations change frequently; for a small, stable operation such as area alone, ordinary shape methods may be simpler.

When to Use It

11. Detailed Comparison

Pattern Selection Guide

Choosing the right Behavioral pattern depends on the specific problem you are facing. The table below summarizes each pattern along the dimensions most relevant to that decision.

Pattern Coupling Direction Key Mechanism Undo/Redo Runtime Swap Typical Use Case
Iterator Client → Iterator Sequential access No Yes Traversing any collection uniformly
Command Invoker → Command → Receiver Encapsulated request Optional; requires saved state or compensation Yes GUI buttons, transactions, job queues
Strategy Context → Strategy Interchangeable algorithm No Yes Sorting, compression, payment methods
Observer Subject → Observers Event notification No Yes (subscribe/unsubscribe) Event systems, MVC, reactive UI
Chain of Responsibility Sender → Chain Request forwarding No Yes Middleware, logging pipelines, auth checks
Template Method Base template → overridden steps Inheritance skeleton No Steps are subclass-defined, not independently injected Frameworks, data processing pipelines
State Context → State Delegated state behavior Not automatic; requires transition history Yes (transitions) Finite state machines, UI workflows
Mediator Colleagues → Mediator Centralized routing No Possible if the design exposes reconfiguration Chat rooms, air traffic control, dialogs
Memento Caretaker → Originator; stores opaque Mementos Immutable state snapshots Supports restoration; redo needs additional history logic Not its purpose; checkpoints can be selected at runtime Editor checkpoints, local rollback
Visitor Element → Visitor interface; visit methods name concrete types Double dispatch through accept/visit No; requires a separate history mechanism Yes (choose a visitor for each operation) Reports, exporters, syntax-tree analyses

Common Pitfalls

Even experienced developers sometimes misapply Behavioral patterns. Here are the most frequent mistakes and how to avoid them:

Pattern Pitfall Better Approach
Command Treating undo as the opposite action, or assuming all requests are reversible Capture the prior state for undo; add redo or compensation only when needed
Observer Keeping strong subscriber references beyond the subscriber lifecycle Unsubscribe explicitly or use a suitable weak-reference policy; long-lived publishers otherwise retain subscribers
Strategy Mixing algorithm configuration with mutable context internals Inject stable configuration through the constructor and pass per-call data explicitly
Chain of Responsibility Leaving end-of-chain behavior undefined Specify whether an unhandled request is returned, logged, ignored, or rejected
State Confusing state transitions with client-selected strategies Let states or the context own transitions; direct state-to-state construction is valid for a small, stable state graph
Mediator The mediator becoming a "God object" that knows too much Split large mediators into domain-specific ones
Template Method Overriding the orchestration instead of the intended extension steps Use final in Java and a documented @final contract with type checking in Python
Memento Retaining mutable aliases or keeping unlimited snapshots Define snapshot isolation and a bounded history policy
Visitor Choosing Visitor for an element hierarchy that changes frequently Use it for stable element types; budget for updates to every visitor when adding a type

12. Putting It All Together

Behavioral Design Patterns are not silver bullets: they are vocabulary for solutions to recurring design problems . Understanding them deeply means you can recognize the problem first and then apply the appropriate pattern, rather than forcing a pattern onto a problem that does not need it.

A few guiding principles to carry forward:

With a solid understanding of Behavioral patterns, combined with Creational and Structural patterns, you will be equipped to design systems that are easier to extend, test, and reason about, systems that communicate intent through their structure rather than through comments.