Design Principle
Factory Pattern
Learn the Factory pattern: create objects without specifying the exact class. Understand Factory Method and Abstract Factory patterns with examples.
The Factory Pattern provides an interface for creating objects without specifying their exact classes. It encapsulates object creation logic and decouples object creation from usage.
What is the Factory Pattern?
Factory Pattern provides:
- Encapsulation: Encapsulates object creation logic
- Decoupling: Decouples object creation from usage
- Flexibility: Easy to add new types
- Centralization: Centralizes object creation logic
Types:
- Factory Method: Subclasses decide which class to instantiate
- Abstract Factory: Provides interface for creating families of related objects
Factory Method Pattern
Factory Method lets subclasses decide which class to instantiate.
Structure
Creator (abstract)
├─ factoryMethod() (abstract)
└─ createProduct() (uses factoryMethod)
ConcreteCreator
└─ factoryMethod() (returns ConcreteProduct)
Product (interface)
ConcreteProduct (implements Product)
Examples
Factory Method Example
// Product interface
interface Transport {
deliver(): void;
}
// Concrete products
class Truck implements Transport {
deliver(): void {
console.log("Delivering by truck");
}
}
class Ship implements Transport {
deliver(): void {
console.log("Delivering by ship");
}
}
// Creator (abstract)
abstract class Logistics {
abstract createTransport(): Transport;
planDelivery(): void {
const transport = this.createTransport();
transport.deliver();
}
}
// Concrete creators
class RoadLogistics extends Logistics {
createTransport(): Transport {
return new Truck();
}
}
class SeaLogistics extends Logistics {
createTransport(): Transport {
return new Ship();
}
}
// Usage
const roadLogistics = new RoadLogistics();
roadLogistics.planDelivery(); // Delivering by truck
const seaLogistics = new SeaLogistics();
seaLogistics.planDelivery(); // Delivering by ship
Simple Factory Example
// Product interface
interface PaymentMethod {
pay(amount: number): void;
}
// Concrete products
class CreditCard implements PaymentMethod {
pay(amount: number): void {
console.log(`Paying ${amount} with credit card`);
}
}
class PayPal implements PaymentMethod {
pay(amount: number): void {
console.log(`Paying ${amount} with PayPal`);
}
}
class BankTransfer implements PaymentMethod {
pay(amount: number): void {
console.log(`Paying ${amount} with bank transfer`);
}
}
// Factory
class PaymentFactory {
static createPaymentMethod(type: string): PaymentMethod {
switch (type) {
case "creditcard":
return new CreditCard();
case "paypal":
return new PayPal();
case "banktransfer":
return new BankTransfer();
default:
throw new Error(`Unknown payment method: ${type}`);
}
}
}
// Usage
const payment = PaymentFactory.createPaymentMethod("creditcard");
payment.pay(100);
Abstract Factory Pattern
// Abstract products
interface Button {
render(): void;
}
interface Checkbox {
render(): void;
}
// Concrete products - Windows
class WindowsButton implements Button {
render(): void {
console.log("Rendering Windows button");
}
}
class WindowsCheckbox implements Checkbox {
render(): void {
console.log("Rendering Windows checkbox");
}
}
// Concrete products - Mac
class MacButton implements Button {
render(): void {
console.log("Rendering Mac button");
}
}
class MacCheckbox implements Checkbox {
render(): void {
console.log("Rendering Mac checkbox");
}
}
// Abstract factory
interface UIFactory {
createButton(): Button;
createCheckbox(): Checkbox;
}
// Concrete factories
class WindowsFactory implements UIFactory {
createButton(): Button {
return new WindowsButton();
}
createCheckbox(): Checkbox {
return new WindowsCheckbox();
}
}
class MacFactory implements UIFactory {
createButton(): Button {
return new MacButton();
}
createCheckbox(): Checkbox {
return new MacCheckbox();
}
}
// Usage
function createUI(factory: UIFactory): void {
const button = factory.createButton();
const checkbox = factory.createCheckbox();
button.render();
checkbox.render();
}
const windowsUI = createUI(new WindowsFactory());
const macUI = createUI(new MacFactory());
Common Pitfalls
- Over-engineering: Using factory for simple cases. Fix: Use factory only when needed
- Tight coupling: Factory still coupled to concrete classes. Fix: Use interfaces, dependency injection
- Complexity: Abstract Factory can be complex. Fix: Use only when creating families of objects
- Not extensible: Hard to add new types. Fix: Use open-closed principle, registry pattern
Interview Questions
Beginner
Q: What is the Factory pattern and why is it useful?
A:
Factory Pattern provides an interface for creating objects without specifying their exact classes.
Benefits:
- Encapsulation: Encapsulates object creation logic
- Decoupling: Decouples object creation from usage
- Flexibility: Easy to add new types
- Centralization: Centralizes creation logic
Example:
class PaymentFactory {
static create(type: string): PaymentMethod {
switch (type) {
case "creditcard":
return new CreditCard();
case "paypal":
return new PayPal();
default:
throw new Error("Unknown type");
}
}
}
Use cases:
- Object creation complexity: Complex object creation
- Multiple types: Creating different types of objects
- Decoupling: Decouple creation from usage
- Configuration: Creation based on configuration
Intermediate
Q: Explain the difference between Factory Method and Abstract Factory patterns. When would you use each?
A:
Factory Method Pattern:
Lets subclasses decide which class to instantiate:
abstract class Creator {
abstract factoryMethod(): Product;
create(): Product {
return this.factoryMethod();
}
}
class ConcreteCreator extends Creator {
factoryMethod(): Product {
return new ConcreteProduct();
}
}
Use when:
- Single product family: Creating one type of product
- Subclass responsibility: Subclasses decide what to create
- Flexibility: Need flexibility in product creation
Abstract Factory Pattern:
Provides interface for creating families of related objects:
interface Factory {
createButton(): Button;
createCheckbox(): Checkbox;
}
class WindowsFactory implements Factory {
createButton(): Button { return new WindowsButton(); }
createCheckbox(): Checkbox { return new WindowsCheckbox(); }
}
Use when:
- Multiple product families: Creating families of related objects
- Consistency: Need consistent products from same family
- Platform-specific: Different implementations for different platforms
Key Differences:
| Feature | Factory Method | Abstract Factory |
|---|---|---|
| Products | Single product | Family of products |
| Complexity | Simpler | More complex |
| Use case | One product type | Multiple related products |
Senior
Q: Design a factory system for a plugin architecture where plugins can be loaded dynamically. How do you handle plugin registration, creation, and ensure type safety?
A:
// Plugin interface
interface Plugin {
name: string;
execute(): void;
}
// Plugin registry
class PluginRegistry {
private static plugins: Map<string, new () => Plugin> = new Map();
static register(name: string, pluginClass: new () => Plugin): void {
this.plugins.set(name, pluginClass);
}
static create(name: string): Plugin {
const PluginClass = this.plugins.get(name);
if (!PluginClass) {
throw new Error(`Plugin ${name} not found`);
}
return new PluginClass();
}
static getAvailablePlugins(): string[] {
return Array.from(this.plugins.keys());
}
}
// Plugin factory
class PluginFactory {
createPlugin(name: string): Plugin {
return PluginRegistry.create(name);
}
createPlugins(names: string[]): Plugin[] {
return names.map(name => this.createPlugin(name));
}
}
// Dynamic plugin loader
class DynamicPluginLoader {
async loadPlugin(path: string): Promise<void> {
const module = await import(path);
const PluginClass = module.default;
if (this.isValidPlugin(PluginClass)) {
PluginRegistry.register(PluginClass.name, PluginClass);
} else {
throw new Error("Invalid plugin");
}
}
private isValidPlugin(PluginClass: any): boolean {
return typeof PluginClass === 'function' &&
PluginClass.prototype &&
typeof PluginClass.prototype.execute === 'function';
}
}
// Usage
const loader = new DynamicPluginLoader();
await loader.loadPlugin('./plugins/MyPlugin.js');
const factory = new PluginFactory();
const plugin = factory.createPlugin('MyPlugin');
plugin.execute();
Features:
- Plugin registry: Register and store plugins
- Dynamic loading: Load plugins at runtime
- Type safety: Validate plugin structure
- Factory: Create plugins by name
Key Takeaways
- Factory pattern: Encapsulates object creation logic
- Factory Method: Subclasses decide which class to instantiate
- Abstract Factory: Creates families of related objects
- Benefits: Decoupling, flexibility, centralization
- Use cases: Complex creation, multiple types, plugin systems
- Best practices: Use interfaces, make extensible, avoid over-engineering
Keep exploring
Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.