Web Analytics

Design Patterns

Advanced ~25 min read

Design patterns are reusable solutions to common problems in software design. They represent best practices evolved over time by experienced developers. Understanding these patterns is essential for writing maintainable, flexible code.

Pattern Categories

Category Purpose Examples
Creational Object creation mechanisms Singleton, Factory, Builder
Structural Object composition Adapter, Decorator, Facade
Behavioral Object communication Observer, Strategy, Command

Singleton Pattern

Ensures a class has only one instance and provides global access to it.

Thread-Safe Singleton (Bill Pugh)

public class Singleton {
    // Private constructor prevents instantiation
    private Singleton() {}

    // Inner class holds the singleton instance
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;  // Lazy, thread-safe
    }
}

Enum Singleton (Recommended)

public enum DatabaseConnection {
    INSTANCE;

    private Connection connection;

    DatabaseConnection() {
        // Initialize connection
        connection = createConnection();
    }

    public Connection getConnection() {
        return connection;
    }
}

// Usage
DatabaseConnection.INSTANCE.getConnection();
Why Enum? Enum singletons are thread-safe, prevent reflection attacks, and handle serialization automatically.

Factory Pattern

Creates objects without exposing creation logic to the client.

// Product interface
interface Shape {
    void draw();
}

// Concrete products
class Circle implements Shape {
    public void draw() { System.out.println("Drawing Circle"); }
}

class Rectangle implements Shape {
    public void draw() { System.out.println("Drawing Rectangle"); }
}

// Factory
class ShapeFactory {
    public static Shape createShape(String type) {
        return switch (type.toLowerCase()) {
            case "circle" -> new Circle();
            case "rectangle" -> new Rectangle();
            default -> throw new IllegalArgumentException("Unknown shape: " + type);
        };
    }
}

// Usage
Shape shape = ShapeFactory.createShape("circle");
shape.draw();

Builder Pattern

Constructs complex objects step by step. Perfect for objects with many optional parameters.

public class User {
    private final String firstName;    // required
    private final String lastName;     // required
    private final int age;             // optional
    private final String email;        // optional
    private final String phone;        // optional

    private User(Builder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
        this.email = builder.email;
        this.phone = builder.phone;
    }

    public static class Builder {
        // Required parameters
        private final String firstName;
        private final String lastName;

        // Optional parameters - initialized to defaults
        private int age = 0;
        private String email = "";
        private String phone = "";

        public Builder(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        public Builder age(int age) {
            this.age = age;
            return this;
        }

        public Builder email(String email) {
            this.email = email;
            return this;
        }

        public Builder phone(String phone) {
            this.phone = phone;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }
}

// Usage - fluent, readable API
User user = new User.Builder("John", "Doe")
    .age(30)
    .email("[email protected]")
    .build();

Strategy Pattern

Defines a family of algorithms and makes them interchangeable at runtime.

// Strategy interface
interface PaymentStrategy {
    void pay(double amount);
}

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

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

    @Override
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " with credit card");
    }
}

class PayPalPayment implements PaymentStrategy {
    private String email;

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

    @Override
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " via PayPal");
    }
}

// Context
class ShoppingCart {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }

    public void checkout(double amount) {
        paymentStrategy.pay(amount);
    }
}

// Usage - switch strategies at runtime
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment("1234-5678"));
cart.checkout(100.00);

cart.setPaymentStrategy(new PayPalPayment("[email protected]"));
cart.checkout(50.00);

Observer Pattern

Defines a one-to-many dependency between objects. When one object changes state, all dependents are notified automatically.

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

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

// Subject (Observable)
class NewsAgency {
    private List<Observer> observers = new ArrayList<>();
    private String news;

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

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

    public void setNews(String news) {
        this.news = news;
        notifyObservers();
    }

    private void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(news);
        }
    }
}

// Concrete observer
class NewsChannel implements Observer {
    private String name;

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

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

// Usage
NewsAgency agency = new NewsAgency();
agency.addObserver(new NewsChannel("CNN"));
agency.addObserver(new NewsChannel("BBC"));

agency.setNews("Breaking: Java 21 Released!");
// Output:
// CNN received: Breaking: Java 21 Released!
// BBC received: Breaking: Java 21 Released!

Decorator Pattern

Adds behavior to objects dynamically without affecting other objects.

// Component interface
interface Coffee {
    String getDescription();
    double getCost();
}

// Concrete component
class SimpleCoffee implements Coffee {
    public String getDescription() { return "Coffee"; }
    public double getCost() { return 2.00; }
}

// Base decorator
abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;

    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }
}

// Concrete decorators
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) { super(coffee); }

    public String getDescription() {
        return coffee.getDescription() + ", Milk";
    }

    public double getCost() {
        return coffee.getCost() + 0.50;
    }
}

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) { super(coffee); }

    public String getDescription() {
        return coffee.getDescription() + ", Sugar";
    }

    public double getCost() {
        return coffee.getCost() + 0.25;
    }
}

// Usage - stack decorators
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);

System.out.println(coffee.getDescription()); // Coffee, Milk, Sugar
System.out.println("$" + coffee.getCost());  // $2.75

When to Use Each Pattern

Pattern Use When
Singleton Single shared resource (database connection, logger, config)
Factory Object creation depends on runtime conditions
Builder Complex objects with many optional parameters
Strategy Multiple algorithms that can be swapped at runtime
Observer One-to-many event notification
Decorator Add responsibilities to objects dynamically

Summary

  • Singleton: Use enum or Bill Pugh idiom for thread safety
  • Factory: Centralizes object creation logic
  • Builder: Creates readable APIs for complex objects
  • Strategy: Enables runtime algorithm selection
  • Observer: Decouples event sources from handlers
  • Decorator: Adds features without subclassing