Singleton and Factory Pattern: Controlled Object Creation
Creational design patterns manage object creation in a flexible and reusable way. Singleton ensures a single instance of a class, while Factory delegates object creation to specialized methods. Both are fundamental for solid software architecture.
🎯 What You'll Learn
- Singleton pattern: unique global instance
- Factory Method: delegate creation to subclasses
- Abstract Factory: families of related objects
- When to use each pattern
Singleton Pattern
The Singleton ensures a class has only one instance and provides a global access point to it. Useful for shared resources like database connections, loggers, caches.
class DatabaseConnection {{ '{' }}
private static instance: DatabaseConnection;
private connected: boolean = false;
// Private constructor: prevents `new DatabaseConnection()`
private constructor() {{ '{' }}
console.log("DatabaseConnection created");
{{ '}' }}
// Static method to get the unique instance
public static getInstance(): DatabaseConnection {{ '{' }}
if (!DatabaseConnection.instance) {{ '{' }}
DatabaseConnection.instance = new DatabaseConnection();
{{ '}' }}
return DatabaseConnection.instance;
{{ '}' }}
public connect(): void {{ '{' }}
if (!this.connected) {{ '{' }}
console.log("Connecting to database...");
this.connected = true;
{{ '}' }}
{{ '}' }}
public query(sql: string): void {{ '{' }}
console.log(`Executing: ${{ '{' }}sql{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// Usage
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true - same instance!
db1.connect();
db2.query("SELECT * FROM users"); // Uses same connection
Singleton with Lazy Initialization
The instance is created only when needed, not at application startup:
class Logger {{ '{' }}
private static instance: Logger;
private logs: string[] = [];
private constructor() {{ '{' }}{{ '}' }}
public static getInstance(): Logger {{ '{' }}
// Created only on first call
if (!Logger.instance) {{ '{' }}
console.log("Logger instance created");
Logger.instance = new Logger();
{{ '}' }}
return Logger.instance;
{{ '}' }}
public log(message: string): void {{ '{' }}
const timestamp = new Date().toISOString();
this.logs.push(`[${{ '{' }}timestamp{{ '}' }}] ${{ '{' }}message{{ '}' }}`);
console.log(this.logs[this.logs.length - 1]);
{{ '}' }}
public getLogs(): string[] {{ '{' }}
return [...this.logs];
{{ '}' }}
{{ '}' }}
// First call: creates instance
const logger = Logger.getInstance(); // "Logger instance created"
logger.log("Application started");
// Subsequent calls: reuse instance
const logger2 = Logger.getInstance(); // No creation log
logger2.log("User logged in");
console.log(logger.getLogs());
// ["[2024-12-26T...] Application started", "[2024-12-26T...] User logged in"]
Singleton: Pros and Cons
✅ Advantages:
- Guarantees unique instance
- Global access
- Lazy initialization
- Control over shared resources
❌ Disadvantages:
- Global state (hard to test)
- Violates Single Responsibility
- Strong coupling
- Problems with multi-threading
⚠️ Alternatives to Singleton
In many cases, Dependency Injection is preferable to Singleton. Frameworks like Angular use DI to provide single instances without Singleton pattern problems.
Factory Method Pattern
The Factory Method defines an interface for creating objects, but delegates to subclasses the decision on which concrete class to instantiate.
// Product interface
interface Vehicle {{ '{' }}
drive(): void;
getType(): string;
{{ '}' }}
// Concrete products
class Car implements Vehicle {{ '{' }}
drive(): void {{ '{' }}
console.log("Driving a car 🚗");
{{ '}' }}
getType(): string {{ '{' }}
return "Car";
{{ '}' }}
{{ '}' }}
class Motorcycle implements Vehicle {{ '{' }}
drive(): void {{ '{' }}
console.log("Riding a motorcycle 🏍️");
{{ '}' }}
getType(): string {{ '{' }}
return "Motorcycle";
{{ '}' }}
{{ '}' }}
class Truck implements Vehicle {{ '{' }}
drive(): void {{ '{' }}
console.log("Driving a truck 🚚");
{{ '}' }}
getType(): string {{ '{' }}
return "Truck";
{{ '}' }}
{{ '}' }}
// Abstract factory
abstract class VehicleFactory {{ '{' }}
// Factory method (to be implemented in subclasses)
abstract createVehicle(): Vehicle;
// Method that uses the factory method
public deliverVehicle(): void {{ '{' }}
const vehicle = this.createVehicle();
console.log(`Delivering a ${{ '{' }}vehicle.getType(){{ '}' }}`);
vehicle.drive();
{{ '}' }}
{{ '}' }}
// Concrete factories
class CarFactory extends VehicleFactory {{ '{' }}
createVehicle(): Vehicle {{ '{' }}
return new Car();
{{ '}' }}
{{ '}' }}
class MotorcycleFactory extends VehicleFactory {{ '{' }}
createVehicle(): Vehicle {{ '{' }}
return new Motorcycle();
{{ '}' }}
{{ '}' }}
class TruckFactory extends VehicleFactory {{ '{' }}
createVehicle(): Vehicle {{ '{' }}
return new Truck();
{{ '}' }}
{{ '}' }}
// Usage
function clientCode(factory: VehicleFactory) {{ '{' }}
factory.deliverVehicle();
{{ '}' }}
clientCode(new CarFactory());
// "Delivering a Car"
// "Driving a car 🚗"
clientCode(new MotorcycleFactory());
// "Delivering a Motorcycle"
// "Riding a motorcycle 🏍️"
Simple Factory (Factory Pattern)
A simpler variant: a single factory method decides which class to instantiate:
class VehicleSimpleFactory {{ '{' }}
public static createVehicle(type: string): Vehicle {{ '{' }}
switch (type) {{ '{' }}
case 'car':
return new Car();
case 'motorcycle':
return new Motorcycle();
case 'truck':
return new Truck();
default:
throw new Error(`Unknown vehicle type: ${{ '{' }}type{{ '}' }}`);
{{ '}' }}
{{ '}' }}
{{ '}' }}
// Usage
const car = VehicleSimpleFactory.createVehicle('car');
car.drive(); // "Driving a car 🚗"
const truck = VehicleSimpleFactory.createVehicle('truck');
truck.drive(); // "Driving a truck 🚚"
Abstract Factory Pattern
Abstract Factory creates families of related objects without specifying concrete classes. Useful for UI themes, multi-vendor databases, etc.
// Product interfaces
interface Button {{ '{' }}
render(): void;
{{ '}' }}
interface Checkbox {{ '{' }}
render(): void;
{{ '}' }}
// Concrete products - Dark Theme
class DarkButton implements Button {{ '{' }}
render(): void {{ '{' }}
console.log("Rendering dark button");
{{ '}' }}
{{ '}' }}
class DarkCheckbox implements Checkbox {{ '{' }}
render(): void {{ '{' }}
console.log("Rendering dark checkbox");
{{ '}' }}
{{ '}' }}
// Concrete products - Light Theme
class LightButton implements Button {{ '{' }}
render(): void {{ '{' }}
console.log("Rendering light button");
{{ '}' }}
{{ '}' }}
class LightCheckbox implements Checkbox {{ '{' }}
render(): void {{ '{' }}
console.log("Rendering light checkbox");
{{ '}' }}
{{ '}' }}
// Abstract Factory
interface UIFactory {{ '{' }}
createButton(): Button;
createCheckbox(): Checkbox;
{{ '}' }}
// Concrete factories
class DarkThemeFactory implements UIFactory {{ '{' }}
createButton(): Button {{ '{' }}
return new DarkButton();
{{ '}' }}
createCheckbox(): Checkbox {{ '{' }}
return new DarkCheckbox();
{{ '}' }}
{{ '}' }}
class LightThemeFactory implements UIFactory {{ '{' }}
createButton(): Button {{ '{' }}
return new LightButton();
{{ '}' }}
createCheckbox(): Checkbox {{ '{' }}
return new LightCheckbox();
{{ '}' }}
{{ '}' }}
// Client code
class Application {{ '{' }}
private button: Button;
private checkbox: Checkbox;
constructor(factory: UIFactory) {{ '{' }}
this.button = factory.createButton();
this.checkbox = factory.createCheckbox();
{{ '}' }}
render(): void {{ '{' }}
this.button.render();
this.checkbox.render();
{{ '}' }}
{{ '}' }}
// Usage - Switch theme without modifying Application
const darkApp = new Application(new DarkThemeFactory());
darkApp.render();
// "Rendering dark button"
// "Rendering dark checkbox"
const lightApp = new Application(new LightThemeFactory());
lightApp.render();
// "Rendering light button"
// "Rendering light checkbox"
When to Use Each Pattern
✅ Use Singleton when:
- You need a single global instance (logger, cache, config)
- Control over shared resource (DB connection)
- Better: Use Dependency Injection instead if possible
✅ Use Factory Method when:
- The class doesn't know in advance which object to create
- You want to delegate creation to subclasses
- You need extensibility (new types without modifying existing code)
✅ Use Abstract Factory when:
- You need to create families of related objects
- Objects must be used together (UI theme, cross-platform UI)
- You want to hide implementation details
Practical Example: Notification System
// Singleton Logger
class NotificationLogger {{ '{' }}
private static instance: NotificationLogger;
private constructor() {{ '{' }}{{ '}' }}
static getInstance(): NotificationLogger {{ '{' }}
if (!this.instance) this.instance = new NotificationLogger();
return this.instance;
{{ '}' }}
log(msg: string): void {{ '{' }}
console.log(`[NOTIFICATION] ${{ '{' }}msg{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// Factory for Notifications
interface Notification {{ '{' }}
send(message: string): void;
{{ '}' }}
class EmailNotification implements Notification {{ '{' }}
send(message: string): void {{ '{' }}
NotificationLogger.getInstance().log(`Email sent: ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
{{ '}' }}
class SMSNotification implements Notification {{ '{' }}
send(message: string): void {{ '{' }}
NotificationLogger.getInstance().log(`SMS sent: ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
{{ '}' }}
class NotificationFactory {{ '{' }}
static create(type: 'email' | 'sms'): Notification {{ '{' }}
switch (type) {{ '{' }}
case 'email':
return new EmailNotification();
case 'sms':
return new SMSNotification();
{{ '}' }}
{{ '}' }}
{{ '}' }}
// Usage
const emailNotif = NotificationFactory.create('email');
emailNotif.send("Welcome!"); // [NOTIFICATION] Email sent: Welcome!
const smsNotif = NotificationFactory.create('sms');
smsNotif.send("Code: 1234"); // [NOTIFICATION] SMS sent: Code: 1234
Conclusion
Singleton and Factory are fundamental creational patterns. Singleton ensures a unique global instance, while Factory delegates creation for flexibility. Abstract Factory extends the concept to families of objects. Use them wisely: Dependency Injection is often preferable to Singleton, and Simple Factory may suffice instead of complex Factory Method.
Related deep dive: 04 - Polimorfismo: Flessibilità attraverso le Interfacce.
Related deep dive: 05 - Principi SOLID: Le Basi dell\.
🎯 Key Points
- Singleton = single global instance (private constructor)
- Factory Method = delegate creation to subclasses
- Simple Factory = static method decides what to create
- Abstract Factory = families of related objects
- Prefer Dependency Injection to Singleton when possible
Frequently asked questions
What is the main purpose of the Singleton pattern in software architecture?
The main purpose of the Singleton pattern is to ensure that a class has only one instance and provides a global access point to it.
Is the Factory Method pattern more complex than the Simple Factory pattern?
Yes, the Factory Method pattern is generally more complex than the Simple Factory pattern because it defines an interface for creating objects but delegates the decision on which concrete class to instantiate to subclasses.
What are some common use cases for the Abstract Factory pattern?
The Abstract Factory pattern is useful for creating families of related objects without specifying concrete classes, such as UI themes or multi-vendor databases.
Is Dependency Injection always preferable to using the Singleton pattern in software architecture?
No, while Dependency Injection is often a good alternative to the Singleton pattern, there may be cases where the Singleton pattern is still necessary or desirable.
Related deep dive: ArchUnit e Clean Architecture: il Diagramma a 4 Layer è una Bugia Finché Non lo Forzi.
Commenti
Caricamento commenti...
Sign in to comment