← Snippets

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"