Behavioral Design Patterns in Java
Behavioral Design Patterns in Java

Behavioral Design Patterns in Java

Behavioral patterns focus on how objects interact and communicate with each other. They help make your code more flexible, reusable, and easier to maintain.

In this blog post, we’ll explore the most common behavioral design patterns in Java, what they do, and when to use them.

1. Chain of Responsibility Pattern

What It Does: This pattern lets you pass a request along a chain of handlers. Each handler decides whether to process the request or pass it to the next handler in the chain.

When to Use It:

  • When you want to decouple the sender of a request from its receivers.
  • When you want multiple objects to handle a request, but you don’t know which one will handle it at runtime.

Example:
Think of a customer support system. A request (like a complaint) goes through multiple levels (e.g., frontline support, manager, senior manager) until someone handles it.

abstract class Handler {
    private Handler next;

    public void setNext(Handler next) {
        this.next = next;
    }

    public void handleRequest(String request) {
        if (canHandle(request)) {
            processRequest(request);
        } else if (next != null) {
            next.handleRequest(request); // Pass to the next handler
        }
    }

    abstract boolean canHandle(String request);
    abstract void processRequest(String request);
}

class FrontlineSupport extends Handler {
    @Override
    boolean canHandle(String request) {
        return request.equals("basic");
    }

    @Override
    void processRequest(String request) {
        System.out.println("Frontline support handled the request.");
    }
}

class Manager extends Handler {
    @Override
    boolean canHandle(String request) {
        return request.equals("complex");
    }

    @Override
    void processRequest(String request) {
        System.out.println("Manager handled the request.");
    }
}

public class Client {
    public static void main(String[] args) {
        Handler frontline = new FrontlineSupport();
        Handler manager = new Manager();

        frontline.setNext(manager);

        frontline.handleRequest("basic");  // Handled by frontline
        frontline.handleRequest("complex"); // Handled by manager
    }
}

2. Command Pattern

What It Does: This pattern turns a request into an object, allowing you to parameterize clients with different requests, queue requests, or log them.

When to Use It:

  • When you want to decouple the object that invokes an operation from the one that performs it.
  • When you want to support undo/redo functionality.

Example:
Think of a remote control for a TV. Each button (like “Power” or “Volume Up”) is a command object that performs an action.

interface Command {
    void execute();
}

class Light {
    void on() {
        System.out.println("Light is on.");
    }
}

class LightOnCommand implements Command {
    private Light light;

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

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

class RemoteControl {
    private Command command;

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

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

public class Client {
    public static void main(String[] args) {
        Light light = new Light();
        Command lightOn = new LightOnCommand(light);

        RemoteControl remote = new RemoteControl();
        remote.setCommand(lightOn);
        remote.pressButton(); // Turns the light on
    }
}

3. Iterator Pattern

What It Does: This pattern provides a way to access elements of a collection without exposing its underlying structure.

When to Use It:

  • When you want to provide a standard way to traverse a collection.
  • When you want to hide the implementation details of a collection.

Example:
Think of a playlist. You can use an iterator to go through the songs without knowing how the playlist is stored.

interface Iterator {
    boolean hasNext();
    Object next();
}

class Playlist {
    private String[] songs = {"Song 1", "Song 2", "Song 3"};
    private int index = 0;

    public Iterator getIterator() {
        return new PlaylistIterator();
    }

    private class PlaylistIterator implements Iterator {
        @Override
        public boolean hasNext() {
            return index < songs.length;
        }

        @Override
        public Object next() {
            return songs[index++];
        }
    }
}

public class Client {
    public static void main(String[] args) {
        Playlist playlist = new Playlist();
        Iterator iterator = playlist.getIterator();

        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

4. Mediator Pattern

What It Does: This pattern defines an object that encapsulates how a set of objects interact. It promotes loose coupling by keeping objects from referring to each other directly.

When to Use It:

  • When you want to reduce dependencies between objects.
  • When you want to centralize complex communication logic.

Example:
Think of an air traffic control system. The mediator (air traffic controller) coordinates communication between planes.

class Mediator {
    void notify(Object sender, String event) {
        if (event.equals("land")) {
            System.out.println("Mediator: Allowing plane to land.");
        }
    }
}

class Plane {
    private Mediator mediator;

    public Plane(Mediator mediator) {
        this.mediator = mediator;
    }

    public void land() {
        mediator.notify(this, "land");
    }
}

public class Client {
    public static void main(String[] args) {
        Mediator mediator = new Mediator();
        Plane plane = new Plane(mediator);

        plane.land(); // Mediator handles the landing
    }
}

5. Memento Pattern

What It Does: This pattern allows you to save and restore an object’s state without exposing its internal structure.

When to Use It:

  • When you want to implement undo/redo functionality.
  • When you want to save and restore an object’s state.

Example:
Think of a text editor. You can save the current state of the document and restore it later.

class Memento {
    private String state;

    public Memento(String state) {
        this.state = state;
    }

    public String getState() {
        return state;
    }
}

class Editor {
    private String content;

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

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

    public void restore(Memento memento) {
        content = memento.getState();
    }

    public void printContent() {
        System.out.println("Content: " + content);
    }
}

public class Client {
    public static void main(String[] args) {
        Editor editor = new Editor();
        editor.setContent("Hello, World!");

        Memento savedState = editor.save(); // Save state
        editor.setContent("Goodbye, World!");

        editor.restore(savedState); // Restore state
        editor.printContent(); // Prints "Hello, World!"
    }
}

6. Observer Pattern

What It Does: This pattern defines a one-to-many dependency between objects. When one object changes state, all its dependents are notified.

When to Use It:

  • When you want to notify multiple objects about changes in another object.
  • When you want to decouple the sender and receiver of notifications.

Example:
Think of a news publisher and subscribers. When the publisher releases a new article, all subscribers are notified.

interface Observer {
    void update(String message);
}

class NewsPublisher {
    private List<Observer> observers = new ArrayList<>();

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

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

class Subscriber implements Observer {
    @Override
    public void update(String message) {
        System.out.println("Received news: " + message);
    }
}

public class Client {
    public static void main(String[] args) {
        NewsPublisher publisher = new NewsPublisher();
        Subscriber subscriber = new Subscriber();

        publisher.addObserver(subscriber);
        publisher.notifyObservers("Breaking news!"); // Subscriber gets notified
    }
}

7. State Pattern

What It Does: This pattern allows an object to change its behavior when its internal state changes.

When to Use It:

  • When an object’s behavior depends on its state, and it must change its behavior at runtime.
  • When you have many conditional statements that depend on the object’s state.

Example:
Think of a vending machine. Its behavior changes depending on whether it has money, is out of stock, or is dispensing an item.

interface State {
    void handle();
}

class VendingMachine {
    private State state;

    public void setState(State state) {
        this.state = state;
    }

    public void request() {
        state.handle();
    }
}

class HasMoneyState implements State {
    @Override
    public void handle() {
        System.out.println("Dispensing item...");
    }
}

public class Client {
    public static void main(String[] args) {
        VendingMachine machine = new VendingMachine();
        machine.setState(new HasMoneyState());

        machine.request(); // Dispenses item
    }
}

8. Strategy Pattern

What It Does: This pattern lets you define a family of algorithms, encapsulate each one, and make them interchangeable.

When to Use It:

  • When you want to switch between different algorithms or behaviors at runtime.
  • When you want to avoid hardcoding algorithms into your code.

Example:
Think of a navigation app. You can switch between different routing strategies (e.g., fastest route, shortest route).

interface RouteStrategy {
    void buildRoute();
}

class FastestRoute implements RouteStrategy {
    @Override
    public void buildRoute() {
        System.out.println("Building fastest route...");
    }
}

class ShortestRoute implements RouteStrategy {
    @Override
    public void buildRoute() {
        System.out.println("Building shortest route...");
    }
}

class Navigator {
    private RouteStrategy strategy;

    public void setStrategy(RouteStrategy strategy) {
        this.strategy = strategy;
    }

    public void buildRoute() {
        strategy.buildRoute();
    }
}

public class Client {
    public static void main(String[] args) {
        Navigator navigator = new Navigator();
        navigator.setStrategy(new FastestRoute());
        navigator.buildRoute(); // Builds fastest route
    }
}

9. Template Method Pattern

What It Does: This pattern defines the skeleton of an algorithm in a method, letting subclasses override specific steps without changing the algorithm’s structure.

When to Use It:

  • When you want to define a common algorithm but allow some steps to vary.
  • When you want to avoid code duplication in similar algorithms.

Example:
Think of a recipe. The steps (e.g., boil water, add ingredients) are the same, but the ingredients can vary.

abstract class Recipe {
    public void cook() {
        boilWater();
        addIngredients();
        serve();
    }

    abstract void addIngredients();

    void boilWater() {
        System.out.println("Boiling water...");
    }

    void serve() {
        System.out.println("Serving dish...");
    }
}

class PastaRecipe extends Recipe {
    @Override
    void addIngredients() {
        System.out.println("Adding pasta and sauce...");
    }
}

public class Client {
    public static void main(String[] args) {
        Recipe recipe = new PastaRecipe();
        recipe.cook(); // Follows the recipe steps
    }
}

10. Visitor Pattern

What It Does: This pattern lets you add new operations to a set of objects without changing their classes.

When to Use It:

  • When you want to perform operations on a group of objects with different types.
  • When you want to separate algorithms from the objects they operate on.

Example:
Think of a shopping cart. You can apply different operations (e.g., calculate tax, apply discounts) to each item.

interface Visitor {
    void visit(Book book);
    void visit(Fruit fruit);
}

class ShoppingCartVisitor implements Visitor {
    @Override
    public void visit(Book book) {
        System.out.println("Calculating tax for book...");
    }

    @Override
    public void visit(Fruit fruit) {
        System.out.println("Calculating tax for fruit...");
    }
}

interface Item {
    void accept(Visitor visitor);
}

class Book implements Item {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

class Fruit implements Item {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

public class Client {
    public static void main(String[] args) {
        Item[] items = {new Book(), new Fruit()};
        Visitor visitor = new ShoppingCartVisitor();

        for (Item item : items) {
            item.accept(visitor); // Applies visitor operation
        }
    }
}

Conclusion

Behavioral design patterns help you manage how objects interact and communicate. Whether you need to decouple objects (Observer), encapsulate algorithms (Strategy), or add new operations (Visitor), these patterns provide flexible and reusable solutions.

Try using these patterns in your projects to make your code more modular and easier to maintain!

Comments

No comments yet. Why don’t you start the discussion?

    Leave a Reply