Today I discussed design patterns, specifically factory design patterns. There are three main types of factory patterns:

  1. Simple Factory
  2. Factory Method
  3. Abstract Factory

Simple Factory

Simple Factory is used when we have an input parameter and need to create a specific class or instance based on that parameters.

It's straightforward and commonly used in everyday programming.

Here's a simple example:

// Product interface
interface Animal {
    makeSound(): void;
}

// Concrete products
class Dog implements Animal {
    makeSound() {
        console.log("Woof!");
    }
}

class Cat implements Animal {
    makeSound() {
        console.log("Meow!");
    }
}

// Simple factory
class AnimalFactory {
    createAnimal(type: string): Animal {
        switch(type.toLowerCase()) {
            case "dog":
                return new Dog();
            case "cat":
                return new Cat();
            default:
                throw new Error("Unknown animal type");
        }
    }
}

// Usage
const factory = new AnimalFactory();
const dog = factory.createAnimal("dog");
const cat = factory.createAnimal("cat");

dog.makeSound(); // Output: Woof!
cat.makeSound(); // Output: Meow!

In this example, the AnimalFactory is a simple factory that creates different types of animals based on a string parameter. It demonstrates how a simple factory can encapsulate object creation logic in a single place.

IMPROTANT:

Dynamic Creation Discussion

Sometimes, you need to inject additional logic in your factory method to determine which variant of a product to return. For instance, you might decide between a “US” version or “EU” version of the same service based on user location, configuration, or runtime parameters. This does not violate the Factory Method pattern as long as:

  1. You keep the creation logic encapsulated in the concrete factory.
  2. All returned products still implement the same interface (i.e., they belong to the same “product family”).

Consider the following dynamic example, where the same factory creates slightly different variants of a payment service depending on the region:

// Product interface
interface PaymentService {
  pay(amount: number): void;
}

// Concrete products
class PayPalUSService implements PaymentService {
  pay(amount: number): void {
    console.log(`PayPal US processing payment: $${amount}`);
  }
}

class PayPalEUService implements PaymentService {
  pay(amount: number): void {
    console.log(`PayPal EU processing payment: €${amount}`);
  }
}

// Creator interface
interface PaymentFactory {
  createPaymentService(): PaymentService;
}

// Concrete creator with dynamic logic
class PayPalFactory implements PaymentFactory {
  constructor(private region: string) {}

  createPaymentService(): PaymentService {
    // Decide which PayPal variant to create, all still PaymentService
    if (this.region.toLowerCase() === "eu") {
      return new PayPalEUService();
    }
    return new PayPalUSService();
  }
}

// Usage
const euFactory = new PayPalFactory("EU");
const euPayment = euFactory.createPaymentService();
euPayment.pay(100); // Output: PayPal EU processing payment: €100

const usFactory = new PayPalFactory("US");
const usPayment = usFactory.createPaymentService();
usPayment.pay(100); // Output: PayPal US processing payment: $100

Factory Method