Skip to contentJMRG
All posts

Factory Method Pattern: Delegating Object Creation

design-patternscreationalfactory-method

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 ..> Product

When 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).