./articles/ Software Engineering Essentials: Behavioral Design Patterns
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:
-
Iterator
:
declares
has_next()andnext(), with adapters for the language’s native iteration protocol. - Concrete Iterator : implements the interface and tracks the current position in the collection.
-
IterableCollection
:
the interface declaring a
factory method
create_iterator(). - Concrete Collection : stores the elements and returns a Concrete Iterator when asked.
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
- When you want to hide the internal structure of a collection from its clients.
- When you need to support multiple simultaneous traversals of the same collection (each iterator keeps its own position).
- When you want a uniform interface to traverse different types of collections.
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:
-
Command
:
declares
execute(); this reversible example also requiresundo(). - Concrete Command : implements the interface, holds a reference to the receiver, and defines the action.
-
Receiver
:
the object that actually performs the
work (e.g.,
Light). -
Invoker
:
stores and triggers commands without
knowing what they do internally (e.g.,
RemoteControl).
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
- Read the light’s current state, execute the action, then store the successful execution.
- Undo restores that saved state; simply calling the opposite action is insufficient when the light was already on or off.
- 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
- When you need to parameterize objects with operations (e.g., menu items, toolbar buttons).
- When you need to support undo/redo functionality.
- When you want to queue, schedule, or log operations for later execution.
- When grouping operations into a macro command. Reliable rollback needs explicit failure handling and reversible actions; the pattern alone does not provide transaction atomicity.
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
-
Strategy
:
the interface declaring the algorithm
method (e.g.,
calculate(order)). -
Concrete Strategy
:
implements the algorithm
(e.g.,
StandardShipping,ExpressShipping). - Context : holds a reference to a Strategy and delegates the work to it. The Context does not know which concrete strategy it is using.
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
- When you have multiple variants of an algorithm and want to switch between them at runtime.
- When you want to eliminate large conditional blocks that select behavior based on type.
- When different classes differ only in their behavior: extract that behavior into a strategy.
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
-
Subject (Publisher)
:
maintains a list of observers
and provides
subscribe(),unsubscribe(), andnotify()methods. -
Observer (Subscriber)
:
the interface declaring
update(), called by the subject when state changes. -
Concrete Subject
:
stores the state and triggers
notifications (e.g.,
WeatherStation). -
Concrete Observer
:
reacts to updates
(e.g.,
DisplayPanel,Logger).
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
- When a change in one object requires notifying others, and you do not know in advance how many objects need to change.
- When objects should be able to notify other objects without making assumptions about who those objects are.
- It is the foundation of event systems, GUI frameworks, and reactive programming paradigms.
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
-
Handler
:
the interface declaring
set_next(handler)andhandle(request). - Base Handler : an optional abstract class that stores the reference to the next handler and implements the default forwarding logic.
- Concrete Handlers : process requests they can handle; forward the rest to the next handler in the chain.
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
- When more than one object may handle a request, and the handler is not known a priori.
- When you want to issue a request to one of several handlers without specifying the receiver explicitly.
- It is widely used in middleware pipelines, logging frameworks, and HTTP request processing (e.g., Django middleware, Express.js).
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
- Abstract Class : defines the template method (the algorithm skeleton) and declares abstract primitive operations that subclasses must implement. It may also provide hook methods with default (often empty) implementations.
- Concrete Class : implements the primitive operations to carry out subclass-specific steps of the algorithm.
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
- When several classes share the same algorithm structure but differ in specific steps.
- When you want to avoid code duplication by pulling the common skeleton up into a base class.
-
When you want to control which parts of an algorithm subclasses can
override (using
finalfor the template method in Java).
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
-
Context
:
maintains an instance of a Concrete State
and delegates all state-dependent behavior to it
(e.g.,
VendingMachine). -
State
:
the interface declaring the actions
(e.g.,
insert_coin(),select_item(),dispense()). -
Concrete States
:
implement behavior for each state
and trigger transitions by calling
context.set_state().
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
- When an object's behavior depends on its state, and it must change behavior at runtime depending on that state.
- When you have large conditional blocks that select behavior based on the object's state: extract each branch into its own class.
- Ideal for implementing finite state machines, game characters with multiple modes, or UI workflows with discrete steps.
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
-
Mediator
:
the interface declaring
notify(sender, event)or a similar routing method. -
Concrete Mediator
:
implements the coordination
logic and keeps references to all colleague objects
(e.g.,
ChatRoom). -
Colleague
:
each object that communicates through
the mediator (e.g.,
User). Colleagues hold a reference to the mediator, not to each other.
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
- When a set of objects communicate in well-defined but complex ways, resulting in interdependencies that are difficult to understand.
- When reusing an object is difficult because it refers to and communicates with many other objects.
- Common in UI frameworks (e.g., a dialog box mediating its controls), air traffic control systems, and message brokers.
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
- Originator:
Editorcreates snapshots and restores its own state from them. - Memento: an immutable snapshot containing the text, cursor, and an owner identity used to reject snapshots from another editor.
- Caretaker:
Historystores mementos through a narrow interface and decides when a saved checkpoint should be restored.
From Checkpoint to Undo
- Before an edit,
backup()asks the editor to create a memento and pushes it onto a stack. - The editor changes independently; the saved checkpoint remains unchanged.
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
- When an editor, form, or simulation needs user-controlled checkpoints or rollback of local state.
- When exposing an object's internals to a history manager would undermine encapsulation.
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
- Element:
Shapedeclaresaccept(visitor). - Concrete Elements:
CircleandRectangleforward themselves to the matching visitor method. - Visitor:
ShapeVisitordeclares one visit method for each concrete element type. - Concrete Visitors:
AreaVisitorcomputes a number;DescriptionVisitorproduces text. - Object Structure: the client's list holds different shapes and controls traversal. Visitor does not itself require a tree or define an iteration order.
Two Dispatch Steps, One Operation
- The client calls
shape.accept(visitor); the concrete shape determines whichaccept()implementation runs. - That method calls the shape-specific visit method on the visitor, passing the shape itself.
- 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
- When several unrelated operations must work across the same stable set of element types.
- When reports, exporters, or syntax-tree analyses should evolve separately from the underlying domain objects.
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:
- Favor composition over inheritance. Strategy, Command, Observer, and Mediator all achieve flexibility through object composition. Template Method is the main exception, relying on inheritance, which is why it is less flexible at runtime.
- Program to interfaces, not implementations. Use narrow contracts for collaborators. For example, the Memento caretaker handles opaque snapshots, while Visitor elements depend on a visitor interface. These boundaries let implementations change without spreading unrelated details through the client.
- Start simple. Not every conditional needs a Strategy, and not every object graph needs a Mediator. Apply a pattern when the problem it solves (large conditionals, tight coupling, missing undo) actually exists in your codebase.
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.