Singleton & Factory Patterns
Design patterns are reusable solutions to common problems in software design. We begin our exploration with Creational Patterns, which handle object creation mechanisms, trying to create objects in a manner suitable to the situation.
1. The Singleton Pattern
The Problem
Sometimes you need exactly one instance of a class. For example, a configuration manager, a thread pool, or a database connection pool. If multiple instances are created, it could lead to inconsistent state or wasted resources.
The Solution
The Singleton pattern ensures a class has only one instance and provides a global point of access to it. It achieves this by:
- Making the constructor private.
- Providing a static method that returns the single instance.
Java Implementation (Thread-Safe)
public class DatabaseConnector {
// 1. Private static instance
private static volatile DatabaseConnector instance;
// 2. Private constructor
private DatabaseConnector() {
// Initialize connection here
}
// 3. Static access method with Double-Checked Locking
public static DatabaseConnector getInstance() {
if (instance == null) {
synchronized (DatabaseConnector.class) {
if (instance == null) {
instance = new DatabaseConnector();
}
}
}
return instance;
}
public void query(String sql) {
System.out.println("Executing: " + sql);
}
}
Pros and Cons
- Pros: Controlled access to a single instance, reduced memory footprint.
- Cons: Can be difficult to unit test (global state), can hide dependencies.
2. The Factory Method Pattern
The Problem
Imagine a logistics app that initially only handles truck transport. If you want to add maritime transport, you’d have to change a lot of code everywhere you instantiate a Truck. Direct instantiation (new Truck()) couples your code to specific classes.
The Solution
The Factory Method pattern defines an interface for creating an object but lets subclasses decide which class to instantiate.
Python Example
from abc import ABC, abstractmethod
# Product Interface
class Transport(ABC):
@abstractmethod
def deliver(self):
pass
# Concrete Products
class Truck(Transport):
def deliver(self):
return "Delivering by land in a box."
class Ship(Transport):
def deliver(self):
return "Delivering by sea in a container."
# Creator (Factory)
class Logistics(ABC):
@abstractmethod
def create_transport(self) -> Transport:
pass
def plan_delivery(self):
transport = self.create_transport()
return f"Logistics: {transport.deliver()}"
# Concrete Creators
class RoadLogistics(Logistics):
def create_transport(self) -> Transport:
return Truck()
class SeaLogistics(Logistics):
def create_transport(self) -> Transport:
return Ship()
# Usage
logistics = RoadLogistics()
print(logistics.plan_delivery())
When to use Factory Method?
- When a class can’t anticipate the class of objects it must create.
- When you want to provide users of your library with a way to extend its internal components.
3. Abstract Factory (Bonus)
While the Factory Method creates one product, the Abstract Factory creates families of related products.
Imagine a UI toolkit that needs to create Buttons, Checkboxes, and Sliders. You might have a WindowsFactory and a MacFactory. Each factory produces a set of components that “match” each other.
Key Difference
- Factory Method: One method, one product.
- Abstract Factory: Many methods, many related products.
Comparison Summary
| Pattern | Focus | Use Case |
|---|---|---|
| Singleton | Uniqueness | Resource managers, global state. |
| Factory Method | Subclassing | When exact types are determined by subclasses. |
| Abstract Factory | Families | When products must work together (themes, OS). |