Coding Patterns in Java
Introduction
Software Design Patterns (or Coding Patterns) are typical solutions to common problems in software design. They are like pre-made blueprints that you can customize to solve a recurring design problem in your code.
In Java, utilizing correct design patterns makes code more readable, reusable, and easier to maintain. This guide covers some of the most critical patterns categorized into Creational, Structural, and Behavioral patterns.
1. Creational Patterns
Creational patterns provide object creation mechanisms that increase flexibility and reuse of existing code.
Builder Pattern
The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create various representations.
When to use: When a class has many constructor parameters (the "Telescoping Constructor Anti-pattern") or when object creation involves multiple steps.
public class User {
private final String firstName; // Required
private final String lastName; // Required
private final int age; // Optional
private final String phone; // Optional
private User(UserBuilder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.phone = builder.phone;
}
public static class UserBuilder {
private final String firstName;
private final String lastName;
private int age;
private String phone;
public UserBuilder(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public UserBuilder age(int age) {
this.age = age;
return this;
}
public UserBuilder phone(String phone) {
this.phone = phone;
return this;
}
public User build() {
return new User(this);
}
}
}
// Usage
User user = new User.UserBuilder("John", "Doe")
.age(30)
.phone("123-456-7890")
.build();
(Note: Tools like Lombok @Builder automate this in modern Java).
Singleton Pattern
Ensures a class has only one instance and provides a global point of access to it.
// Thread-safe Singleton using Bill Pugh method
public class DatabaseConnection {
private DatabaseConnection() {}
private static class SingletonHelper {
private static final DatabaseConnection INSTANCE = new DatabaseConnection();
}
public static DatabaseConnection getInstance() {
return SingletonHelper.INSTANCE;
}
}
2. Structural Patterns
Structural patterns explain how to assemble objects and classes into larger structures while keeping these structures flexible and efficient.
Adapter Pattern
Allows objects with incompatible interfaces to collaborate.
// Old system
interface OldPaymentSystem {
void processOldPayment(String account, double amount);
}
// New system we want to use
interface ModernPaymentGateway {
void executePayment(String customerId, double total);
}
// Adapter
public class PaymentAdapter implements ModernPaymentGateway {
private OldPaymentSystem oldSystem;
public PaymentAdapter(OldPaymentSystem oldSystem) {
this.oldSystem = oldSystem;
}
@Override
public void executePayment(String customerId, double total) {
// Transform the modern request into the old format
oldSystem.processOldPayment(customerId, total);
}
}
Decorator Pattern
Lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.
interface Coffee {
double getCost();
String getDescription();
}
class SimpleCoffee implements Coffee {
public double getCost() { return 2.0; }
public String getDescription() { return "Simple Coffee"; }
}
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) { this.decoratedCoffee = coffee; }
public double getCost() { return decoratedCoffee.getCost(); }
public String getDescription() { return decoratedCoffee.getDescription(); }
}
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) { super(coffee); }
public double getCost() { return super.getCost() + 0.5; }
public String getDescription() { return super.getDescription() + ", Milk"; }
}
// Usage
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
System.out.println(coffee.getDescription() + " $" + coffee.getCost());
// Outputs: Simple Coffee, Milk $2.5
3. Behavioral Patterns
Behavioral patterns take care of effective communication and the assignment of responsibilities between objects.
Strategy Pattern
Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from the clients that use it.
// Strategy Interface
interface SortingStrategy {
void sort(int[] array);
}
// Concrete Strategies
class BubbleSort implements SortingStrategy {
public void sort(int[] array) { System.out.println("Sorting using Bubble Sort"); }
}
class QuickSort implements SortingStrategy {
public void sort(int[] array) { System.out.println("Sorting using Quick Sort"); }
}
// Context
class Sorter {
private SortingStrategy strategy;
public Sorter(SortingStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(SortingStrategy strategy) {
this.strategy = strategy;
}
public void executeSort(int[] array) {
strategy.sort(array);
}
}
// Usage
Sorter sorter = new Sorter(new QuickSort());
sorter.executeSort(new int[]{5, 2, 8});
Observer Pattern
Lets you define a subscription mechanism to notify multiple objects about any events that happen to the object they're observing.
import java.util.ArrayList;
import java.util.List;
interface Observer {
void update(String message);
}
class UserObserver implements Observer {
private String name;
public UserObserver(String name) { this.name = name; }
public void update(String message) { System.out.println(name + " received: " + message); }
}
class NewsPublisher {
private List<Observer> subscribers = new ArrayList<>();
public void subscribe(Observer observer) { subscribers.add(observer); }
public void unsubscribe(Observer observer) { subscribers.remove(observer); }
public void publishNews(String news) {
for (Observer sub : subscribers) {
sub.update(news);
}
}
}
Summary
Design patterns provide a standard terminology and are specific to particular scenarios.
- Creational patterns handle object instantiation.
- Structural patterns deal with class and object composition.
- Behavioral patterns focus on communication between objects.
Understanding these patterns is a hallmark of a senior Java developer, enabling you to communicate complex architectures efficiently and write highly maintainable code.