Behavioral Design Patterns

Saurav Kumar
8 min readNov 22, 2023

--

Behavioral design patterns deal with the communication and collaboration between objects, focusing on how they interact and distribute responsibilities. There are 11 commonly recognized behavioral design patterns. Here, I’ll provide a detailed explanation and a full example for each one:

1. Chain of Responsibility Pattern:

  • Purpose: Passes a request along a chain of handlers, each handling the request or passing it to the next handler in the chain.
  • Example: Implementing a help desk system where support requests are first handled by Level 1 support, and if they can’t resolve it, they pass it to Level 2, and so on.
abstract class SupportHandler {
private SupportHandler nextHandler;

public void setNextHandler(SupportHandler nextHandler) {
this.nextHandler = nextHandler;
}

public abstract void handleRequest(String request);
}

class Level1SupportHandler extends SupportHandler {
@Override
public void handleRequest(String request) {
if (request.equals("Level 1")) {
System.out.println("Handled by Level 1 support");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}

class Level2SupportHandler extends SupportHandler {
@Override
public void handleRequest(String request) {
if (request.equals("Level 2")) {
System.out.println("Handled by Level 2 support");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}

2. Command Pattern:

  • Purpose: Encapsulates a request as an object, thereby allowing for parameterization of clients with queuing, requests, and operations.
  • Example: Creating a remote control for home automation, where each button press corresponds to a specific command (e.g., turn on lights, adjust thermostat).
// Command interface
interface Command {
void execute();
}

// Concrete Command
class LightOnCommand implements Command {
private Light light;

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

@Override
public void execute() {
light.turnOn();
}
}

// Receiver
class Light {
public void turnOn() {
System.out.println("Light is on");
}
}

// Invoker
class RemoteControl {
private Command command;

public void setCommand(Command command) {
this.command = command;
}

public void pressButton() {
command.execute();
}
}

3. Interpreter Pattern:

  • Purpose: Provides a way to evaluate language grammar or expressions.
  • Example: Implementing a simple arithmetic expression evaluator.
interface Expression {
int interpret();
}

class NumberExpression implements Expression {
private int number;

public NumberExpression(int number) {
this.number = number;
}

@Override
public int interpret() {
return number;
}
}

class AddExpression implements Expression {
private Expression left;
private Expression right;

public AddExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}

@Override
public int interpret() {
return left.interpret() + right.interpret();
}
}

4. Iterator Pattern:

  • Purpose: Provides a way to access elements of an aggregate object sequentially without exposing its underlying representation.
  • Example: Implementing an iterator for a collection (e.g., ArrayList) that allows you to iterate through its elements without exposing its internal structure.
// Iterator interface
interface Iterator<T> {
boolean hasNext();
T next();
}

// Aggregate interface
interface IterableCollection<T> {
Iterator<T> createIterator();
}

// Concrete Iterator
class ArrayListIterator<T> implements Iterator<T> {
private List<T> list;
private int position = 0;

public ArrayListIterator(List<T> list) {
this.list = list;
}

@Override
public boolean hasNext() {
return position < list.size();
}

@Override
public T next() {
if (hasNext()) {
return list.get(position++);
}
throw new NoSuchElementException();
}
}

// Concrete Aggregate
class ArrayListCollection<T> implements IterableCollection<T> {
private List<T> list = new ArrayList<>();

public void add(T item) {
list.add(item);
}

@Override
public Iterator<T> createIterator() {
return new ArrayListIterator<>(list);
}
}

5. Mediator Pattern:

  • Purpose: Defines an object that encapsulates how a set of objects interact, promoting loose coupling between them.
  • Example: Implementing a chat application where users can send messages to each other through a central chat room mediator.
// Mediator interface
interface ChatMediator {
void sendMessage(String message, User user);
}

// Concrete Mediator
class ChatRoom implements ChatMediator {
@Override
public void sendMessage(String message, User user) {
System.out.println(user.getName() + " says: " + message);
}
}

// Colleague
class User {
private String name;
private ChatMediator mediator;

public User(String name, ChatMediator mediator) {
this.name = name;
this.mediator = mediator;
}

public void sendMessage(String message) {
mediator.sendMessage(message, this);
}

public String getName() {
return name;
}
}

6. Memento Pattern:

  • Purpose: Captures and externalizes an object’s internal state so the object can be restored to this state later.
  • Example: Implementing an undo feature in a text editor where you can save and restore the editor’s state.
// Memento
class EditorMemento {
private String content;

public EditorMemento(String content) {
this.content = content;
}

public String getContent() {
return content;
}
}

// Originator
class TextEditor {
private String content;

public void setContent(String content) {
this.content = content;
}

public EditorMemento save() {
return new EditorMemento(content);
}

public void restore(EditorMemento memento) {
content = memento.getContent();
}

public String getContent() {
return content;
}
}

7. Observer Pattern:

  • Purpose: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
  • Example: Implementing a notification system where multiple subscribers are notified when an event occurs.
import java.util.ArrayList;
import java.util.List;

// Subject (Observable)
interface Subject {
void addObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers(String message);
}

// Concrete Subject (Concrete Observable)
class NewsPublisher implements Subject {
private List<Observer> observers = new ArrayList<>();

@Override
public void addObserver(Observer observer) {
observers.add(observer);
}

@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}

@Override
public void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}

// Simulate publishing news
public void publishNews(String news) {
System.out.println("Published news: " + news);
notifyObservers(news);
}
}

// Observer (Subscriber)
interface Observer {
void update(String message);
}

// Concrete Observer (Concrete Subscriber)
class NewsSubscriber implements Observer {
private String name;

public NewsSubscriber(String name) {
this.name = name;
}

@Override
public void update(String message) {
System.out.println(name + " received news: " + message);
}
}

In this example, the NewsPublisher is the subject that notifies its subscribers (observers) when news is published. Subscribers like NewsSubscriber to receive and process the news.

8. State Pattern:

  • Purpose: Allows an object to alter its behavior when its internal state changes. The object will appear to change its class.
  • Example: Implementing a vending machine that dispenses different items based on its current state (e.g., has coins, item selected).
// State interface
interface VendingMachineState {
void insertCoin();
void selectItem();
void dispenseItem();
}

// Concrete States
class NoCoinState implements VendingMachineState {
@Override
public void insertCoin() {
System.out.println("Coin inserted");
}

@Override
public void selectItem() {
System.out.println("Please insert a coin first");
}

@Override
public void dispenseItem() {
System.out.println("Please select an item first");
}
}

class HasCoinState implements VendingMachineState {
@Override
public void insertCoin() {
System.out.println("Coin already inserted");
}

@Override
public void selectItem() {
System.out.println("Item selected");
}

@Override
public void dispenseItem() {
System.out.println("Item dispensed");
}
}

// Context
class VendingMachine {
private VendingMachineState currentState;

public VendingMachine() {
currentState = new NoCoinState();
}

public void insertCoin() {
currentState.insertCoin();
if (currentState instanceof NoCoinState) {
currentState = new HasCoinState();
}
}

public void selectItem() {
currentState.selectItem();
if (currentState instanceof HasCoinState) {
currentState = new NoCoinState();
currentState.dispenseItem();
}
}
}

9. Strategy Pattern:

  • Purpose: Defines a family of algorithms, encapsulates each one and makes them interchangeable. It allows the client to choose the algorithm to be used at runtime.
  • Example: Implementing a payment system where different payment methods (credit card, PayPal) are interchangeable.
// Strategy interface
interface PaymentStrategy {
void pay(int amount);
}

// Concrete Strategies
class CreditCardPayment implements PaymentStrategy {
private String cardNumber;

public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}

@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using credit card " + cardNumber);
}
}

class PayPalPayment implements PaymentStrategy {
private String email;

public PayPalPayment(String email) {
this.email = email;
}

@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using PayPal account " + email);
}
}

// Context
class ShoppingCart {
private PaymentStrategy paymentStrategy;
private int totalAmount;

public ShoppingCart(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}

public void addItem(int price) {
totalAmount += price;
}

public void checkout() {
paymentStrategy.pay(totalAmount);
}
}

In this example, the ShoppingCart allows the client to choose a payment strategy (credit card or PayPal) at runtime.

10. Template Method Pattern:

  • Purpose: Defines the skeleton of an algorithm in the base class but lets subclasses override specific steps of the algorithm without changing its structure.
  • Example: Implementing a document generation process where different document types (e.g., PDF, Word) have the same overall structure but require different rendering steps.
// Abstract Template
abstract class Document {
public void generateDocument() {
addHeader();
addContent();
addFooter();
}

protected abstract void addHeader();
protected abstract void addContent();
protected abstract void addFooter();
}

// Concrete Templates
class PDFDocument extends Document {
@Override
protected void addHeader() {
System.out.println("PDF Header");
}

@Override
protected void addContent() {
System.out.println("PDF Content");
}

@Override
protected void addFooter() {
System.out.println("PDF Footer");
}
}

class WordDocument extends Document {
@Override
protected void addHeader() {
System.out.println("Word Header");
}

@Override
protected void addContent() {
System.out.println("Word Content");
}

@Override
protected void addFooter() {
System.out.println("Word Footer");
}
}

The Document class defines the overall document generation process, but the PDFDocument and WordDocument subclasses implement the specific steps for their respective document types.

11. Visitor Pattern:

  • Purpose: Represents an operation to be performed on the elements of an object structure. It lets you define a new operation without changing the classes of the elements on which it operates.
  • Example: Implementing a visitor that computes the total cost of items in a shopping cart without modifying the item classes.
// Element interface
interface ShoppingCartItem {
void accept(ShoppingCartVisitor visitor);
}

// Concrete Elements
class BookItem implements ShoppingCartItem {
private int price;
private String isbn;

// Constructor, getters, and setters
@Override
public void accept(ShoppingCartVisitor visitor) {
visitor.visit(this);
}
}

class FruitItem implements ShoppingCartItem {
private int pricePerKg;
private int weight;
private String name;

// Constructor, getters, and setters

@Override
public void accept(ShoppingCartVisitor visitor) {
visitor.visit(this);
}
}

// Visitor interface
interface ShoppingCartVisitor {
void visit(BookItem book);
void visit(FruitItem fruit);
}

// Concrete Visitor
class ShoppingCartVisitorImpl implements ShoppingCartVisitor {
@Override
public void visit(BookItem book) {
int discountedPrice = book.getPrice() - 5; // Apply a discount
System.out.println("Book: " + book.getName() + ", Price after discount: $" + discountedPrice);
}

@Override
public void visit(FruitItem fruit) {
int cost = fruit.getPricePerKg() * fruit.getWeight();
System.out.println("Fruit: " + fruit.getName() + ", Cost: $" + cost);
}
}

// Client code
public class ShoppingCartClient {
public static void main(String[] args) {
ShoppingCartItem[] items = {
new BookItem("Design Patterns", 30, "12345"),
new FruitItem("Apple", 2, 5, "Red"),
new FruitItem("Banana", 1, 3, "Yellow")
};

ShoppingCartVisitor visitor = new ShoppingCartVisitorImpl();

int totalCost = calculateTotalCost(items, visitor);
System.out.println("Total cost: $" + totalCost);
}

private static int calculateTotalCost(ShoppingCartItem[] items, ShoppingCartVisitor visitor) {
int totalCost = 0;
for (ShoppingCartItem item : items) {
item.accept(visitor);
}
return totalCost;
}
}

In this example, the ShoppingCartVisitor interface defines the visitor, and the ShoppingCartVisitorImpl class provides the concrete implementation of the visitor. The ShoppingCartItem interface represents the elements in the shopping cart, and the BookItem and FruitItem classes are concrete implementations of these elements.

The calculateTotalCost function iterates through the shopping cart items and uses the visitor to compute the total cost without modifying the item classes themselves. This demonstrates how the Visitor Pattern allows you to add new operations (in this case, calculating costs) without changing the structure of the item classes.

--

--

Saurav Kumar

Experienced Software Engineer adept in Java, Spring Boot, Microservices, Kafka & Azure.