Creational Design Patterns in Java
Introduction
In software engineering, Design Patterns are proven, reusable solutions to common problems that occur during software design. Creational Patterns specifically deal with the process of object creation.
Directly instantiating objects using the new keyword scattered throughout a codebase can lead to tight coupling, making the system hard to test, modify, and extend. Creational patterns abstract the instantiation process, making systems independent of how their objects are created, composed, and represented.
1. Singleton Pattern
Purpose: Ensures that a class has only one instance while providing a global point of access to it.
When to use: For resources that are expensive to create or need coordinated access across the system (e.g., database connection pools, logging services, configuration managers).
Implementation (Thread-Safe with Double-Checked Locking):
public class ConfigurationManager {
// Volatile ensures visibility of changes across threads
private static volatile ConfigurationManager instance;
private ConfigurationManager() {
// Private constructor prevents instantiation
}
public static ConfigurationManager getInstance() {
if (instance == null) {
synchronized (ConfigurationManager.class) {
if (instance == null) {
instance = new ConfigurationManager();
}
}
}
return instance;
}
}
2. Factory Method Pattern
Purpose: Defines an interface for creating an object, but lets subclasses alter the type of objects that will be created.
When to use: When a class cannot anticipate the class of objects it must create, or when a class wants its subclasses to specify the objects it creates.
Implementation:
interface Notification {
void notifyUser();
}
class EmailNotification implements Notification {
public void notifyUser() { System.out.println("Sending an e-mail"); }
}
class SMSNotification implements Notification {
public void notifyUser() { System.out.println("Sending an SMS"); }
}
abstract class NotificationFactory {
// The Factory Method
public abstract Notification createNotification();
public void processNotification() {
Notification notification = createNotification();
notification.notifyUser();
}
}
class EmailNotificationFactory extends NotificationFactory {
public Notification createNotification() {
return new EmailNotification();
}
}
3. Abstract Factory Pattern
Purpose: Provides an interface for creating families of related or dependent objects without specifying their concrete classes. It is essentially a factory of factories.
When to use: When a system should be configured with one of multiple families of products (e.g., UI elements for Windows vs. MacOS).
Implementation:
interface Button { void paint(); }
interface Checkbox { void render(); }
// Windows variants
class WinButton implements Button { public void paint() {} }
class WinCheckbox implements Checkbox { public void render() {} }
// MacOS variants
class MacButton implements Button { public void paint() {} }
class MacCheckbox implements Checkbox { public void render() {} }
// The Abstract Factory
interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}
class WindowsFactory implements GUIFactory {
public Button createButton() { return new WinButton(); }
public Checkbox createCheckbox() { return new WinCheckbox(); }
}
4. Builder Pattern
Purpose: Separates the construction of a complex object from its representation, allowing the same construction process to create various representations.
When to use: When an object requires multiple steps to build, or when a class has many optional parameters in its constructor (solving the "Telescoping Constructor" anti-pattern).
Implementation:
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).build();
5. Prototype Pattern
Purpose: Creates new objects by copying an existing object (the prototype) rather than creating them from scratch using constructors.
When to use: When the cost of creating a new instance is more expensive than copying an existing one, or to hide the complexity of creating a new instance.
Implementation (Using Java's Cloneable):
abstract class Shape implements Cloneable {
private String id;
protected String type;
abstract void draw();
public Object clone() {
Object clone = null;
try {
clone = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
}
class Rectangle extends Shape {
public Rectangle() { type = "Rectangle"; }
@Override
public void draw() { System.out.println("Inside Rectangle::draw() method."); }
}
Summary
Creational patterns decouple the system from how its objects are created. By leveraging Singletons, Factories, Builders, and Prototypes, you can construct Java applications that are inherently more flexible, scalable, and easier to maintain in the long run.