ข้ามไปเนื้อหาหลัก

Category: reference

JavaScript Design Patterns

Design patterns ที่ใช้บ่อยใน JavaScript/TypeScript: Singleton, Observer, Factory, Strategy, Command, และ Module pattern

· อ่านประมาณ 4 นาที

สารบัญ

Singleton — Instance เดียวเท่านั้น

// ✓ Module-level singleton (ESM ทำให้ง่าย)
// config.ts — import ทุกครั้งจะได้ instance เดิม
export const config = {
  apiUrl: import.meta.env.PUBLIC_API_URL,
  debug: import.meta.env.DEV,
};

// ✓ Class-based singleton
class Logger {
  private static instance: Logger;
  private logs: string[] = [];

  private constructor() {}  // ป้องกัน new Logger()

  static getInstance(): Logger {
    Logger.instance ??= new Logger();
    return Logger.instance;
  }

  log(msg: string) {
    this.logs.push(`[${new Date().toISOString()}] ${msg}`);
    console.log(msg);
  }

  getLogs() { return [...this.logs]; }
}

const logger = Logger.getInstance();
logger.log('App started');
// Logger.getInstance() === Logger.getInstance() → true (same object)

Observer — Subscribe/Publish Events

type Listener<T> = (data: T) => void;

class EventEmitter<Events extends Record<string, unknown>> {
  private listeners = new Map<keyof Events, Set<Listener<unknown>>>();

  on<K extends keyof Events>(event: K, listener: Listener<Events[K]>): () => void {
    if (!this.listeners.has(event)) this.listeners.set(event, new Set());
    this.listeners.get(event)!.add(listener as Listener<unknown>);
    return () => this.off(event, listener);  // unsubscribe function
  }

  off<K extends keyof Events>(event: K, listener: Listener<Events[K]>): void {
    this.listeners.get(event)?.delete(listener as Listener<unknown>);
  }

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

// ใช้งาน
interface AppEvents {
  'user:login': { userId: string; email: string };
  'cart:update': { itemCount: number };
  'error': Error;
}

const events = new EventEmitter<AppEvents>();

const unsub = events.on('user:login', ({ userId, email }) => {
  console.log(`${email} logged in`);
});

events.emit('user:login', { userId: '1', email: 'alice@example.com' });
unsub();  // unsubscribe

Factory — สร้าง Object โดยไม่ระบุ Class

interface Notification {
  send(message: string): Promise<void>;
}

class EmailNotification implements Notification {
  constructor(private to: string) {}
  async send(message: string) {
    console.log(`Email to ${this.to}: ${message}`);
  }
}

class SlackNotification implements Notification {
  constructor(private channel: string) {}
  async send(message: string) {
    console.log(`Slack #${this.channel}: ${message}`);
  }
}

class SMSNotification implements Notification {
  constructor(private phone: string) {}
  async send(message: string) {
    console.log(`SMS to ${this.phone}: ${message}`);
  }
}

// Factory function
function createNotification(
  type: 'email' | 'slack' | 'sms',
  target: string,
): Notification {
  switch (type) {
    case 'email': return new EmailNotification(target);
    case 'slack': return new SlackNotification(target);
    case 'sms':   return new SMSNotification(target);
  }
}

const notifier = createNotification('email', 'alice@example.com');
await notifier.send('Deployment complete!');

Strategy — เปลี่ยน Algorithm ณ Runtime

// strategy = function ที่ swap ได้
type SortStrategy<T> = (items: T[]) => T[];

function createSorter<T>(strategy: SortStrategy<T>) {
  return {
    sort: (items: T[]) => strategy([...items]),
    setStrategy: (newStrategy: SortStrategy<T>) => {
      strategy = newStrategy;
    },
  };
}

const sorter = createSorter<number>((arr) => arr.sort((a, b) => a - b));
sorter.sort([3, 1, 4, 1, 5]);  // ascending

// เปลี่ยน strategy ตาม user เลือก
sorter.setStrategy((arr) => arr.sort((a, b) => b - a));
sorter.sort([3, 1, 4, 1, 5]);  // descending

// ตัวอย่าง: validation strategies
type Validator = (value: string) => string | null;

const validators: Record<string, Validator> = {
  required:  (v) => v.trim() ? null : 'Required',
  email:     (v) => /\S+@\S+\.\S+/.test(v) ? null : 'Invalid email',
  minLength: (v) => v.length >= 6 ? null : 'Too short',
};

function validate(value: string, rules: (keyof typeof validators)[]): string[] {
  return rules.flatMap((rule) => validators[rule]?.(value) ?? []).filter(Boolean);
}

validate('', ['required', 'email']);  // ['Required', 'Invalid email']

Command — Encapsulate Action (Undo/Redo)

interface Command {
  execute(): void;
  undo(): void;
}

class TextEditor {
  private text = '';
  private history: Command[] = [];
  private undoStack: Command[] = [];

  execute(command: Command) {
    command.execute();
    this.history.push(command);
    this.undoStack = [];  // clear redo stack เมื่อ execute ใหม่
  }

  undo() {
    const command = this.history.pop();
    if (command) {
      command.undo();
      this.undoStack.push(command);
    }
  }

  redo() {
    const command = this.undoStack.pop();
    if (command) {
      command.execute();
      this.history.push(command);
    }
  }

  getText() { return this.text; }
  setText(text: string) { this.text = text; }
}

// Concrete commands
class InsertCommand implements Command {
  constructor(
    private editor: TextEditor,
    private text: string,
    private position: number,
  ) {}

  execute() {
    const current = this.editor.getText();
    this.editor.setText(
      current.slice(0, this.position) + this.text + current.slice(this.position)
    );
  }

  undo() {
    const current = this.editor.getText();
    this.editor.setText(
      current.slice(0, this.position) + current.slice(this.position + this.text.length)
    );
  }
}

const editor = new TextEditor();
editor.execute(new InsertCommand(editor, 'Hello', 0));
editor.execute(new InsertCommand(editor, ' World', 5));
editor.getText();  // 'Hello World'
editor.undo();
editor.getText();  // 'Hello'
editor.redo();
editor.getText();  // 'Hello World'

Decorator — เพิ่ม Behavior โดยไม่แก้ Original

// Function decorator
function memoize<T extends (...args: unknown[]) => unknown>(fn: T): T {
  const cache = new Map<string, unknown>();
  return ((...args: unknown[]) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  }) as T;
}

const expensiveCalc = memoize((n: number) => {
  console.log(`Computing ${n}...`);
  return n * n;
});

expensiveCalc(5);  // Computing 5... → 25
expensiveCalc(5);  // (from cache) → 25
expensiveCalc(10); // Computing 10... → 100

// Rate limiter decorator
function rateLimit<T extends (...args: unknown[]) => void>(fn: T, ms: number): T {
  let lastCall = 0;
  return ((...args: unknown[]) => {
    const now = Date.now();
    if (now - lastCall >= ms) {
      lastCall = now;
      fn(...args);
    }
  }) as T;
}

const onResize = rateLimit(() => console.log('resized'), 100);
window.addEventListener('resize', onResize);

เลือก Pattern อย่างไร

ปัญหาPattern
ต้องการ instance เดียว (config, logger)Singleton
ส่ง event ระหว่าง componentsObserver
สร้าง object แบบยืดหยุ่น (hide complexity)Factory
เปลี่ยน algorithm ณ runtimeStrategy
ต้องการ undo/redoCommand
เพิ่ม behavior โดยไม่แก้ originalDecorator