Design Patterns
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();
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
Enjoying these tutorials?