Skip to content

Latest commit

 

History

History
478 lines (366 loc) · 11 KB

File metadata and controls

478 lines (366 loc) · 11 KB

Factory Pattern 🏭

Overview

The Factory Pattern provides an interface for creating objects, but lets subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.

Difficulty: ⭐ Easy
Category: Creational Pattern
Interview Frequency: ⭐⭐⭐⭐⭐ Extremely Common

Problem It Solves

Imagine you need to create different types of objects, but:

  1. Object creation logic is complex
  2. You don't know the exact type until runtime
  3. You want to centralize object creation
  4. You need to decouple object creation from usage

Real-World Analogy

Think of a car factory:

  • Input: "I want an SUV"
  • Factory: Handles all the complexity of building an SUV
  • Output: A ready-to-use SUV

You don't need to know how to assemble a car; you just tell the factory what you want.

When to Use

Use Factory Pattern when:

  • Object creation is complex
  • Type depends on runtime conditions
  • You want to hide creation logic from clients
  • You need centralized object creation
  • Supporting new types shouldn't require changing client code

Don't use when:

  • Object creation is simple (just new MyClass())
  • Only one type of object
  • No conditional logic for creation

Types of Factory Patterns

1. Simple Factory (Not a real pattern, but useful)

class SimpleFactory {
  static create(type: string): Product {
    if (type === "A") return new ProductA();
    if (type === "B") return new ProductB();
    throw new Error("Unknown type");
  }
}

2. Factory Method Pattern

abstract class Creator {
  abstract factoryMethod(): Product;

  operation(): void {
    const product = this.factoryMethod();
    product.use();
  }
}

3. Abstract Factory Pattern

Creates families of related objects (covered separately).

Implementation

See factory.ts for complete implementations.

TypeScript Example

// 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 Truck implements Vehicle {
  drive(): void {
    console.log("Driving a truck");
  }
  getType(): string {
    return "Truck";
  }
}

// Factory
class VehicleFactory {
  static createVehicle(type: "car" | "truck"): Vehicle {
    switch (type) {
      case "car":
        return new Car();
      case "truck":
        return new Truck();
      default:
        throw new Error("Unknown vehicle type");
    }
  }
}

// Usage
const vehicle = VehicleFactory.createVehicle("car");
vehicle.drive(); // "Driving a car"

Benefits

Single Responsibility: Object creation logic in one place
Open/Closed Principle: Easy to add new types
Loose Coupling: Clients don't depend on concrete classes
Testability: Easy to mock factories
Consistency: Centralized creation ensures objects are created correctly

Drawbacks

❌ Code can become more complex with many subclasses
❌ Overkill for simple object creation
❌ Requires maintenance when adding new types

Interview Questions & Answers

Q1: What's the difference between Factory Method and Abstract Factory?

Answer:

  • Factory Method: Creates ONE type of product. Uses inheritance.
    abstract class Creator {
      abstract createProduct(): Product;
    }
  • Abstract Factory: Creates FAMILIES of related products. Uses composition.
    interface AbstractFactory {
      createProductA(): ProductA;
      createProductB(): ProductB;
    }

Example:

  • Factory Method: DocumentFactory creates documents (PDF, Word, Excel)
  • Abstract Factory: UIFactory creates related UI components (WindowsButton + WindowsCheckbox) or (MacButton + MacCheckbox)

Q2: When would you use a Factory over just using constructors?

Answer: Use Factory when:

  1. Complex initialization:

    // Without factory - messy
    const user = new User();
    user.setName("John");
    user.setEmail("john@example.com");
    user.validate();
    user.hashPassword();
    
    // With factory - clean
    const user = UserFactory.create({ name: "John", email: "john@example.com" });
  2. Type depends on data:

    const shape = ShapeFactory.create(shapeType); // Don't know type until runtime
  3. Need different implementations (testing, production):

    const logger = LoggerFactory.create(isProduction ? "file" : "console");

Q3: How do you add a new product type without modifying existing code?

Answer: Use polymorphism and register new types:

class ShapeFactory {
  private static registry = new Map<string, () => Shape>();

  static register(type: string, creator: () => Shape): void {
    this.registry.set(type, creator);
  }

  static create(type: string): Shape {
    const creator = this.registry.get(type);
    if (!creator) throw new Error(`Unknown type: ${type}`);
    return creator();
  }
}

// Register built-in shapes
ShapeFactory.register("circle", () => new Circle());
ShapeFactory.register("square", () => new Square());

// Later, add new shape without modifying factory
ShapeFactory.register("triangle", () => new Triangle());

Q4: What are common mistakes when implementing Factory pattern?

Answer:

  1. God Factory - Factory that creates too many unrelated types

    // BAD - Factory does too much
    class ObjectFactory {
      create(type: string) {
        if (type === 'user') return new User();
        if (type === 'product') return new Product();
        if (type === 'order') return new Order();
        // 100 more types...
      }
    }
    
    // GOOD - Separate factories
    class UserFactory { ... }
    class ProductFactory { ... }
    class OrderFactory { ... }
  2. Exposing concrete classes - Defeats the purpose

    // BAD - Client knows concrete classes
    const shape = ShapeFactory.createCircle();
    
    // GOOD - Client only knows interface
    const shape = ShapeFactory.create("circle");
  3. Not using polymorphism - Makes adding types hard

    // BAD - Hard-coded types
    create(type: string): Shape {
      if (type === 'circle') return new Circle();
      if (type === 'square') return new Square();
      // Need to modify for new types
    }
    
    // GOOD - Registry pattern
    private registry = new Map<string, ShapeConstructor>();

Q5: How would you test code using factories?

Answer: Factories make testing easier:

// Production
class UserFactory {
  static create(data: UserData): User {
    return new User(data, new RealDatabase());
  }
}

// Testing
class TestUserFactory {
  static create(data: UserData): User {
    return new User(data, new MockDatabase());
  }
}

// Or use dependency injection
class UserFactory {
  constructor(private db: Database) {}

  create(data: UserData): User {
    return new User(data, this.db);
  }
}

// Test
const factory = new UserFactory(mockDatabase);
const user = factory.create(testData);

Q6: Can you implement Factory pattern with TypeScript types?

Answer: Yes, using generics and type guards:

interface Product {
  use(): void;
}

class ConcreteProductA implements Product {
  use(): void {
    console.log("Using Product A");
  }
}

class ConcreteProductB implements Product {
  use(): void {
    console.log("Using Product B");
  }
}

// Generic factory
class Factory<T extends Product> {
  constructor(private ctor: new () => T) {}

  create(): T {
    return new this.ctor();
  }
}

// Usage
const factoryA = new Factory(ConcreteProductA);
const productA = factoryA.create(); // Type: ConcreteProductA

Real-World Use Cases

  1. Document Generation: PDF, Word, Excel documents
  2. Database Connections: MySQL, PostgreSQL, MongoDB
  3. UI Components: Cross-platform UI (Windows, Mac, Linux)
  4. Logging Systems: Console, File, Cloud logging
  5. Payment Gateways: Stripe, PayPal, Square
  6. Notification Services: Email, SMS, Push notifications
  7. Data Parsers: JSON, XML, CSV parsers
  8. Authentication: OAuth, JWT, SAML

Code Smell Factory Pattern Fixes

Before:

function processFile(type: string, data: any) {
  let parser;

  if (type === "json") {
    parser = new JSONParser();
    parser.setValidation(true);
    parser.setStrict(false);
  } else if (type === "xml") {
    parser = new XMLParser();
    parser.setValidation(true);
    parser.setNamespaceAware(true);
  } else if (type === "csv") {
    parser = new CSVParser();
    parser.setDelimiter(",");
  }

  return parser.parse(data);
}

After:

class ParserFactory {
  static create(type: string): Parser {
    switch (type) {
      case "json":
        return new JSONParser({ validation: true, strict: false });
      case "xml":
        return new XMLParser({ validation: true, namespaceAware: true });
      case "csv":
        return new CSVParser({ delimiter: "," });
      default:
        throw new Error(`Unsupported parser: ${type}`);
    }
  }
}

function processFile(type: string, data: any) {
  const parser = ParserFactory.create(type);
  return parser.parse(data);
}

Variations

1. Parameterized Factory

class Factory {
  create(type: string, params: any): Product {
    const product = this.createByType(type);
    product.configure(params);
    return product;
  }
}

2. Factory with Dependency Injection

class Factory {
  constructor(private logger: Logger, private config: Config) {}

  create(): Product {
    return new Product(this.logger, this.config);
  }
}

3. Lazy Factory

class LazyFactory {
  private instance?: Product;

  create(): Product {
    if (!this.instance) {
      this.instance = new Product();
    }
    return this.instance;
  }
}

Related Patterns

  • Abstract Factory: Creates families of objects
  • Builder: Constructs complex objects step-by-step
  • Prototype: Creates objects by cloning
  • Singleton: Often used with Factory to ensure one instance

Testing Tips

describe("Factory", () => {
  test("should create correct type", () => {
    const product = Factory.create("typeA");
    expect(product).toBeInstanceOf(ProductA);
  });

  test("should throw for unknown type", () => {
    expect(() => Factory.create("unknown")).toThrow();
  });

  test("should create independent instances", () => {
    const p1 = Factory.create("typeA");
    const p2 = Factory.create("typeA");
    expect(p1).not.toBe(p2);
  });
});

Summary

The Factory Pattern is one of the most important and frequently asked patterns in interviews. Master it by:

  1. Understanding when object creation needs abstraction
  2. Practicing with real-world examples
  3. Knowing the difference from similar patterns
  4. Being able to implement quickly

Key Takeaway: "Don't call constructors directly if creation logic is complex or might change."

Next: Try the Observer Pattern