Creational Design Patterns¶
Overview¶
Creational design patterns abstract the instantiation process, making a system independent of how its objects are created, composed, and represented. These patterns help make a system independent of how its objects are created, composed, and represented. This guide covers the essential creational design patterns in Java, their implementation, use cases, advantages, and potential drawbacks.
Prerequisites¶
- Solid understanding of Java programming
- Familiarity with object-oriented programming concepts
- Basic knowledge of SOLID principles
- Understanding of class inheritance and interfaces
Learning Objectives¶
- Understand the purpose and benefits of creational design patterns
- Learn when and how to implement different creational patterns
- Recognize appropriate use cases for each pattern
- Implement creational patterns in Java applications
- Understand the trade-offs between different creational patterns
- Apply best practices when implementing creational patterns
Table of Contents¶
- Introduction to Creational Patterns
- Singleton Pattern
- Factory Method Pattern
- Abstract Factory Pattern
- Builder Pattern
- Prototype Pattern
- Object Pool Pattern
- Best Practices
- Common Pitfalls
- Comparing Creational Patterns
Introduction to Creational Patterns¶
Creational design patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. The basic form of object creation could result in design problems or added complexity to the design. Creational design patterns solve this problem by controlling the object creation process.
Why Use Creational Patterns?¶
- Flexibility: They provide flexibility in deciding which objects need to be created for a given case.
- Decoupling: They promote decoupling the system from how its objects are created, composed, and represented.
- Encapsulation: They encapsulate knowledge about which concrete classes the system uses.
- Hide Complexity: They hide the complexities of creating objects.
When to Use Creational Patterns¶
- When a system should be independent of how its products are created, composed, and represented
- When a class wants its subclasses to specify the objects it creates
- When you want to encapsulate object creation logic
- When you want to hide the complexity of creating complex objects
Singleton Pattern¶
The Singleton pattern ensures a class has only one instance and provides a global point of access to it.
Intent¶
- Ensure a class has only one instance
- Provide a global point of access to it
- Control concurrent access to a shared resource
Implementation¶
// Basic implementation
public class Singleton {
private static Singleton instance;
// Private constructor prevents instantiation from other classes
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
// Business methods
public void doSomething() {
System.out.println("Singleton is doing something");
}
}
Thread-Safe Implementation¶
// Thread-safe implementation with double-checked locking
public class ThreadSafeSingleton {
private static volatile ThreadSafeSingleton instance;
private ThreadSafeSingleton() {
}
public static ThreadSafeSingleton getInstance() {
if (instance == null) {
synchronized (ThreadSafeSingleton.class) {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
}
Enum Singleton (Java 5+)¶
// Using enum (thread-safe by default, handles serialization)
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
System.out.println("EnumSingleton is doing something");
}
}
// Usage:
EnumSingleton.INSTANCE.doSomething();
Initialization on Demand Holder Idiom¶
public class LazyInitializationSingleton {
private LazyInitializationSingleton() {
}
// Inner static class - not loaded until first access
private static class SingletonHolder {
private static final LazyInitializationSingleton INSTANCE = new LazyInitializationSingleton();
}
public static LazyInitializationSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
When to Use the Singleton Pattern¶
- When there must be exactly one instance of a class, and it must be accessible from a well-known access point
- When you need stricter control over global variables
- When a shared resource needs controlled access (e.g., a connection pool or registry)
Advantages¶
- Controlled access to sole instance
- Reduced namespace usage
- Can be refined through inheritance
- Can be configured with variable number of instances
- More flexible than static methods
Disadvantages¶
- Makes unit testing difficult
- Violates the Single Responsibility Principle
- May make the code more complex if not needed
- Special handling required for multi-threaded environments
- Can be difficult to subclass
Factory Method Pattern¶
The Factory Method pattern defines an interface for creating an object but lets subclasses decide which class to instantiate.
Intent¶
- Define an interface for creating an object, but let subclasses decide which class to instantiate
- Allow a class to defer instantiation to subclasses
- Create objects without specifying the exact class of object to be created
Implementation¶
// Product interface
interface Product {
void operation();
}
// Concrete products
class ConcreteProductA implements Product {
@Override
public void operation() {
System.out.println("Operation of ConcreteProductA");
}
}
class ConcreteProductB implements Product {
@Override
public void operation() {
System.out.println("Operation of ConcreteProductB");
}
}
// Creator abstract class
abstract class Creator {
// Factory method
public abstract Product createProduct();
// Operation that uses the factory method
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();
}
}
// Client code
Creator creator = new ConcreteCreatorA();
creator.someOperation(); // Outputs: Operation of ConcreteProductA
creator = new ConcreteCreatorB();
creator.someOperation(); // Outputs: Operation of ConcreteProductB
Parameterized Factory Method¶
enum ProductType {
TYPE_A, TYPE_B
}
class SimpleFactory {
public static Product createProduct(ProductType type) {
switch (type) {
case TYPE_A:
return new ConcreteProductA();
case TYPE_B:
return new ConcreteProductB();
default:
throw new IllegalArgumentException("Invalid product type");
}
}
}
// Usage
Product productA = SimpleFactory.createProduct(ProductType.TYPE_A);
productA.operation();
When to Use the Factory Method Pattern¶
- When a class can't anticipate the class of objects it must create
- When a class wants its subclasses to specify the objects it creates
- When classes delegate responsibility to one of several helper subclasses, and you want to localize the knowledge of which helper subclass is the delegate
Advantages¶
- Provides hooks for subclasses to extend a class's internal object creation
- Connects parallel class hierarchies
- Decouples the implementation of an object from its use
- Follows the "Open/Closed Principle"
- Promotes loose coupling
Disadvantages¶
- May lead to large number of small classes
- Complexity may increase as the pattern requires creating new subclasses
Abstract Factory Pattern¶
The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.
Intent¶
- Provide an interface for creating families of related or dependent objects without specifying their concrete classes
- Create a system that is independent of how its products are created, composed, and represented
- Support the creation of products that must work together
Implementation¶
// Abstract products
interface Button {
void render();
void onClick();
}
interface Checkbox {
void render();
void toggle();
}
// Concrete products for Windows
class WindowsButton implements Button {
@Override
public void render() {
System.out.println("Render Windows button");
}
@Override
public void onClick() {
System.out.println("Windows button clicked");
}
}
class WindowsCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("Render Windows checkbox");
}
@Override
public void toggle() {
System.out.println("Windows checkbox toggled");
}
}
// Concrete products for macOS
class MacOSButton implements Button {
@Override
public void render() {
System.out.println("Render macOS button");
}
@Override
public void onClick() {
System.out.println("macOS button clicked");
}
}
class MacOSCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("Render macOS checkbox");
}
@Override
public void toggle() {
System.out.println("macOS checkbox toggled");
}
}
// 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 render() {
button.render();
checkbox.render();
}
}
// Usage
GUIFactory factory = new WindowsFactory();
Application app = new Application(factory);
app.render(); // Renders Windows UI components
factory = new MacOSFactory();
app = new Application(factory);
app.render(); // Renders macOS UI components
When to Use the Abstract Factory Pattern¶
- When a system should be independent of how its products are created, composed, and represented
- When a system should be configured with one of multiple families of products
- When a family of related product objects is designed to be used together, and you need to enforce this constraint
- When you want to provide a class library of products, and you want to reveal just their interfaces, not their implementations
Advantages¶
- Isolates concrete classes from the client
- Makes exchanging product families easy
- Promotes consistency among products
- Follows the "Open/Closed Principle"
- Supports the Dependency Inversion Principle
Disadvantages¶
- Adding new products requires changing the abstract factory interface and all implementations
- Complexity may increase as the pattern requires many interfaces and classes
Builder Pattern¶
The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
Intent¶
- Separate the construction of a complex object from its representation
- Allow the same construction process to create different representations
- Provide a clear step-by-step creation of objects
Implementation¶
// Product
class Pizza {
private String dough;
private String sauce;
private String topping;
public void setDough(String dough) {
this.dough = dough;
}
public void setSauce(String sauce) {
this.sauce = sauce;
}
public void setTopping(String topping) {
this.topping = topping;
}
@Override
public String toString() {
return "Pizza with " + dough + " dough, " + sauce + " sauce, and " + topping + " topping.";
}
}
// Builder interface
interface PizzaBuilder {
void buildDough();
void buildSauce();
void buildTopping();
Pizza getPizza();
}
// Concrete builders
class HawaiianPizzaBuilder implements PizzaBuilder {
private Pizza pizza;
public HawaiianPizzaBuilder() {
this.pizza = new Pizza();
}
@Override
public void buildDough() {
pizza.setDough("thin");
}
@Override
public void buildSauce() {
pizza.setSauce("mild");
}
@Override
public void buildTopping() {
pizza.setTopping("ham and pineapple");
}
@Override
public Pizza getPizza() {
return pizza;
}
}
class SpicyPizzaBuilder implements PizzaBuilder {
private Pizza pizza;
public SpicyPizzaBuilder() {
this.pizza = new Pizza();
}
@Override
public void buildDough() {
pizza.setDough("thick");
}
@Override
public void buildSauce() {
pizza.setSauce("hot");
}
@Override
public void buildTopping() {
pizza.setTopping("pepperoni and jalapeños");
}
@Override
public Pizza getPizza() {
return pizza;
}
}
// Director
class Waiter {
private PizzaBuilder pizzaBuilder;
public void setPizzaBuilder(PizzaBuilder pb) {
this.pizzaBuilder = pb;
}
public Pizza getPizza() {
return pizzaBuilder.getPizza();
}
public void constructPizza() {
pizzaBuilder.buildDough();
pizzaBuilder.buildSauce();
pizzaBuilder.buildTopping();
}
}
// Client code
Waiter waiter = new Waiter();
PizzaBuilder hawaiianBuilder = new HawaiianPizzaBuilder();
waiter.setPizzaBuilder(hawaiianBuilder);
waiter.constructPizza();
Pizza pizza = waiter.getPizza();
System.out.println(pizza); // Outputs: Pizza with thin dough, mild sauce, and ham and pineapple topping.
Modern Builder Pattern (Fluent Builder)¶
// Modern builder pattern with fluent interface
class User {
// Required parameters
private final String firstName;
private final String lastName;
// Optional parameters
private final int age;
private final String phone;
private final String address;
private User(UserBuilder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public int getAge() {
return age;
}
public String getPhone() {
return phone;
}
public String getAddress() {
return address;
}
@Override
public String toString() {
return "User: " + firstName + " " + lastName + ", " + age + " years old, phone: " +
phone + ", address: " + address;
}
// Builder class
public static class UserBuilder {
// Required parameters
private final String firstName;
private final String lastName;
// Optional parameters - initialized with default values
private int age = 0;
private String phone = "";
private String address = "";
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 UserBuilder address(String address) {
this.address = address;
return this;
}
public User build() {
return new User(this);
}
}
}
// Usage
User user = new User.UserBuilder("John", "Doe")
.age(30)
.phone("1234567890")
.address("123 Street, City")
.build();
System.out.println(user);
When to 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
- When the construction process must allow different representations for the object that's constructed
- When you need to build complex objects step by step
- When you need to create immutable objects with many optional parameters
Advantages¶
- Clear separation between object construction and representation
- Provides better control over the construction process
- Supports creating immutable objects
- Creates complex objects step by step
- Enables the creation of different representations of an object
Disadvantages¶
- Code complexity increases due to additional classes and interfaces
- Creates additional overhead for simple objects
- The builder must be mutable while the final object may be immutable
Prototype Pattern¶
The Prototype pattern is used to create new objects by copying an existing object, known as the prototype.
Intent¶
- Specify the kinds of objects to create using a prototypical instance
- Create new objects by copying this prototype
- Reduce the need for subclassing
- Hide the complexity of creating new instances from the client
Implementation¶
// 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) {
// This shouldn't happen since we implement Cloneable
return null;
}
}
}
// Client code
ConcretePrototype original = new ConcretePrototype("Original Value");
ConcretePrototype copy = (ConcretePrototype) original.clone();
System.out.println(original.getField()); // Outputs: Original Value
System.out.println(copy.getField()); // Outputs: Original Value
copy.setField("Modified Value");
System.out.println(original.getField()); // Outputs: Original Value
System.out.println(copy.getField()); // Outputs: Modified Value
Deep vs. Shallow Cloning¶
// Example of a class with references that requires deep cloning
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();
}
}
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;
}
// Shallow copy
public Person shallowCopy() throws CloneNotSupportedException {
return (Person) super.clone();
}
// Deep copy
public Person deepCopy() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = (Address) this.address.clone();
return cloned;
}
}
// Testing deep vs. shallow copy
Person original = new Person("John", new Address("123 Street", "New York"));
// Shallow copy
Person shallowCopy = original.shallowCopy();
// Deep copy
Person deepCopy = original.deepCopy();
// Change address in shallow copy
shallowCopy.getAddress().setCity("Boston");
// Print original address city
System.out.println(original.getAddress().getCity()); // Outputs: Boston (changed because of shallow copy)
// Change address in deep copy
deepCopy.getAddress().setCity("Chicago");
// Print original address city
System.out.println(original.getAddress().getCity()); // Outputs: Boston (unchanged because deep copy doesn't affect original)
Prototype Registry¶
// Prototype registry for storing and retrieving prototypes
class PrototypeRegistry {
private Map<String, Prototype> prototypes = new HashMap<>();
public void addPrototype(String key, Prototype prototype) {
prototypes.put(key, prototype);
}
public Prototype getPrototype(String key) {
return prototypes.get(key).clone();
}
}
// Usage
PrototypeRegistry registry = new PrototypeRegistry();
ConcretePrototype prototypeA = new ConcretePrototype("Prototype A");
registry.addPrototype("A", prototypeA);
ConcretePrototype prototypeB = new ConcretePrototype("Prototype B");
registry.addPrototype("B", prototypeB);
ConcretePrototype cloneA = (ConcretePrototype) registry.getPrototype("A");
System.out.println(cloneA.getField()); // Outputs: Prototype A
When to Use the Prototype Pattern¶
- When the classes to instantiate are specified at run-time
- When avoiding the inherent cost of creating a new object in the standard way (e.g., when it's prohibitively expensive)
- When objects have few variations in state
- When composing objects requires complex processes
- When the client application needs to be unaware of object creation and representation
Advantages¶
- Reduces the need for subclassing
- Hides complexities of creating objects
- Clients can work with application-specific classes without modification
- Add and remove products at run-time
- Specify new objects by varying values
- Can reduce the number of classes needed
Disadvantages¶
- Each subclass of Prototype must implement the clone operation
- Implementing clone can be difficult when the objects have circular references
- Deep copying complex objects can be challenging and error-prone
Object Pool Pattern¶
The Object Pool pattern recycles and reuses expensive objects rather than creating and destroying them on demand.
Intent¶
- Improve performance and memory usage by reusing objects
- Reduce the overhead of initialization and destruction
- Provide a mechanism to limit the number of instantiated objects
Implementation¶
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.Supplier;
class ObjectPool<T> {
private BlockingQueue<T> pool;
private Supplier<T> objectFactory;
public ObjectPool(int size, Supplier<T> objectFactory) {
this.objectFactory = objectFactory;
this.pool = new LinkedBlockingQueue<>(size);
// Initialize the pool with objects
for (int i = 0; i < size; i++) {
pool.add(objectFactory.get());
}
}
public T borrowObject() throws InterruptedException {
return pool.take();
}
public void returnObject(T object) throws InterruptedException {
pool.put(object);
}
public int getSize() {
return pool.size();
}
}
// Example resource class
class DatabaseConnection {
private String connectionId;
public DatabaseConnection(String connectionId) {
this.connectionId = connectionId;
// Simulate expensive resource initialization
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Connection created: " + connectionId);
}
public String getConnectionId() {
return connectionId;
}
public void executeQuery(String query) {
System.out.println("Executing query: " + query + " using connection: " + connectionId);
}
public void close() {
// Cleanup resources
System.out.println("Connection closed: " + connectionId);
}
}
// Usage
int poolSize = 5;
AtomicInteger counter = new AtomicInteger();
ObjectPool<DatabaseConnection> connectionPool = new ObjectPool<>(poolSize,
() -> new DatabaseConnection("Connection-" + counter.incrementAndGet()));
// Using the pool
try {
DatabaseConnection connection = connectionPool.borrowObject();
connection.executeQuery("SELECT * FROM users");
connectionPool.returnObject(connection);
// Borrow again
connection = connectionPool.borrowObject();
connection.executeQuery("SELECT * FROM products");
connectionPool.returnObject(connection);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Generic Object Pool with Auto-Return¶
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.Consumer;
import java.util.function.Supplier;
class AutoReturnObjectPool<T> {
private BlockingQueue<T> pool;
private Supplier<T> objectFactory;
private Consumer<T> objectReset;
public AutoReturnObjectPool(int size, Supplier<T> objectFactory, Consumer<T> objectReset) {
this.objectFactory = objectFactory;
this.objectReset = objectReset;
this.pool = new LinkedBlockingQueue<>(size);
// Initialize the pool with objects
for (int i = 0; i < size; i++) {
pool.add(objectFactory.get());
}
}
public void executeWithObject(Consumer<T> action) throws InterruptedException {
T object = pool.take();
try {
action.accept(object);
} finally {
objectReset.accept(object);
pool.put(object);
}
}
}
// Usage
AutoReturnObjectPool<DatabaseConnection> connectionPool = new AutoReturnObjectPool<>(
5,
() -> new DatabaseConnection("Connection-" + counter.incrementAndGet()),
connection -> { /* Reset connection state if needed */ }
);
// Using the pool with auto-return
try {
connectionPool.executeWithObject(connection -> {
connection.executeQuery("SELECT * FROM users");
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
When to Use the Object Pool Pattern¶
- When objects are expensive to create (database connections, thread pools, etc.)
- When you need to limit the number of objects created (e.g., manage licensing limitations)
- When object initialization is resource-intensive but the object is needed only temporarily
- When the rate of object creation and destruction is high
Advantages¶
- Improves performance by reusing objects
- Reduces garbage collection overhead
- Controls resource usage by limiting the number of objects
- Allows objects to be pre-initialized
- Predictable memory usage
Disadvantages¶
- Increases complexity
- Can lead to resource leaks if objects aren't properly returned to the pool
- May result in stale objects if not properly managed
- Not suitable for lightweight objects
- Can cause threading issues if not implemented correctly
Best Practices¶
General Best Practices for Creational Patterns¶
-
Choose the Right Pattern: Select the appropriate pattern based on your specific requirements and constraints.
-
Keep It Simple: Don't over-engineer solutions. Use the simplest pattern that meets your needs.
-
Encapsulate What Varies: Identify what aspects of your application might change, and encapsulate them.
-
Program to Interfaces: Define interfaces or abstract classes for creators and products.
-
Use Composition Over Inheritance: Whenever possible, prefer composition over inheritance for more flexible designs.
-
Follow SOLID Principles: Ensure your implementations follow SOLID principles, especially Single Responsibility and Open/Closed principles.
-
Consider Performance: For performance-critical applications, be mindful of the overhead introduced by certain patterns.
-
Document Your Patterns: Make it clear to other developers which patterns you're using and why.
Singleton Best Practices¶
- Consider using enum singletons in Java for simplicity and thread safety
- Be careful with lazy initialization in multi-threaded environments
- If using double-checked locking, ensure the instance field is volatile
- Consider using dependency injection instead of singletons for better testability
Factory Method Best Practices¶
- Define a clear factory method interface
- Keep the factory method focused on object creation
- Consider using parameterized factory methods for flexibility
- Use static factory methods for simple cases
Abstract Factory Best Practices¶
- Keep product families coherent and related
- Design for extensibility when adding new products
- Create comprehensive interfaces for both factories and products
- Consider using factory methods within your abstract factories
Builder Best Practices¶
- Use the fluent interface pattern for a cleaner API
- Consider making the product immutable
- Validate parameters in the build method
- Consider using a director class for complex construction processes
- Use static inner builder classes for convenience and encapsulation
Prototype Best Practices¶
- Implement a proper deep copy mechanism for complex objects
- Consider serialization for deep copying when appropriate
- Provide a common cloning interface
- Use a prototype registry or manager for organizing and accessing prototypes
Common Pitfalls¶
Overusing Patterns¶
One of the most common mistakes is using design patterns when they're not needed, leading to unnecessary complexity.
// Unnecessary use of Singleton pattern for simple utility methods
public class MathUtils {
private static MathUtils instance;
private MathUtils() {}
public static MathUtils getInstance() {
if (instance == null) {
instance = new MathUtils();
}
return instance;
}
public int add(int a, int b) {
return a + b;
}
}
// Better approach - use static methods
public class MathUtils {
private MathUtils() {} // Prevent instantiation
public static int add(int a, int b) {
return a + b;
}
}
Ignoring Thread Safety¶
Not considering concurrency issues can lead to bugs in multi-threaded environments.
// Incorrect Singleton in multi-threaded environment
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // Multiple threads might pass this check simultaneously
instance = new Singleton();
}
return instance;
}
}
// Correct thread-safe implementation
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Not Considering Object Lifecycle¶
Failing to properly manage object lifecycles can lead to memory leaks or excessive resource consumption.
// Resource leaks in object pool
public void processItems(List<Item> items) {
DatabaseConnection connection = connectionPool.borrowObject();
for (Item item : items) {
try {
// Process item using connection
} catch (Exception e) {
// If an exception occurs, the connection is never returned!
logger.error("Error processing item", e);
}
}
connectionPool.returnObject(connection);
}
// Better approach - ensure the resource is always returned
public void processItems(List<Item> items) {
DatabaseConnection connection = null;
try {
connection = connectionPool.borrowObject();
for (Item item : items) {
try {
// Process item using connection
} catch (Exception e) {
logger.error("Error processing item", e);
// Continue processing other items
}
}
} finally {
if (connection != null) {
connectionPool.returnObject(connection);
}
}
}
Complexity Creep¶
Adding unnecessary complexity as the system evolves.
// Over-engineered factory
interface ProductFactory {
Product createProduct();
void registerProduct(Product product);
void validateProduct(Product product);
void initializeProduct(Product product);
// ... more methods that don't belong in a factory
}
// Better approach - keep factories focused
interface ProductFactory {
Product createProduct();
}
// Separate concerns into different classes
interface ProductRegistry {
void registerProduct(Product product);
}
interface ProductValidator {
boolean validateProduct(Product product);
}
Comparing Creational Patterns¶
When to Choose Which Pattern¶
Pattern | When to Use |
---|---|
Singleton | When you need exactly one instance of a class system-wide |
Factory Method | When you want subclasses to decide which concrete classes to instantiate |
Abstract Factory | When your system needs to use multiple related object families |
Builder | When you need to create complex objects step by step with many optional parameters |
Prototype | When creating new objects by copying existing ones is more efficient |
Object Pool | When creating objects is expensive and you need to reuse them |
Comparison of Key Characteristics¶
Pattern | Complexity | Flexibility | Use Case Focus |
---|---|---|---|
Singleton | Low | Low | Resource Management |
Factory Method | Medium | Medium | Subclass Instantiation |
Abstract Factory | High | High | Product Families |
Builder | Medium | High | Complex Object Construction |
Prototype | Low | Medium | Object Copying |
Object Pool | Medium | Low | Object Reuse |
Pattern Combinations¶
Creational patterns can be combined for more powerful solutions:
- Builder + Singleton: A singleton builder that constructs complex objects.
- Factory Method + Prototype: A factory that returns clones of prototype objects.
- Abstract Factory + Builder: An abstract factory that uses builders to create complex objects.
- Object Pool + Factory Method: A factory method that draws objects from a pool.
Summary¶
Creational design patterns provide solutions to object creation problems, making systems more flexible, maintainable, and decoupled:
- Singleton Pattern: Ensures a class has only one instance with global access.
- Factory Method Pattern: Lets subclasses decide which class to instantiate.
- Abstract Factory Pattern: Creates families of related objects without specifying concrete classes.
- Builder Pattern: Separates complex object construction from its representation.
- Prototype Pattern: Creates new objects by copying existing ones.
- Object Pool Pattern: Recycles and reuses objects to improve performance.
Each pattern has its specific use cases, advantages, and disadvantages. The key to effective use of creational patterns is understanding when to apply them and how to combine them to solve specific design problems.
By mastering these patterns, you'll be able to create more flexible, maintainable, and extensible code that follows good object-oriented design principles.
Further Reading¶
- "Design Patterns: Elements of Reusable Object-Oriented Software" by Gamma, Helm, Johnson, and Vlissides
- "Head First Design Patterns" by Eric Freeman and Elisabeth Robson
- "Effective Java" by Joshua Bloch
- "Clean Code" by Robert C. Martin
- Refactoring Guru - Design Patterns
- SourceMaking - Design Patterns
- Java Design Patterns