🚀AdvancedDesign Patterns

Behavioral Design Patterns

Implement Observer, Strategy, Command, and Template Method patterns for flexible object behavior.

Overview

Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects. They characterize complex control flow and describe how objects communicate and collaborate. The four most important behavioral patterns are: - Observer: defines a one-to-many dependency so that when one object changes state, its dependents are notified - Strategy: defines a family of algorithms, encapsulates each one, and makes them interchangeable - Command: encapsulates a request as an object, supporting undo/redo and queuing - Template Method: defines the skeleton of an algorithm in a base class, deferring specific steps to subclasses

Key Points

  • •Observer decouples publishers from subscribers; subjects do not know who is listening
  • •Strategy replaces inheritance-based variation with composition; algorithms are objects
  • •Strategy is essentially dependency injection at the algorithm level
  • •Command wraps a request as an object with execute() and optionally undo()
  • •Command supports queuing, logging, and transactional behavior
  • •Template Method uses inheritance; hook methods are overridden in subclasses
  • •Template Method follows the Hollywood principle: do not call us, we will call you
  • •Prefer Strategy (composition) over Template Method (inheritance) for modern Java
  • •Java functional interfaces enable lightweight Strategy without explicit classes

Examples

Observer pattern for a stock price ticker

import java.util.*;

interface StockObserver {
    void onPriceChange(String symbol, double newPrice);
}

class StockTicker {
    private final List<StockObserver> observers = new ArrayList<>();
    private double price;
    private final String symbol;

    StockTicker(String symbol, double price) {
        this.symbol = symbol;
        this.price = price;
    }

    public void addObserver(StockObserver o) { observers.add(o); }

    public void setPrice(double newPrice) {
        this.price = newPrice;
        observers.forEach(o -> o.onPriceChange(symbol, newPrice));
    }
}

public class Main {
    public static void main(String[] args) {
        StockTicker aapl = new StockTicker("AAPL", 180.0);

        aapl.addObserver((sym, p) -> System.out.println("Email alert: " + sym + " = " + p));
        aapl.addObserver((sym, p) -> System.out.println("Dashboard updated: " + sym + " = " + p));

        aapl.setPrice(185.0);
        aapl.setPrice(192.5);
    }
}

Observers are lambdas here because StockObserver is a functional interface. The ticker does not know what the observers do; they are registered dynamically at runtime.

Strategy pattern for sorting algorithms

import java.util.Arrays;
import java.util.function.Consumer;

public class Sorter {
    private Consumer<int[]> strategy;

    public Sorter(Consumer<int[]> strategy) {
        this.strategy = strategy;
    }

    public void setStrategy(Consumer<int[]> strategy) {
        this.strategy = strategy;
    }

    public void sort(int[] arr) { strategy.accept(arr); }

    public static void main(String[] args) {
        int[] data = {5, 3, 8, 1, 9, 2};

        // Use Arrays.sort as the strategy
        Sorter sorter = new Sorter(Arrays::sort);
        sorter.sort(data);
        System.out.println("Sorted: " + Arrays.toString(data));

        // Switch to reverse-sort strategy
        int[] data2 = {5, 3, 8, 1, 9, 2};
        sorter.setStrategy(arr -> {
            Arrays.sort(arr);
            for (int i = 0, j = arr.length - 1; i < j; i++, j--) {
                int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp;
            }
        });
        sorter.sort(data2);
        System.out.println("Reverse sorted: " + Arrays.toString(data2));
    }
}

Using Consumer<int[]> as the strategy type allows lambda expressions directly without a dedicated interface. This is the modern Java approach to Strategy.

Command pattern with undo for a text editor

import java.util.*;

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

class TextEditor {
    private StringBuilder text = new StringBuilder();
    private final Deque<Command> history = new ArrayDeque<>();

    public void executeCommand(Command cmd) {
        cmd.execute();
        history.push(cmd);
    }

    public void undoLast() {
        if (!history.isEmpty()) history.pop().undo();
    }

    class InsertCommand implements Command {
        private final String insertion;
        InsertCommand(String text) { this.insertion = text; }

        @Override
        public void execute() { text.append(insertion); }
        @Override
        public void undo() { text.delete(text.length() - insertion.length(), text.length()); }
    }

    public String getText() { return text.toString(); }

    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        editor.executeCommand(editor.new InsertCommand("Hello"));
        editor.executeCommand(editor.new InsertCommand(" World"));
        System.out.println(editor.getText()); // Hello World

        editor.undoLast();
        System.out.println(editor.getText()); // Hello
    }
}

Command objects are stored in a history stack. undoLast() pops the most recent command and calls undo(). This is the core of undo/redo systems in text editors and IDEs.

Common Mistakes

  • Making observer callbacks synchronous and slow. If an observer does expensive work, it blocks the publisher. Use an async observer or post events to a queue for processing on a separate thread.
  • Forgetting to remove observers when they are no longer needed. This causes memory leaks because the publisher holds references to the observer objects, preventing garbage collection.
  • Choosing Template Method over Strategy by default. Template Method couples the algorithm skeleton to inheritance. Strategy allows the algorithm to change at runtime and is easier to test in isolation.

Note

Java has built-in Observer infrastructure: PropertyChangeSupport for JavaBeans, Flow.Publisher/Subscriber for reactive streams (Java 9+), and EventListener for GUI components. Before rolling your own, check if an existing mechanism fits.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 2 exercises

Knowledge Check

Question 1 of 4

What problem does the Observer pattern solve?