Java Design Patterns: A Comprehensive Guide

Introduction to Design Patterns

Design patterns are reusable solutions to common problems in software design. This guide covers the most important design patterns in Java, their implementation, and use cases.

Creational Patterns

Singleton Pattern


public class Singleton {
    private static volatile Singleton instance;
    private final String data;
    
    private Singleton(String data) {
        this.data = data;
    }
    
    public static Singleton getInstance(String data) {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(data);
                }
            }
        }
        return instance;
    }
    
    public String getData() {
        return data;
    }
}
                

Factory Pattern


interface Product {
    void operation();
}

class ConcreteProductA implements Product {
    @Override
    public void operation() {
        System.out.println("ConcreteProductA operation");
    }
}

class ConcreteProductB implements Product {
    @Override
    public void operation() {
        System.out.println("ConcreteProductB operation");
    }
}

class Factory {
    public Product createProduct(String type) {
        switch (type) {
            case "A":
                return new ConcreteProductA();
            case "B":
                return new ConcreteProductB();
            default:
                throw new IllegalArgumentException("Unknown product type");
        }
    }
}
                

Structural Patterns

Adapter Pattern


interface Target {
    void request();
}

class Adaptee {
    public void specificRequest() {
        System.out.println("Specific request");
    }
}

class Adapter implements Target {
    private final Adaptee adaptee;
    
    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }
    
    @Override
    public void request() {
        adaptee.specificRequest();
    }
}
                

Decorator Pattern


interface Component {
    void operation();
}

class ConcreteComponent implements Component {
    @Override
    public void operation() {
        System.out.println("ConcreteComponent operation");
    }
}

abstract class Decorator implements Component {
    protected Component component;
    
    public Decorator(Component component) {
        this.component = component;
    }
    
    @Override
    public void operation() {
        component.operation();
    }
}

class ConcreteDecorator extends Decorator {
    public ConcreteDecorator(Component component) {
        super(component);
    }
    
    @Override
    public void operation() {
        super.operation();
        System.out.println("Additional behavior");
    }
}
                

Behavioral Patterns

Observer Pattern


interface Observer {
    void update(String message);
}

interface Subject {
    void attach(Observer observer);
    void detach(Observer observer);
    void notifyObservers();
}

class ConcreteSubject implements Subject {
    private final List observers = new ArrayList<>();
    private String state;
    
    @Override
    public void attach(Observer observer) {
        observers.add(observer);
    }
    
    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
    }
    
    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(state);
        }
    }
    
    public void setState(String state) {
        this.state = state;
        notifyObservers();
    }
}
                

Strategy Pattern


interface Strategy {
    void execute();
}

class ConcreteStrategyA implements Strategy {
    @Override
    public void execute() {
        System.out.println("Strategy A");
    }
}

class ConcreteStrategyB implements Strategy {
    @Override
    public void execute() {
        System.out.println("Strategy B");
    }
}

class Context {
    private Strategy strategy;
    
    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }
    
    public void executeStrategy() {
        strategy.execute();
    }
}
                

Real-World Examples

Builder Pattern in Database Connection


public class DatabaseConnection {
    private final String host;
    private final int port;
    private final String username;
    private final String password;
    private final boolean useSSL;
    
    private DatabaseConnection(Builder builder) {
        this.host = builder.host;
        this.port = builder.port;
        this.username = builder.username;
        this.password = builder.password;
        this.useSSL = builder.useSSL;
    }
    
    public static class Builder {
        private String host;
        private int port;
        private String username;
        private String password;
        private boolean useSSL;
        
        public Builder host(String host) {
            this.host = host;
            return this;
        }
        
        public Builder port(int port) {
            this.port = port;
            return this;
        }
        
        public Builder username(String username) {
            this.username = username;
            return this;
        }
        
        public Builder password(String password) {
            this.password = password;
            return this;
        }
        
        public Builder useSSL(boolean useSSL) {
            this.useSSL = useSSL;
            return this;
        }
        
        public DatabaseConnection build() {
            return new DatabaseConnection(this);
        }
    }
}
                

Design Pattern Best Practices

  • Choose patterns based on problem requirements
  • Don't overuse patterns
  • Consider maintainability and readability
  • Use patterns to solve real problems
  • Document pattern usage and rationale
  • Consider performance implications

Common Anti-Patterns

  • Over-engineering with patterns
  • Using patterns without understanding
  • Ignoring simpler solutions
  • Creating unnecessary abstractions
  • Violating pattern principles

Testing Design Patterns


@Test
public void testSingleton() {
    Singleton instance1 = Singleton.getInstance("data");
    Singleton instance2 = Singleton.getInstance("data");
    
    assertSame(instance1, instance2);
    assertEquals("data", instance1.getData());
}

@Test
public void testFactory() {
    Factory factory = new Factory();
    Product productA = factory.createProduct("A");
    Product productB = factory.createProduct("B");
    
    assertTrue(productA instanceof ConcreteProductA);
    assertTrue(productB instanceof ConcreteProductB);
}
                

Conclusion

Design patterns provide proven solutions to common software design problems. Understanding and properly applying these patterns can help create more maintainable, flexible, and robust applications. Remember to use patterns judiciously and always consider the specific requirements of your project.