Factory Method Pattern: Delegating Object Creation

What is the Factory Method Pattern?
The Factory Method pattern defines an interface for creating objects, but lets subclasses decide which class to instantiate. It's ideal when you don't know in advance the exact types of objects you need to create or when you want to delegate creation logic to specialized subclasses.
classDiagram
class Creator {
<<abstract>>
+factoryMethod(): Product
+operation()
}
class ConcreteCreatorA {
+factoryMethod(): ProductA
}
class ConcreteCreatorB {
+factoryMethod(): ProductB
}
class Product {
<<interface>>
+use()
}
class ProductA {
+use()
}
class ProductB {
+use()
}
Creator <|-- ConcreteCreatorA
Creator <|-- ConcreteCreatorB
Product <|.. ProductA
Product <|.. ProductB
Creator ..> ProductWhen to Use Factory Method
Use Factory Method:
- When you don't know the exact types of objects in advance
- To provide a way to extend internal components
- To reuse existing objects instead of creating new ones
- Integration with different providers (payments, notifications, etc.)
Avoid Factory Method:
- If you only have one product type (over-engineering)
- When creation is trivial and won't change
Step by Step Implementation
1. Product Interface
Defines the common contract that all products must implement:
interface NotificationResult {
success: boolean;
messageId: string;
timestamp: Date;
}
interface Notification {
readonly type: string;
send(recipient: string, message: string): Promise<NotificationResult>;
validate(recipient: string): boolean;
}2. Concrete Products
Specific implementations of the interface:
class EmailNotification implements Notification {
readonly type = 'email';
validate(recipient: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(recipient);
}
async send(recipient: string, message: string): Promise<NotificationResult> {
console.log(`Sending email to ${recipient}: ${message}`);
return {
success: true,
messageId: `email_${Date.now()}`,
timestamp: new Date(),
};
}
}
class SMSNotification implements Notification {
readonly type = 'sms';
validate(recipient: string): boolean {
const phoneRegex = /^\+?[1-9]\d{9,14}$/;
return phoneRegex.test(recipient);
}
async send(recipient: string, message: string): Promise<NotificationResult> {
console.log(`Sending SMS to ${recipient}: ${message}`);
return {
success: true,
messageId: `sms_${Date.now()}`,
timestamp: new Date(),
};
}
}3. Abstract Creator
Defines the abstract factory method and can include common logic:
abstract class NotificationCreator {
/** Factory Method - subclasses decide what to create */
protected abstract createNotification(): Notification;
/** Template Method that uses the factory method */
public async notify(
recipient: string,
message: string
): Promise<NotificationResult> {
const notification = this.createNotification();
if (!notification.validate(recipient)) {
throw new Error(`Invalid recipient for ${notification.type}`);
}
return notification.send(recipient, message);
}
}4. Concrete Creators
Implement the factory method to create specific products:
class EmailNotificationCreator extends NotificationCreator {
protected createNotification(): Notification {
return new EmailNotification();
}
}
class SMSNotificationCreator extends NotificationCreator {
protected createNotification(): Notification {
return new SMSNotification();
}
}Usage Example
// Using specific creators
const emailCreator = new EmailNotificationCreator();
await emailCreator.notify('[email protected]', 'Welcome!');
const smsCreator = new SMSNotificationCreator();
await smsCreator.notify('+34612345678', 'Your code is: 123456');Variant: Factory with Dynamic Registration
Allows registering new types at runtime:
type NotificationType = 'email' | 'sms' | 'push' | 'slack';
class NotificationFactory {
private static creators = new Map<NotificationType, () => Notification>();
static register(type: NotificationType, creator: () => Notification): void {
this.creators.set(type, creator);
}
static create(type: NotificationType): Notification {
const creator = this.creators.get(type);
if (!creator) {
throw new Error(`Notification type not registered: ${type}`);
}
return creator();
}
}
// Register factories
NotificationFactory.register('email', () => new EmailNotification());
NotificationFactory.register('sms', () => new SMSNotification());
// Usage
const notification = NotificationFactory.create('email');
await notification.send('[email protected]', 'Hello!');Summary
| Aspect | Description |
|---|---|
| Purpose | Delegate object creation to subclasses |
| Components | Product (interface), Concrete products, Abstract creator, Concrete creators |
| Benefits | Decoupling, extensibility, Open/Closed principle |
| Variants | Factory with dynamic registration, Parametrized Factory |
Conclusion
The Factory Method pattern is an essential tool for handling object creation in a flexible and extensible way. It's the foundation of many modern frameworks and libraries.
In the next article we'll explore the Decorator Pattern, which allows adding behavior to objects dynamically.
Based on "Design Patterns: Elements of Reusable Object-Oriented Software" (Gang of Four).