Category: reference
JavaScript Design Patterns
Design patterns ที่ใช้บ่อยใน JavaScript/TypeScript: Singleton, Observer, Factory, Strategy, Command, และ Module pattern
สารบัญ
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 ระหว่าง components | Observer |
| สร้าง object แบบยืดหยุ่น (hide complexity) | Factory |
| เปลี่ยน algorithm ณ runtime | Strategy |
| ต้องการ undo/redo | Command |
| เพิ่ม behavior โดยไม่แก้ original | Decorator |