正在加载,请稍候…

Design Patterns in TypeScript: Factory, Observer, Strategy, and More

Implement classic Gang of Four design patterns in TypeScript. Learn when and how to apply Factory, Observer, Strategy, Decorator, and Repository patterns.

Design Patterns in TypeScript: Practical Guide

Factory Pattern

Create objects without specifying exact classes.

interface Logger {
  log(message: string): void;
  error(message: string): void;
}

class ConsoleLogger implements Logger {
  log(msg: string) { console.log(`[LOG] ${msg}`); }
  error(msg: string) { console.error(`[ERR] ${msg}`); }
}

class FileLogger implements Logger {
  constructor(private path: string) {}
  log(msg: string) { /* write to file */ }
  error(msg: string) { /* write to file */ }
}

class LoggerFactory {
  static create(type: 'console' | 'file', options?: { path?: string }): Logger {
    switch (type) {
      case 'console': return new ConsoleLogger();
      case 'file': return new FileLogger(options?.path ?? 'app.log');
      default: throw new Error(`Unknown logger type: ${type}`);
    }
  }
}

const logger = LoggerFactory.create('console');

Observer Pattern

Define a subscription mechanism to notify objects of events.

type EventMap = Record<string, unknown[]>;

class EventEmitter<T extends EventMap> {
  private listeners = new Map<keyof T, Set<Function>>();

  on<K extends keyof T>(event: K, listener: (...args: T[K]) => void): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(listener);
  }

  off<K extends keyof T>(event: K, listener: Function): void {
    this.listeners.get(event)?.delete(listener);
  }

  emit<K extends keyof T>(event: K, ...args: T[K]): void {
    this.listeners.get(event)?.forEach(l => l(...args));
  }
}

// Usage
type OrderEvents = {
  created: [order: Order];
  shipped: [orderId: string, trackingCode: string];
  delivered: [orderId: string];
};

class OrderService extends EventEmitter<OrderEvents> {
  async createOrder(data: CreateOrderDto): Promise<Order> {
    const order = await this.db.create(data);
    this.emit('created', order); // notify listeners
    return order;
  }
}

orderService.on('created', (order) => emailService.sendConfirmation(order));
orderService.on('shipped', (id, tracking) => notificationService.notify(id, tracking));

Strategy Pattern

Define a family of algorithms and make them interchangeable.

interface SortStrategy<T> {
  sort(data: T[]): T[];
}

class QuickSort<T> implements SortStrategy<T> {
  sort(data: T[]): T[] {
    if (data.length <= 1) return data;
    const pivot = data[Math.floor(data.length / 2)];
    const left = data.filter(x => x < pivot);
    const mid = data.filter(x => x === pivot);
    const right = data.filter(x => x > pivot);
    return [...this.sort(left), ...mid, ...this.sort(right)];
  }
}

class BubbleSort<T> implements SortStrategy<T> {
  sort(data: T[]): T[] {
    const arr = [...data];
    for (let i = 0; i < arr.length; i++) {
      for (let j = 0; j < arr.length - i - 1; j++) {
        if (arr[j] > arr[j + 1]) [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
    return arr;
  }
}

class Sorter<T> {
  constructor(private strategy: SortStrategy<T>) {}
  setStrategy(strategy: SortStrategy<T>) { this.strategy = strategy; }
  sort(data: T[]): T[] { return this.strategy.sort(data); }
}

Decorator Pattern

Add behavior to objects dynamically.

interface TextProcessor {
  process(text: string): string;
}

class PlainText implements TextProcessor {
  process(text: string): string { return text; }
}

abstract class TextDecorator implements TextProcessor {
  constructor(protected wrapped: TextProcessor) {}
  process(text: string): string { return this.wrapped.process(text); }
}

class UpperCaseDecorator extends TextDecorator {
  process(text: string): string { return super.process(text).toUpperCase(); }
}

class TrimDecorator extends TextDecorator {
  process(text: string): string { return super.process(text).trim(); }
}

// Compose decorators
const processor = new UpperCaseDecorator(new TrimDecorator(new PlainText()));
processor.process('  hello world  '); // 'HELLO WORLD'

Repository Pattern

Abstract data access behind an interface.

interface UserRepository {
  findById(id: string): Promise<User | null>;
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<User>;
  delete(id: string): Promise<void>;
  findAll(filter: UserFilter): Promise<User[]>;
}

class PostgresUserRepository implements UserRepository {
  constructor(private db: Database) {}

  async findById(id: string): Promise<User | null> {
    const rows = await this.db.query('SELECT * FROM users WHERE id = $1', [id]);
    return rows[0] ? this.mapToUser(rows[0]) : null;
  }

  async save(user: User): Promise<User> {
    const result = await this.db.query(
      'INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET ...',
      [user.id, user.email, user.name]
    );
    return this.mapToUser(result[0]);
  }

  private mapToUser(row: Record<string, unknown>): User {
    return { id: row.id as string, email: row.email as string, name: row.name as string };
  }
}

// In-memory implementation for tests
class InMemoryUserRepository implements UserRepository {
  private users = new Map<string, User>();
  async findById(id: string) { return this.users.get(id) ?? null; }
  async save(user: User) { this.users.set(user.id, user); return user; }
  async delete(id: string) { this.users.delete(id); }
  async findByEmail(email: string) {
    return [...this.users.values()].find(u => u.email === email) ?? null;
  }
  async findAll() { return [...this.users.values()]; }
}

Patterns are solutions to recurring problems, not blueprints to force on every problem.