In software development, design patterns are proven solutions to recurring problems in software design. They provide a blueprint on how to structure and organize your code to address common complexities. Rather than reinventing the wheel, developers can rely on well-tested patterns to make their applications more robust, maintainable, and clear.
In this article, we will explore three categories of design patterns:
- Creational Patterns
- Structural Patterns
- Behavioral Patterns
Creational Patterns
Creational patterns handle the efficient and reliable creation of objects. They abstract the instantiation process to make systems independent of how objects are created and represented.
Singleton Pattern
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This is particularly useful for managing shared resources such as configuration settings, thread pools, or database connections.
When to Use:
- You want to control access to a single instance of a class.
- You need a centralized resource that should not be duplicated.
Code Example:
public class DatabaseConnection {
private static DatabaseConnection instance;
private DatabaseConnection() {}
public static synchronized DatabaseConnection getInstance() {
if (instance == null) {
instance = new DatabaseConnection();
}
return instance;
}
public void query(String sql) {
System.out.println("Executing SQL: " + sql);
}
}
Factory Method Pattern
The Factory Method pattern defines an interface for creating an object, allowing subclasses to decide which concrete classes to instantiate. It enables flexibility in deciding the type of objects to create at runtime.
When to Use:
- You have a superclass that outlines the creation method, but subclasses determine which class to create.
- You want to delegate the object creation to subclasses without exposing creation logic to the client.
Code Example:
abstract class Dialog {
public abstract Button createButton();
public void render() {
Button button = createButton();
button.onClick();
button.render();
}
}
class WindowsDialog extends Dialog {
@Override
public Button createButton() {
return new WindowsButton();
}
}
Abstract Factory Pattern
The Abstract Factory pattern provides an interface to create families of related or dependent objects without specifying their concrete classes. It is useful when you need to ensure that products created together are compatible.
When to Use:
- You need to produce related objects (e.g., GUI elements) that must work together.
- You want to enforce product families without specifying concrete classes at compile time.
Code Example:
interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}
class WinFactory implements GUIFactory {
public Button createButton() { return new WindowsButton(); }
public Checkbox createCheckbox() { return new WindowsCheckbox(); }
}
Structural Patterns
Structural patterns help you organize classes and objects into large, more complex structures. They focus on how classes and objects compose to form new functionalities.
Adapter Pattern
The Adapter pattern allows incompatible classes to work together by converting the interface of one class into an interface expected by the client.
When to Use:
- You want to reuse existing classes that do not have compatible interfaces.
- You are integrating third-party libraries or legacy code.
Code Example:
interface MediaPlayer {
void play(String audioType, String fileName);
}
class AudioPlayer implements MediaPlayer {
public void play(String audioType, String fileName) {
if("mp3".equalsIgnoreCase(audioType)) {
System.out.println("Playing mp3: " + fileName);
} else {
MediaAdapter adapter = new MediaAdapter(audioType);
adapter.play(audioType, fileName);
}
}
}
Decorator Pattern
The Decorator pattern adds responsibilities to an object dynamically. It provides a flexible and transparent alternative to subclassing for extending functionality.
When to Use:
- You want to add features to objects at runtime without affecting other instances.
- You have many optional features that could be composed in different ways.
Code Example:
interface Coffee {
double getCost();
String getDescription();
}
class SimpleCoffee implements Coffee {
public double getCost() { return 2.0; }
public String getDescription() { return "Simple Coffee"; }
}
Facade Pattern
The Facade pattern provides a simplified interface to a complex subsystem, making the subsystem easier to use and reducing dependencies between clients and the subsystem.
When to Use:
- You want to hide complexity behind a simple interface.
- You have a complex system that can be daunting to use or maintain directly.
Code Example:
class ComputerFacade {
private CPU cpu;
private Memory memory;
private HardDrive hardDrive;
public ComputerFacade() {
this.cpu = new CPU();
this.memory = new Memory();
this.hardDrive = new HardDrive();
}
public void start() {
cpu.freeze();
memory.load(0, hardDrive.read(0, 1024));
cpu.jump(0);
cpu.execute();
}
}
Behavioral Patterns
Behavioral patterns focus on communication between objects and how responsibilities are distributed. They define patterns for algorithm encapsulation, event-driven interactions, and dynamic behavior changes.
Strategy Pattern
The Strategy pattern encapsulates multiple algorithms or behaviors and makes them interchangeable at runtime. This avoids conditional logic and keeps code flexible.
When to Use:
- You want to switch between different algorithms on the fly.
- You have families of related functionalities that can vary independently.
Code Example:
interface PaymentStrategy {
void pay(int amount);
}
class CreditCardStrategy implements PaymentStrategy {
private String cardNumber;
public CreditCardStrategy(String cardNumber) {
this.cardNumber = cardNumber;
}
public void pay(int amount) {
System.out.println("Paid " + amount + " using Credit Card: " + cardNumber);
}
}
Observer Pattern
The Observer pattern establishes a one-to-many relationship between objects so that when the state of one object changes, all its dependents are notified and updated automatically.
When to Use:
- You have an object whose changes should trigger updates in other objects.
- You want to maintain loose coupling between objects.
Code Example:
interface Observer {
void update(float temperature, float humidity);
}
class WeatherStation implements Subject {
// ... implementation to register, remove, and notify observers
}
Command Pattern
The Command pattern encapsulates requests as objects, enabling queuing, logging, and potential undo/redo operations. It decouples the object making the request from the one that executes it.
When to Use:
- You need to issue requests at different times or in different contexts.
- You want to implement undo/redo functionality or maintain command history.
Code Example:
interface Command {
void execute();
}
class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
}
Conclusion
Design patterns are not rigid rules but guidelines that help solve common problems in software design. As you gain experience, you will recognize scenarios where these patterns become natural solutions. The patterns discussed—such as the Singleton, Factory Method, Abstract Factory, Adapter, Decorator, Facade, Strategy, Observer, and Command—form a core toolkit that developers can draw upon to create cleaner, more modular, and scalable Java applications.