Design Patterns
Creational
Creational design patterns provide various object creation mechanisms.
Factory
A factory is a method or function that creates an object, or a set of objects, without exposing the creation logic to the client.
class MacDialog {} class WindowsDialog {} // Without Factory const dialog1 = os === 'mac' ? new MacDialog() : new WindowsDialog() const dialog2 = os === 'mac' ? new MacDialog() : new WindowsDialog() // With Factory class DialogFactory { createDialog(os: string) : MacDialog | WindowsDialog { return os === 'mac' ? new MacDialog() : new WindowsDialog() } } const factory = new DialogFactory() const dialog1 = factory.createDialog(os) const dialog2 = factory.createDialog(os)
Builder
The builder pattern allows you to construct complex objects step by step.
class Coffee { constructor( public name: string, public sugar?: boolean, public milk?: boolean ) {} withSugar() { this.sugar = true return this } withMilk() { this.milk = true return this } } const culliCoffee = new Coffee('Culi Coffee').withSugar() const whiteCoffee = new Coffee('White Coffee').withMilk() const vietCoffee = new Coffee('Viet Coffee').withSugar().withMilk()
Prototype
Prototype allows objects to be cloned.
const ai = { training() { return '⛑' } } const openai = Object.create(ai, { name: { value: 'openai' } }) openai.__proto__ Object.getPrototypeOf(openai) const vercel = Object.create(openai, { other: { value: 'vercel' } })
Singleton
A singleton is a class that can be instantiated only once.
class Theme { static instance: Theme public readonly mode = 'light' private constructor() {} static getInstance() { if (!Theme.instance) { Theme.instance = new Theme() } return Theme.instance } } const theme = new Theme() // Throws error const theme = Theme.getInstance()
Structural
Structural design patterns provide various object relationships.
Facade
A facade provides a simplified interface to a complex system.
class PlumbingSystem { setPressure(v: number) {} turnOn() {} turnOff() {} } class ElectricalSystem { setVoltage(v: number) {} turnOn() {} turnOff() {} } class Building { private plumbing = new PlumbingSystem() private electical = new ElectricalSystem() public turnOnSystems() { this.electical.setVoltage(220) this.electical.turnOn() this.plumbing.setPressure(600) this.plumbing.turnOn() } public shutDownSystems() { this.plumbing.turnOff() this.electical.turnOff() } } const client = new Building() client.turnOnSystems() client.shutDownSystems()
Proxy
The proxy pattern allows you to control access to an object.
const original = { name: 'thanh' } const reactive = new Proxy(original, { get(target, key) { console.log(`Tracking: ${key}`) return target[key] }, set(target, key, value) { console.log(`Updating: ${key}`) return Reflect.set(target, key, value) } }) reactive.name reactive.name = 'not'
Behavioral
Behavioral patterns are used to identify communication between objects.
Interator
The interator pattern allows you to traverse a collection.
function range(start: number, end: number, step = 1) { return { [Symbol.iterator]() { return this }, next() { if (start < end) { start += step return { value: start, done: false } } return { value: end, done: true } } } } for (const n of range(0, 100, 10)) { console.log(n); }
Mediator
The mediator is provieds a middle layer between objects that communicate each other.
import { Hono } from 'hono' import { createMiddleware } from 'hono/factory' const app = new Hono() // Middleware const mediator = createMiddleware(async (c, next) => { console.log(`[${c.req.method}] ${c.req.url}`) await next() }) app.use(mediator) // Mediator runs before each route handler app.get('/', (c) => { return c.text('Welcome') }) app.get('/hello', (c) => { return c.text('Hello Mediator') })
Observer
The observer pattern allows you to subscribe to events.
type Observer<T> = (data: T) => void class Observable<T> { private observers: Observer<T>[] constructor() { this.observers = [] } subscribe(observer: Observer<T>): void { this.observers.push(observer) } unsubscribe(observer: Observer<T>): void { this.observers = this.observers.filter(obs => obs !== observer) } notify(data: T): void { this.observers.forEach(observer => observer(data)) } } const observable = new Observable<string>() const observer1 = (data: string) => console.log(`Observer 1: ${data}`) const observer2 = (data: string) => console.log(`Observer 2: ${data}`) observable.subscribe(observer1) observable.subscribe(observer2) observable.notify('Hello Observers!') observable.unsubscribe(observer1) observable.notify('Hello Observer 2!')
State
The state pattern is used to encapsulate an object’s state.
interface Emotion { think(): string } class HappyEmotion implements Emotion { think() { return 'I am happy' } } class SadEmotion implements Emotion { think() { return 'I am sad' } } class Human { emotion: Emotion constructor() { this.emotion = new HappyEmotion() } changeEmotion(emotion: Emotion) { this.emotion = emotion } think() { return this.emotion.think() } } const human = new Human() console.log(human.think()) // Prints "I am happy" human.changeEmotion(new SadEmotion()) console.log(human.think()) // Prints "I am sad"