← Back to notes

Design Patterns in JavaScript

javascript

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"