Design Patterns in Java: A Comprehensive Guide
Design patterns represent the best practices used by experienced software developers. They are solutions to general problems that software developers have encountered during software development. These patterns are not finished designs that can be transformed directly into code; they are templates that describe how to solve a problem that can be used in many different situations.
This document explores various design patterns with a focus on implementation in Java. We'll dive deep into each pattern's structure, use cases, advantages, and limitations, accompanied by practical Java examples. By understanding these patterns, developers can create more maintainable, flexible, and robust applications.
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This pattern is useful when exactly one object is needed to coordinate actions across the system.
public class Singleton {
// Private static instance variable
private static Singleton instance;
// Private constructor to prevent instantiation
private Singleton() {
// Initialization code
}
// Public static method to get the instance
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
// Other methods
public void showMessage() {
System.out.println("Hello from Singleton!");
}
}
The above implementation is not thread-safe. Here's a thread-safe version:
public class ThreadSafeSingleton {
private static volatile ThreadSafeSingleton instance;
private ThreadSafeSingleton() {
// Initialization code
}
public static ThreadSafeSingleton getInstance() {
if (instance == null) {
synchronized (ThreadSafeSingleton.class) {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
}
Another approach is eager initialization:
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {
// Initialization code
}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
Java's enum provides a thread-safe and serialization-safe singleton implementation:
public enum EnumSingleton {
INSTANCE;
public void showMessage() {
System.out.println("Hello from Enum Singleton!");
}
}
Use the Singleton pattern when:
There must be exactly one instance of a class
The instance must be accessible to clients from a well-known access point
The sole instance should be extensible by subclassing
The Factory Method pattern defines an interface for creating an object but lets subclasses decide which class to instantiate. It allows a class to defer instantiation to subclasses.
// Product interface
interface Product {
void operation();
}
// Concrete products
class ConcreteProductA implements Product {
@Override
public void operation() {
System.out.println("Operation from ConcreteProductA");
}
}
class ConcreteProductB implements Product {
@Override
public void operation() {
System.out.println("Operation from ConcreteProductB");
}
}
// Creator abstract class
abstract class Creator {
public abstract Product createProduct();
public void someOperation() {
Product product = createProduct();
product.operation();
}
}
// Concrete creators
class ConcreteCreatorA extends Creator {
@Override
public Product createProduct() {
return new ConcreteProductA();
}
}
class ConcreteCreatorB extends Creator {
@Override
public Product createProduct() {
return new ConcreteProductB();
}
}
Use the Factory Method pattern when:
A class cannot anticipate the type of objects it must create
A class wants its subclasses to specify the objects it creates
Classes delegate responsibility to one of several helper subclasses, and you want to localize the knowledge of which helper subclass is the delegate
The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.
// Abstract products
interface Button {
void paint();
}
interface Checkbox {
void check();
}
// Concrete products for Windows
class WindowsButton implements Button {
@Override
public void paint() {
System.out.println("Rendering a Windows button");
}
}
class WindowsCheckbox implements Checkbox {
@Override
public void check() {
System.out.println("Checking a Windows checkbox");
}
}
// Concrete products for macOS
class MacOSButton implements Button {
@Override
public void paint() {
System.out.println("Rendering a macOS button");
}
}
class MacOSCheckbox implements Checkbox {
@Override
public void check() {
System.out.println("Checking a macOS checkbox");
}
}
// Abstract factory
interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}
// Concrete factories
class WindowsFactory implements GUIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
@Override
public Checkbox createCheckbox() {
return new WindowsCheckbox();
}
}
class MacOSFactory implements GUIFactory {
@Override
public Button createButton() {
return new MacOSButton();
}
@Override
public Checkbox createCheckbox() {
return new MacOSCheckbox();
}
}
// Client code
class Application {
private Button button;
private Checkbox checkbox;
public Application(GUIFactory factory) {
button = factory.createButton();
checkbox = factory.createCheckbox();
}
public void paint() {
button.paint();
checkbox.check();
}
}
Use the Abstract Factory pattern when:
A system should be independent of how its products are created, composed, and represented
A system should be configured with one of multiple families of products
A family of related product objects is designed to be used together, and you need to enforce this constraint
You want to provide a class library of products, and you want to reveal just their interfaces, not their implementations
The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
// Product
class House {
private String foundation;
private String structure;
private String roof;
private String interior;
public void setFoundation(String foundation) {
this.foundation = foundation;
}
public void setStructure(String structure) {
this.structure = structure;
}
public void setRoof(String roof) {
this.roof = roof;
}
public void setInterior(String interior) {
this.interior = interior;
}
@Override
public String toString() {
return "House with " + foundation + ", " + structure + ", " + roof + ", and " + interior;
}
}
// Builder interface
interface HouseBuilder {
void buildFoundation();
void buildStructure();
void buildRoof();
void buildInterior();
House getResult();
}
// Concrete builder
class ConcreteHouseBuilder implements HouseBuilder {
private House house;
public ConcreteHouseBuilder() {
this.house = new House();
}
@Override
public void buildFoundation() {
house.setFoundation("concrete foundation");
}
@Override
public void buildStructure() {
house.setStructure("brick structure");
}
@Override
public void buildRoof() {
house.setRoof("wooden roof");
}
@Override
public void buildInterior() {
house.setInterior("modern interior");
}
@Override
public House getResult() {
return house;
}
}
// Director
class Director {
private HouseBuilder builder;
public Director(HouseBuilder builder) {
this.builder = builder;
}
public void constructHouse() {
builder.buildFoundation();
builder.buildStructure();
builder.buildRoof();
builder.buildInterior();
}
}
// Client code
public class BuilderDemo {
public static void main(String[] args) {
HouseBuilder builder = new ConcreteHouseBuilder();
Director director = new Director(builder);
director.constructHouse();
House house = builder.getResult();
System.out.println(house);
}
}
Modern Builder Pattern with Method Chaining
class Person {
private final String firstName;
private final String lastName;
private final int age;
private final String address;
private final String phone;
private Person(Builder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.address = builder.address;
this.phone = builder.phone;
}
public static class Builder {
private final String firstName;
private final String lastName;
private int age;
private String address;
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 address(String address) {
this.address = address;
return this;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
public Person build() {
return new Person(this);
}
}
@Override
public String toString() {
return "Person: " + firstName + " " + lastName + ", " + age + " years old, " +
"address: " + address + ", phone: " + phone;
}
}
// Usage
Person person = new Person.Builder("John", "Doe")
.age(30)
.address("123 Main St")
.phone("555-1234")
.build();
Use the Builder pattern when:
The algorithm for creating a complex object should be independent of the parts that make up the object and how they're assembled
The construction process must allow different representations for the object that's constructed
You need to construct objects that contain a lot of parameters, some of which might be optional
The Prototype pattern creates new objects by copying an existing object, known as the prototype. This is useful when the cost of creating a new object is more expensive than copying an existing one.
// Prototype interface
interface Prototype extends Cloneable {
Prototype clone();
}
// Concrete prototype
class ConcretePrototype implements Prototype {
private String field;
public ConcretePrototype(String field) {
this.field = field;
}
public void setField(String field) {
this.field = field;
}
public String getField() {
return field;
}
@Override
public Prototype clone() {
try {
return (Prototype) super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
}
// Client code
public class PrototypeDemo {
public static void main(String[] args) {
ConcretePrototype original = new ConcretePrototype("Original Value");
ConcretePrototype clone = (ConcretePrototype) original.clone();
System.out.println("Original: " + original.getField());
System.out.println("Clone: " + clone.getField());
clone.setField("Modified Value");
System.out.println("Original after clone modification: " + original.getField());
System.out.println("Clone after modification: " + clone.getField());
}
}
For objects with references to other objects, deep cloning is necessary:
class Address implements Cloneable {
private String street;
private String city;
public Address(String street, String city) {
this.street = street;
this.city = city;
}
public void setStreet(String street) {
this.street = street;
}
public String getStreet() {
return street;
}
public void setCity(String city) {
this.city = city;
}
public String getCity() {
return city;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return street + ", " + city;
}
}
class Person implements Cloneable {
private String name;
private Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAddress(Address address) {
this.address = address;
}
public Address getAddress() {
return address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = (Address) address.clone();
return cloned;
}
@Override
public String toString() {
return name + " lives at " + address;
}
}
Use the Prototype pattern when:
The classes to instantiate are specified at run-time
You need to avoid building a class hierarchy of factories that parallels the class hierarchy of products
Instances of a class can have one of only a few different combinations of state
The Adapter pattern allows classes with incompatible interfaces to work together by wrapping an instance of one class with a new adapter class that implements the interface another class expects.
// Target interface
interface Target {
void request();
}
// Adaptee (the class that needs adapting)
class Adaptee {
public void specificRequest() {
System.out.println("Specific request from Adaptee");
}
}
// Adapter (class adapter using inheritance)
class ClassAdapter extends Adaptee implements Target {
@Override
public void request() {
specificRequest();
}
}
// Adapter (object adapter using composition)
class ObjectAdapter implements Target {
private Adaptee adaptee;
public ObjectAdapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
@Override
public void request() {
adaptee.specificRequest();
}
}
// Client code
public class AdapterDemo {
public static void main(String[] args) {
// Using class adapter
Target classAdapter = new ClassAdapter();
classAdapter.request();
// Using object adapter
Adaptee adaptee = new Adaptee();
Target objectAdapter = new ObjectAdapter(adaptee);
objectAdapter.request();
}
}
// Legacy Rectangle class
class LegacyRectangle {
public void draw(int x, int y, int width, int height) {
System.out.println("Drawing rectangle at (" + x + "," + y + ") with width " + width + " and height " + height);
}
}
// Modern Shape interface
interface Shape {
void draw(int x1, int y1, int x2, int y2);
}
// Adapter to make LegacyRectangle work with Shape interface
class RectangleAdapter implements Shape {
private LegacyRectangle legacyRectangle;
public RectangleAdapter(LegacyRectangle legacyRectangle) {
this.legacyRectangle = legacyRectangle;
}
@Override
public void draw(int x1, int y1, int x2, int y2) {
int width = x2 - x1;
int height = y2 - y1;
legacyRectangle.draw(x1, y1, width, height);
}
}
Use the Adapter pattern when:
You want to use an existing class, but its interface doesn't match the one you need
You want to create a reusable class that cooperates with unrelated or unforeseen classes
You need to use several existing subclasses, but it's impractical to adapt their interface by subclassing each one
The Decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
// Component interface
interface Component {
void operation();
}
// Concrete component
class ConcreteComponent implements Component {
@Override
public void operation() {
System.out.println("ConcreteComponent operation");
}
}
// Decorator abstract class
abstract class Decorator implements Component {
protected Component component;
public Decorator(Component component) {
this.component = component;
}
@Override
public void operation() {
component.operation();
}
}
// Concrete decorators
class ConcreteDecoratorA extends Decorator {
public ConcreteDecoratorA(Component component) {
super(component);
}
@Override
public void operation() {
super.operation();
addedBehavior();
}
private void addedBehavior() {
System.out.println("Added behavior from ConcreteDecoratorA");
}
}
class ConcreteDecoratorB extends Decorator {
public ConcreteDecoratorB(Component component) {
super(component);
}
@Override
public void operation() {
super.operation();
addedBehavior();
}
private void addedBehavior() {
System.out.println("Added behavior from ConcreteDecoratorB");
}
}
// Client code
public class DecoratorDemo {
public static void main(String[] args) {
Component component = new ConcreteComponent();
component.operation();
Component decoratedA = new ConcreteDecoratorA(component);
decoratedA.operation();
Component decoratedB = new ConcreteDecoratorB(decoratedA);
decoratedB.operation();
}
}
Real-World Example: Java I/O
Java's I/O classes use the Decorator pattern extensively:
import java.io.*;
public class JavaIOExample {
public static void main(String[] args) throws IOException {
// Creating a chain of decorators
InputStream fileInputStream = new FileInputStream("file.txt");
InputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
DataInputStream dataInputStream = new DataInputStream(bufferedInputStream);
// Reading data
int data = dataInputStream.readInt();
// Closing resources
dataInputStream.close();
}
}
Use the Decorator pattern when:
You need to add responsibilities to individual objects dynamically and transparently, without affecting other objects
You need to add responsibilities to objects that you cannot anticipate
Extension by subclassing is impractical or impossible
You want to add functionality to an object but subclassing would result in an explosion of subclasses
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
import java.util.ArrayList;
import java.util.List;
// Subject interface
interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
// Observer interface
interface Observer {
void update(String message);
}
// Concrete subject
class ConcreteSubject implements Subject {
private List<Observer> observers = new ArrayList<>();
private String state;
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(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();
}
}
// Concrete observers
class ConcreteObserverA implements Observer {
@Override
public void update(String message) {
System.out.println("Observer A received: " + message);
}
}
class ConcreteObserverB implements Observer {
@Override
public void update(String message) {
System.out.println("Observer B received: " + message);
}
}
// Client code
public class ObserverDemo {
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject();
Observer observerA = new ConcreteObserverA();
Observer observerB = new ConcreteObserverB();
subject.registerObserver(observerA);
subject.registerObserver(observerB);
subject.setState("First state change");
subject.removeObserver(observerA);
subject.setState("Second state change");
}
}
Java's Built-in Observer Pattern
Java provides built-in support for the Observer pattern through java.util.Observable class and java.util.Observer interface (deprecated in Java 9):
import java.util.Observable;
import java.util.Observer;
class WeatherData extends Observable {
private float temperature;
public void setMeasurements(float temperature) {
this.temperature = temperature;
setChanged();
notifyObservers(temperature);
}
public float getTemperature() {
return temperature;
}
}
class TemperatureDisplay implements Observer {
@Override
public void update(Observable o, Object arg) {
if (arg instanceof Float) {
float temperature = (Float) arg;
System.out.println("Temperature changed to: " + temperature);
}
}
}
Use the Observer pattern when:
An abstraction has two aspects, one dependent on the other
A change to one object requires changing others, and you don't know how many objects need to be changed
An object should be able to notify other objects without making assumptions about who these objects are
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.
// Strategy interface
interface PaymentStrategy {
void pay(int amount);
}
// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
private String name;
public CreditCardPayment(String cardNumber, String name) {
this.cardNumber = cardNumber;
this.name = name;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid with credit card " + cardNumber);
}
}
class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using PayPal account " + email);
}
}
// Context
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
// Client code
public class StrategyDemo {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment("1234-5678-9012-3456", "John Doe"));
cart.checkout(100);
cart.setPaymentStrategy(new PayPalPayment("john.doe@example.com"));
cart.checkout(200);
}
}
Use the Strategy pattern when:
Many related classes differ only in their behavior
You need different variants of an algorithm
An algorithm uses data that clients shouldn't know about
A class defines many behaviors, and these appear as multiple conditional statements in its operations
The Command pattern encapsulates a request as an object, thereby allowing for parameterization of clients with different requests, queuing of requests, and logging of the requests. It also allows for the support of undoable operations.
// Command interface
interface Command {
void execute();
void undo();
}
// Receiver
class Light {
private boolean isOn = false;
public void turnOn() {
isOn = true;
System.out.println("Light is now ON");
}
public void turnOff() {
isOn = false;
System.out.println("Light is now OFF");
}
public boolean isOn() {
return isOn;
}
}
// Concrete commands
class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
@Override
public void undo() {
light.turnOff();
}
}
class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOff();
}
@Override
public void undo() {
light.turnOn();
}
}
// Invoker
class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
public void pressUndoButton() {
command.undo();
}
}
// Client code
public class CommandDemo {
public static void main(String[] args) {
Light light = new Light();
Command lightOn = new LightOnCommand(light);
Command lightOff = new LightOffCommand(light);
RemoteControl remote = new RemoteControl();
remote.setCommand(lightOn);
remote.pressButton();
remote.setCommand(lightOff);
remote.pressButton();
remote.pressUndoButton();
}
}
Use the Command pattern when:
You want to parameterize objects with operations
You want to queue operations, schedule their execution, or execute them remotely
You want to support undo operations
You want to structure a system around high-level operations built on primitive operations
The Template Method pattern defines the skeleton of an algorithm in a method, deferring some steps to subclasses. It lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.
// Abstract class with template method
abstract class AbstractClass {
// Template method
public final void templateMethod() {
step1();
step2();
step3();
hook();
}
// Steps that must be implemented by subclasses
protected abstract void step1();
protected abstract void step2();
// Step with default implementation
protected void step3() {
System.out.println("AbstractClass: step3");
}
// Hook with default empty implementation
protected void hook() {
// Default empty implementation
}
}
// Concrete implementations
class ConcreteClassA extends AbstractClass {
@Override
protected void step1() {
System.out.println("ConcreteClassA: step1");
}
@Override
protected void step2() {
System.out.println("ConcreteClassA: step2");
}
@Override
protected void hook() {
System.out.println("ConcreteClassA: hook");
}
}
class ConcreteClassB extends AbstractClass {
@Override
protected void step1() {
System.out.println("ConcreteClassB: step1");
}
@Override
protected void step2() {
System.out.println("ConcreteClassB: step2");
}
@Override
protected void step3() {
System.out.println("ConcreteClassB: step3 (overridden)");
}
}
// Client code
public class TemplateMethodDemo {
public static void main(String[] args) {
AbstractClass instanceA = new ConcreteClassA();
instanceA.templateMethod();
System.out.println();
AbstractClass instanceB = new ConcreteClassB();
instanceB.templateMethod();
}
}
abstract class DataProcessor {
// Template method
public final void process() {
readData();
processData();
writeData();
}
protected abstract void readData();
protected abstract void processData();
protected void writeData() {
System.out.println("Writing processed data to console");
}
}
class CSVProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("Reading data from CSV file");
}
@Override
protected void processData() {
System.out.println("Processing CSV data");
}
}
class DatabaseProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("Reading data from database");
}
@Override
protected void processData() {
System.out.println("Processing database records");
}
@Override
protected void writeData() {
System.out.println("Writing processed data to database");
}
}
Use the Template Method pattern when:
You want to let clients extend only particular steps of an algorithm, but not the whole algorithm or its structure
You have several classes that contain almost identical algorithms with some minor differences
You want to control at which points subclassing is allowed
Design patterns are essential tools in a developer's toolkit. They provide tested, proven development paradigms that can speed up the development process by providing robust, well-tested solutions to common problems. By understanding and applying these patterns appropriately, developers can create more maintainable, flexible, and robust code.
The patterns covered in this document represent just a subset of the many design patterns available. As you continue your journey in software development, you'll encounter more patterns and variations of the ones discussed here. The key is to understand not just how to implement these patterns, but when and why to use them.
Remember that design patterns are not silver bullets. They should be applied judiciously, based on the specific requirements and constraints of your project. Overuse or misuse of patterns can lead to unnecessary complexity and reduced maintainability.
Design Pattern Interview Questions
What is the Singleton pattern, and when would you use it?
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. Use it when exactly one object is needed to coordinate actions across the system, such as a configuration manager, connection pool, or file manager.
How would you implement a thread-safe Singleton in Java?
There