Singleton Pattern
Ensure a class has only one instance and provide global access to it.
// Class-based Singleton
class Database {
static #instance = null;
constructor() {
if (Database.#instance) {
return Database.#instance;
}
this.connection = null;
Database.#instance = this;
}
connect(url) {
if (!this.connection) {
this.connection = `Connected to ${url}`;
}
return this.connection;
}
static getInstance() {
if (!Database.#instance) {
Database.#instance = new Database();
}
return Database.#instance;
}
}
// Usage
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true
const db3 = Database.getInstance();
console.log(db1 === db3); // true
// Module-based Singleton (simpler in ES modules)
// database.js
let connection = null;
export function connect(url) {
if (!connection) {
connection = `Connected to ${url}`;
}
return connection;
}
export function getConnection() {
return connection;
}
// All imports share the same state
Factory Pattern
Create objects without specifying their exact class.
// Simple Factory
class Car {
constructor(make, model) {
this.make = make;
this.model = model;
}
}
class Truck {
constructor(make, model, capacity) {
this.make = make;
this.model = model;
this.capacity = capacity;
}
}
class VehicleFactory {
static create(type, options) {
switch (type) {
case "car":
return new Car(options.make, options.model);
case "truck":
return new Truck(options.make, options.model, options.capacity);
default:
throw new Error(`Unknown vehicle type: ${type}`);
}
}
}
const car = VehicleFactory.create("car", { make: "Toyota", model: "Camry" });
const truck = VehicleFactory.create("truck", {
make: "Ford",
model: "F-150",
capacity: 1000
});
// Factory with registration
class NotificationFactory {
static #creators = new Map();
static register(type, creator) {
this.#creators.set(type, creator);
}
static create(type, options) {
const creator = this.#creators.get(type);
if (!creator) {
throw new Error(`No creator registered for: ${type}`);
}
return creator(options);
}
}
// Register creators
NotificationFactory.register("email", (opts) => ({
type: "email",
send: () => console.log(`Email to ${opts.to}`)
}));
NotificationFactory.register("sms", (opts) => ({
type: "sms",
send: () => console.log(`SMS to ${opts.phone}`)
}));
// Use
const email = NotificationFactory.create("email", { to: "user@example.com" });
email.send();
Observer Pattern
Define a subscription mechanism to notify multiple objects about events.
// EventEmitter implementation
class EventEmitter {
#events = new Map();
on(event, listener) {
if (!this.#events.has(event)) {
this.#events.set(event, []);
}
this.#events.get(event).push(listener);
// Return unsubscribe function
return () => this.off(event, listener);
}
off(event, listener) {
if (!this.#events.has(event)) return;
const listeners = this.#events.get(event);
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
}
emit(event, ...args) {
if (!this.#events.has(event)) return;
this.#events.get(event).forEach(listener => {
listener(...args);
});
}
once(event, listener) {
const wrapper = (...args) => {
listener(...args);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}
// Usage
const store = new EventEmitter();
const unsubscribe = store.on("userLoggedIn", (user) => {
console.log(`Welcome, ${user.name}!`);
});
store.on("userLoggedIn", (user) => {
console.log(`Updating UI for ${user.name}`);
});
store.emit("userLoggedIn", { name: "John" });
// Welcome, John!
// Updating UI for John
unsubscribe(); // Remove first listener
// Pub/Sub (more decoupled)
class PubSub {
static #subscribers = new Map();
static subscribe(topic, callback) {
if (!this.#subscribers.has(topic)) {
this.#subscribers.set(topic, new Set());
}
this.#subscribers.get(topic).add(callback);
return () => this.unsubscribe(topic, callback);
}
static unsubscribe(topic, callback) {
this.#subscribers.get(topic)?.delete(callback);
}
static publish(topic, data) {
this.#subscribers.get(topic)?.forEach(cb => cb(data));
}
}
// Different modules can communicate without knowing each other
PubSub.subscribe("cart:updated", (cart) => {
console.log("Cart badge:", cart.items.length);
});
PubSub.publish("cart:updated", { items: [1, 2, 3] });
Module Pattern
Encapsulate code into self-contained units with public/private members.
// IIFE Module Pattern (pre-ES6)
const Calculator = (function() {
// Private
let result = 0;
function validate(n) {
return typeof n === "number" && !isNaN(n);
}
// Public API
return {
add(n) {
if (validate(n)) result += n;
return this;
},
subtract(n) {
if (validate(n)) result -= n;
return this;
},
getResult() {
return result;
},
reset() {
result = 0;
return this;
}
};
})();
Calculator.add(5).add(3).subtract(2);
console.log(Calculator.getResult()); // 6
// Revealing Module Pattern
const UserService = (function() {
const users = [];
function findById(id) {
return users.find(u => u.id === id);
}
function add(user) {
users.push({ ...user, id: Date.now() });
}
function remove(id) {
const index = users.findIndex(u => u.id === id);
if (index > -1) users.splice(index, 1);
}
function getAll() {
return [...users];
}
// Reveal only what's needed
return {
add,
remove,
getAll,
findById
};
})();
// ES6 Module equivalent
// userService.js
const users = [];
export function add(user) {
users.push({ ...user, id: Date.now() });
}
export function getAll() {
return [...users];
}
Strategy Pattern
Define a family of algorithms and make them interchangeable.
// Validation strategies
const validators = {
email: (value) => {
const pattern = /^[\w.-]+@[\w.-]+\.\w{2,}$/;
return pattern.test(value) ? null : "Invalid email";
},
required: (value) => {
return value?.trim() ? null : "This field is required";
},
minLength: (min) => (value) => {
return value.length >= min ? null : `Minimum ${min} characters`;
},
password: (value) => {
const hasLetter = /[a-zA-Z]/.test(value);
const hasNumber = /\d/.test(value);
const hasMinLength = value.length >= 8;
if (!hasMinLength) return "Password must be at least 8 characters";
if (!hasLetter) return "Password must contain a letter";
if (!hasNumber) return "Password must contain a number";
return null;
}
};
class FormValidator {
#rules = new Map();
addRule(field, validator) {
if (!this.#rules.has(field)) {
this.#rules.set(field, []);
}
this.#rules.get(field).push(validator);
return this;
}
validate(data) {
const errors = {};
for (const [field, validators] of this.#rules) {
for (const validator of validators) {
const error = validator(data[field]);
if (error) {
errors[field] = error;
break;
}
}
}
return {
valid: Object.keys(errors).length === 0,
errors
};
}
}
// Usage
const validator = new FormValidator()
.addRule("email", validators.required)
.addRule("email", validators.email)
.addRule("password", validators.required)
.addRule("password", validators.password)
.addRule("name", validators.required)
.addRule("name", validators.minLength(2));
const result = validator.validate({
email: "test@example.com",
password: "pass123",
name: "J"
});
// { valid: false, errors: { password: "...", name: "..." } }
Decorator Pattern
Add behavior to objects dynamically without affecting other objects.
// Function decorators
function withLogging(fn) {
return function(...args) {
console.log(`Calling ${fn.name} with:`, args);
const result = fn.apply(this, args);
console.log(`${fn.name} returned:`, result);
return result;
};
}
function withTiming(fn) {
return function(...args) {
const start = performance.now();
const result = fn.apply(this, args);
console.log(`${fn.name} took ${performance.now() - start}ms`);
return result;
};
}
function add(a, b) {
return a + b;
}
const enhancedAdd = withLogging(withTiming(add));
enhancedAdd(2, 3);
// Object decorator
function withCache(obj, methodName) {
const original = obj[methodName];
const cache = new Map();
obj[methodName] = function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log("Cache hit");
return cache.get(key);
}
const result = original.apply(this, args);
cache.set(key, result);
return result;
};
return obj;
}
const api = {
fetchUser(id) {
console.log("Fetching...");
return { id, name: "User " + id };
}
};
withCache(api, "fetchUser");
api.fetchUser(1); // Fetching...
api.fetchUser(1); // Cache hit
// Class method decorator (TC39 proposal)
function logged(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling ${name}`);
return original.apply(this, args);
};
return descriptor;
}
Command Pattern
The Command pattern encapsulates a request as an object, letting you parameterize clients with different requests, queue requests, and support undo operations.
// Command interface
class Command {
execute() { throw new Error('Not implemented'); }
undo() { throw new Error('Not implemented'); }
}
// Concrete commands
class AddTextCommand extends Command {
constructor(editor, text) {
super();
this.editor = editor;
this.text = text;
}
execute() {
this.previousPosition = this.editor.cursor;
this.editor.insert(this.text);
}
undo() {
this.editor.cursor = this.previousPosition;
this.editor.delete(this.text.length);
}
}
class DeleteTextCommand extends Command {
constructor(editor, length) {
super();
this.editor = editor;
this.length = length;
}
execute() {
this.deletedText = this.editor.getSelection(this.length);
this.position = this.editor.cursor;
this.editor.delete(this.length);
}
undo() {
this.editor.cursor = this.position;
this.editor.insert(this.deletedText);
}
}
// Command manager with undo/redo
class CommandManager {
constructor() {
this.history = [];
this.redoStack = [];
}
execute(command) {
command.execute();
this.history.push(command);
this.redoStack = []; // Clear redo stack on new command
}
undo() {
const command = this.history.pop();
if (command) {
command.undo();
this.redoStack.push(command);
}
}
redo() {
const command = this.redoStack.pop();
if (command) {
command.execute();
this.history.push(command);
}
}
}
// Usage
const editor = new TextEditor();
const manager = new CommandManager();
manager.execute(new AddTextCommand(editor, "Hello "));
manager.execute(new AddTextCommand(editor, "World"));
manager.undo(); // Removes "World"
manager.redo(); // Adds "World" back
Mediator Pattern
The Mediator pattern defines an object that encapsulates how a set of objects interact, promoting loose coupling by keeping objects from referring to each other explicitly.
// Chat room mediator
class ChatRoom {
constructor() {
this.users = new Map();
}
join(user) {
this.users.set(user.name, user);
user.chatRoom = this;
this.broadcast(`${user.name} joined the chat`, user);
}
leave(user) {
this.users.delete(user.name);
this.broadcast(`${user.name} left the chat`, user);
}
send(message, from, to) {
if (to) {
// Private message
const recipient = this.users.get(to);
if (recipient) {
recipient.receive(message, from.name);
}
} else {
// Broadcast to all
this.broadcast(message, from);
}
}
broadcast(message, from) {
this.users.forEach((user, name) => {
if (name !== from.name) {
user.receive(message, from.name);
}
});
}
}
class User {
constructor(name) {
this.name = name;
this.chatRoom = null;
}
send(message, to = null) {
this.chatRoom.send(message, this, to);
}
receive(message, from) {
console.log(`${this.name} received from ${from}: ${message}`);
}
}
// Usage
const room = new ChatRoom();
const alice = new User('Alice');
const bob = new User('Bob');
room.join(alice);
room.join(bob);
alice.send('Hi everyone!'); // Bob receives
bob.send('Hey Alice!', 'Alice'); // Only Alice receives
State Pattern
The State pattern allows an object to alter its behavior when its internal state changes. The object appears to change its class.
// Traffic light state machine
class TrafficLightState {
constructor(light) {
this.light = light;
}
next() { throw new Error('Not implemented'); }
getColor() { throw new Error('Not implemented'); }
}
class RedState extends TrafficLightState {
next() {
this.light.setState(new GreenState(this.light));
}
getColor() { return 'RED'; }
}
class YellowState extends TrafficLightState {
next() {
this.light.setState(new RedState(this.light));
}
getColor() { return 'YELLOW'; }
}
class GreenState extends TrafficLightState {
next() {
this.light.setState(new YellowState(this.light));
}
getColor() { return 'GREEN'; }
}
class TrafficLight {
constructor() {
this.state = new RedState(this);
}
setState(state) {
this.state = state;
}
next() {
this.state.next();
}
getColor() {
return this.state.getColor();
}
}
// Usage
const light = new TrafficLight();
console.log(light.getColor()); // RED
light.next();
console.log(light.getColor()); // GREEN
light.next();
console.log(light.getColor()); // YELLOW
light.next();
console.log(light.getColor()); // RED
// Promise-like state machine
class AsyncOperation {
#state = 'pending';
#value = undefined;
#handlers = [];
resolve(value) {
if (this.#state !== 'pending') return;
this.#state = 'fulfilled';
this.#value = value;
this.#handlers.forEach(h => h.onFulfilled?.(value));
}
reject(error) {
if (this.#state !== 'pending') return;
this.#state = 'rejected';
this.#value = error;
this.#handlers.forEach(h => h.onRejected?.(error));
}
then(onFulfilled, onRejected) {
if (this.#state === 'fulfilled') {
onFulfilled?.(this.#value);
} else if (this.#state === 'rejected') {
onRejected?.(this.#value);
} else {
this.#handlers.push({ onFulfilled, onRejected });
}
return this;
}
}
- Singleton: When exactly one instance is needed globally
- Factory: When object creation logic is complex or varies
- Observer: When multiple objects need to react to changes
- Command: When you need undo/redo or request queuing
- State: When behavior changes based on internal state
Summary
Singleton
Single instance, global access
Factory
Create objects without specifying class
Observer
Event subscription and notification
Module
Encapsulation with public/private
Strategy
Interchangeable algorithms
Decorator
Add behavior dynamically