
JavaScript Design Patterns You Need to Know
Design patterns are reusable solutions to common programming problems. They represent best practices evolved over time by experienced developers. In JavaScript, these patterns become particularly powerful due to the language's flexible nature.
Why Design Patterns Matter
Before diving into specific patterns, let's understand why they're important:
// Without patterns - chaotic and hard to maintain
const userStuff = {
data: [],
save: function() { /* ... */ },
validate: function() { /* ... */ },
// 50 more unrelated methods...
};
// With patterns - organized and predictable
class UserModule {
#data = [];
save() { /* ... */ }
validate() { /* ... */ }
// Clear separation of concerns
}
1. Module Pattern
The Module Pattern is one of the most fundamental patterns in JavaScript, providing encapsulation and organization.
Basic Module Pattern
// Using IIFE (Immediately Invoked Function Expression)
const CounterModule = (function() {
// Private variables and functions
let count = 0;
function log(message) {
console.log(`[Counter] ${message}`);
}
// Public API
return {
increment() {
count++;
log(`Incremented to ${count}`);
return count;
},
decrement() {
count--;
log(`Decremented to ${count}`);
return count;
},
getCount() {
return count;
},
reset() {
count = 0;
log('Reset');
}
};
})();
// Usage
CounterModule.increment(); // [Counter] Incremented to 1
CounterModule.increment(); // [Counter] Incremented to 2
console.log(CounterModule.count); // undefined (private!)
console.log(CounterModule.getCount()); // 2
Revealing Module Pattern
const UserService = (function() {
// Private data
const users = new Map();
// Private methods
function validateEmail(email) {
return email.includes('@');
}
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
function logAction(action, userId) {
console.log(`[${new Date().toISOString()}] ${action}: ${userId}`);
}
// Revealed public methods
return {
createUser: function(name, email) {
if (!validateEmail(email)) {
throw new Error('Invalid email');
}
const id = generateId();
const user = { id, name, email, createdAt: new Date() };
users.set(id, user);
logAction('User created', id);
return user;
},
getUser: function(id) {
return users.get(id);
},
getAllUsers: function() {
return Array.from(users.values());
},
deleteUser: function(id) {
const deleted = users.delete(id);
if (deleted) {
logAction('User deleted', id);
}
return deleted;
}
};
})();
// Usage
const user = UserService.createUser('John Doe', 'john@example.com');
console.log(UserService.getUser(user.id));
ES6 Module Pattern
// math.js - ES6 module
const PI = 3.14159;
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
// Private helper
function validateNumber(n) {
if (typeof n !== 'number') throw new Error('Not a number');
}
// Public API
export { add, subtract, multiply, divide, PI };
// app.js
// import { add, multiply, PI } from './math.js';
// console.log(add(5, multiply(2, PI)));
2. Singleton Pattern
Ensures a class has only one instance and provides a global point of access to it.
Classic Singleton
class Database {
constructor() {
if (Database.instance) {
return Database.instance;
}
this.connection = null;
this.isConnected = false;
Database.instance = this;
}
connect(connectionString) {
if (!this.isConnected) {
this.connection = connectionString;
this.isConnected = true;
console.log('Connected to database');
}
return this;
}
query(sql) {
if (!this.isConnected) {
throw new Error('Not connected to database');
}
console.log(`Executing query: ${sql}`);
return { rows: [] };
}
disconnect() {
if (this.isConnected) {
this.isConnected = false;
this.connection = null;
console.log('Disconnected from database');
}
}
}
// Usage
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true (same instance)
db1.connect('mongodb://localhost:27017/mydb');
db2.query('SELECT * FROM users'); // Works through same connection
Modern Singleton with Symbol
const DatabaseConnection = (function() {
const instance = Symbol('instance');
class DatabaseConnection {
constructor() {
if (DatabaseConnection[instance]) {
return DatabaseConnection[instance];
}
this.pool = null;
this.transactions = [];
DatabaseConnection[instance] = this;
}
async beginTransaction() {
const id = Date.now().toString();
this.transactions.push(id);
console.log(`Transaction ${id} started`);
return id;
}
async commit(transactionId) {
const index = this.transactions.indexOf(transactionId);
if (index > -1) {
this.transactions.splice(index, 1);
console.log(`Transaction ${transactionId} committed`);
}
}
async rollback(transactionId) {
const index = this.transactions.indexOf(transactionId);
if (index > -1) {
this.transactions.splice(index, 1);
console.log(`Transaction ${transactionId} rolled back`);
}
}
}
return DatabaseConnection;
})();
// Usage
const conn1 = new DatabaseConnection();
const conn2 = new DatabaseConnection();
console.log(conn1 === conn2); // true
3. Factory Pattern
Creates objects without specifying the exact class of object that will be created.
Simple Factory
// Vehicle types
class Car {
constructor(model) {
this.type = 'Car';
this.model = model;
this.wheels = 4;
}
drive() {
console.log(`Driving ${this.type}: ${this.model}`);
}
}
class Motorcycle {
constructor(model) {
this.type = 'Motorcycle';
this.model = model;
this.wheels = 2;
}
drive() {
console.log(`Riding ${this.type}: ${this.model}`);
}
}
class Truck {
constructor(model) {
this.type = 'Truck';
this.model = model;
this.wheels = 6;
}
drive() {
console.log(`Driving ${this.type}: ${this.model}`);
}
}
// Factory
class VehicleFactory {
createVehicle(type, model) {
switch(type.toLowerCase()) {
case 'car':
return new Car(model);
case 'motorcycle':
return new Motorcycle(model);
case 'truck':
return new Truck(model);
default:
throw new Error(`Vehicle type ${type} not supported`);
}
}
// Registry pattern extension
registerVehicle(type, vehicleClass) {
this[`create${type}`] = (model) => new vehicleClass(model);
}
}
// Usage
const factory = new VehicleFactory();
const myCar = factory.createVehicle('car', 'Tesla Model 3');
const myBike = factory.createVehicle('motorcycle', 'Harley Davidson');
myCar.drive(); // Driving Car: Tesla Model 3
myBike.drive(); // Riding Motorcycle: Harley Davidson
Abstract Factory
// Abstract factory for UI components
class UIFactory {
createButton() {
throw new Error('Method createButton must be implemented');
}
createDialog() {
throw new Error('Method createDialog must be implemented');
}
createMenu() {
throw new Error('Method createMenu must be implemented');
}
}
// Concrete factory for dark theme
class DarkThemeFactory extends UIFactory {
createButton() {
return new DarkButton();
}
createDialog() {
return new DarkDialog();
}
createMenu() {
return new DarkMenu();
}
}
// Concrete factory for light theme
class LightThemeFactory extends UIFactory {
createButton() {
return new LightButton();
}
createDialog() {
return new LightDialog();
}
createMenu() {
return new LightMenu();
}
}
// Products
class DarkButton {
constructor() {
this.theme = 'dark';
this.background = '#333';
this.color = '#fff';
}
render() {
console.log(`Rendering ${this.theme} button`);
}
}
class LightButton {
constructor() {
this.theme = 'light';
this.background = '#fff';
this.color = '#333';
}
render() {
console.log(`Rendering ${this.theme} button`);
}
}
class DarkDialog {
constructor() {
this.theme = 'dark';
this.background = '#222';
this.color = '#fff';
}
render() {
console.log(`Rendering ${this.theme} dialog`);
}
}
class LightDialog {
constructor() {
this.theme = 'light';
this.background = '#f5f5f5';
this.color = '#333';
}
render() {
console.log(`Rendering ${this.theme} dialog`);
}
}
// Usage
function createUI(factory) {
const button = factory.createButton();
const dialog = factory.createDialog();
button.render();
dialog.render();
}
createUI(new DarkThemeFactory());
createUI(new LightThemeFactory());
4. Observer Pattern
Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified.
Basic Observer
class Subject {
constructor() {
this.observers = [];
this.state = null;
}
attach(observer) {
if (!this.observers.includes(observer)) {
this.observers.push(observer);
console.log('Observer attached');
}
}
detach(observer) {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
console.log('Observer detached');
}
}
notify() {
console.log(`Notifying ${this.observers.length} observers`);
this.observers.forEach(observer => observer.update(this));
}
setState(state) {
console.log(`State changed to: ${state}`);
this.state = state;
this.notify();
}
getState() {
return this.state;
}
}
class Observer {
constructor(name) {
this.name = name;
}
update(subject) {
console.log(`${this.name} received update: ${subject.getState()}`);
}
}
// Usage
const subject = new Subject();
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');
subject.attach(observer1);
subject.attach(observer2);
subject.setState('New State');
// Observer 1 received update: New State
// Observer 2 received update: New State
subject.detach(observer1);
subject.setState('Another State');
// Observer 2 received update: Another State
Event Emitter Implementation
class EventEmitter {
constructor() {
this.events = {};
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
// Return unsubscribe function
return () => {
this.off(eventName, callback);
};
}
off(eventName, callback) {
if (!this.events[eventName]) return;
this.events[eventName] = this.events[eventName]
.filter(cb => cb !== callback);
}
emit(eventName, data) {
if (!this.events[eventName]) return;
this.events[eventName].forEach(callback => {
try {
callback(data);
} catch (error) {
console.error(`Error in ${eventName} handler:`, error);
}
});
}
once(eventName, callback) {
const wrapper = (data) => {
callback(data);
this.off(eventName, wrapper);
};
this.on(eventName, wrapper);
}
clear(eventName) {
if (eventName) {
delete this.events[eventName];
} else {
this.events = {};
}
}
listenerCount(eventName) {
return this.events[eventName]?.length || 0;
}
}
// Usage
const emitter = new EventEmitter();
// Subscribe to events
const unsubscribe = emitter.on('user:login', (user) => {
console.log(`User logged in: ${user.name}`);
});
emitter.once('app:init', () => {
console.log('App initialized (this runs only once)');
});
// Emit events
emitter.emit('app:init');
emitter.emit('app:init'); // Won't trigger
emitter.emit('user:login', { name: 'John', id: 1 });
emitter.emit('user:login', { name: 'Jane', id: 2 });
unsubscribe(); // Remove specific listener
emitter.emit('user:login', { name: 'Bob', id: 3 }); // Won't trigger
console.log('Listeners:', emitter.listenerCount('user:login')); // 0
Real-World Example: Shopping Cart
class ShoppingCart extends EventEmitter {
constructor() {
super();
this.items = [];
this.total = 0;
}
addItem(item) {
this.items.push(item);
this.calculateTotal();
this.emit('item:added', { item, cartSize: this.items.length });
}
removeItem(itemId) {
const index = this.items.findIndex(i => i.id === itemId);
if (index > -1) {
const removed = this.items.splice(index, 1)[0];
this.calculateTotal();
this.emit('item:removed', { item: removed, cartSize: this.items.length });
}
}
calculateTotal() {
const previousTotal = this.total;
this.total = this.items.reduce((sum, item) => sum + item.price, 0);
if (previousTotal !== this.total) {
this.emit('total:updated', { previous: previousTotal, current: this.total });
}
}
checkout() {
if (this.items.length === 0) {
this.emit('checkout:failed', { reason: 'Cart is empty' });
return;
}
this.emit('checkout:started', { items: this.items, total: this.total });
// Simulate async checkout
setTimeout(() => {
this.emit('checkout:completed', {
orderId: Date.now(),
total: this.total,
items: [...this.items]
});
this.clearCart();
}, 2000);
}
clearCart() {
this.items = [];
this.total = 0;
this.emit('cart:cleared');
}
}
// Usage
const cart = new ShoppingCart();
// Subscribe to various events
cart.on('item:added', ({ item, cartSize }) => {
console.log(`š¦ Added ${item.name} to cart. Cart has ${cartSize} items`);
updateUI();
});
cart.on('total:updated', ({ previous, current }) => {
console.log(`š° Total updated: $${previous} ā $${current}`);
updateTotalDisplay();
});
cart.on('checkout:started', ({ total }) => {
console.log(`š Processing checkout for $${total}...`);
showLoadingSpinner();
});
cart.on('checkout:completed', ({ orderId, items }) => {
console.log(`ā
Order #${orderId} completed!`);
console.log('Items:', items.map(i => i.name).join(', '));
hideLoadingSpinner();
showSuccessMessage();
});
cart.on('checkout:failed', ({ reason }) => {
console.log(`ā Checkout failed: ${reason}`);
showErrorMessage(reason);
});
// Simulate user actions
cart.addItem({ id: 1, name: 'Laptop', price: 999 });
cart.addItem({ id: 2, name: 'Mouse', price: 29 });
cart.removeItem(1);
cart.checkout();
5. Prototype Pattern
Creates new objects by cloning existing ones.
Basic Prototype
class Shape {
constructor() {
this.type = 'shape';
this.color = 'black';
this.x = 0;
this.y = 0;
}
clone() {
return Object.create(
Object.getPrototypeOf(this),
Object.getOwnPropertyDescriptors(this)
);
}
move(x, y) {
this.x = x;
this.y = y;
return this;
}
describe() {
return `${this.color} ${this.type} at (${this.x}, ${this.y})`;
}
}
class Circle extends Shape {
constructor() {
super();
this.type = 'circle';
this.radius = 1;
}
clone() {
const clone = super.clone();
clone.radius = this.radius;
return clone;
}
area() {
return Math.PI * this.radius ** 2;
}
}
class Rectangle extends Shape {
constructor() {
super();
this.type = 'rectangle';
this.width = 1;
this.height = 1;
}
clone() {
const clone = super.clone();
clone.width = this.width;
clone.height = this.height;
return clone;
}
area() {
return this.width * this.height;
}
}
// Usage
const prototypeCircle = new Circle();
prototypeCircle.color = 'red';
prototypeCircle.radius = 5;
const circle1 = prototypeCircle.clone().move(10, 10);
const circle2 = prototypeCircle.clone().move(20, 20);
console.log(circle1.describe()); // red circle at (10, 10)
console.log(circle2.describe()); // red circle at (20, 20)
console.log(circle1.area()); // 78.53981633974483
Prototype Registry
class PrototypeRegistry {
constructor() {
this.prototypes = new Map();
}
register(name, prototype) {
this.prototypes.set(name, prototype);
}
unregister(name) {
this.prototypes.delete(name);
}
create(name, ...args) {
const prototype = this.prototypes.get(name);
if (!prototype) {
throw new Error(`Prototype ${name} not found`);
}
const clone = prototype.clone();
// Apply any additional modifications
if (args.length > 0 && typeof clone.initialize === 'function') {
clone.initialize(...args);
}
return clone;
}
getPrototypes() {
return Array.from(this.prototypes.keys());
}
}
// Usage
const registry = new PrototypeRegistry();
// Register prototypes
registry.register('defaultCircle', new Circle());
registry.register('bigRedCircle',
Object.assign(new Circle(), { color: 'red', radius: 10 })
);
registry.register('square',
Object.assign(new Rectangle(), { width: 10, height: 10 })
);
// Create objects from prototypes
const circle = registry.create('bigRedCircle');
console.log(circle.describe()); // red circle at (0, 0)
const square = registry.create('square').move(5, 5);
console.log(square.describe()); // black rectangle at (5, 5)
6. Command Pattern
Encapsulates a request as an object, allowing parameterization and queuing of requests.
Basic Command
class Command {
execute() {
throw new Error('execute method must be implemented');
}
undo() {
throw new Error('undo method must be implemented');
}
}
class Calculator {
constructor() {
this.value = 0;
}
add(amount) {
this.value += amount;
}
subtract(amount) {
this.value -= amount;
}
multiply(amount) {
this.value *= amount;
}
divide(amount) {
if (amount !== 0) {
this.value /= amount;
}
}
}
class AddCommand extends Command {
constructor(calculator, amount) {
super();
this.calculator = calculator;
this.amount = amount;
}
execute() {
this.calculator.add(this.amount);
}
undo() {
this.calculator.subtract(this.amount);
}
}
class MultiplyCommand extends Command {
constructor(calculator, amount) {
super();
this.calculator = calculator;
this.amount = amount;
}
execute() {
this.calculator.multiply(this.amount);
}
undo() {
this.calculator.divide(this.amount);
}
}
class CommandInvoker {
constructor() {
this.history = [];
this.redoStack = [];
}
executeCommand(command) {
command.execute();
this.history.push(command);
this.redoStack = []; // Clear redo stack
}
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 calculator = new Calculator();
const invoker = new CommandInvoker();
invoker.executeCommand(new AddCommand(calculator, 10));
invoker.executeCommand(new AddCommand(calculator, 5));
invoker.executeCommand(new MultiplyCommand(calculator, 2));
console.log(calculator.value); // 30
invoker.undo();
console.log(calculator.value); // 15
invoker.redo();
console.log(calculator.value); // 30
Advanced Command with Complex Operations
class Editor {
constructor() {
this.content = '';
this.selection = { start: 0, end: 0 };
}
insert(text, position = this.selection.start) {
this.content =
this.content.slice(0, position) +
text +
this.content.slice(position);
this.selection = {
start: position + text.length,
end: position + text.length
};
}
delete(start = this.selection.start, end = this.selection.end) {
const deleted = this.content.slice(start, end);
this.content = this.content.slice(0, start) + this.content.slice(end);
this.selection = { start, end: start };
return deleted;
}
replace(search, replace) {
const index = this.content.indexOf(search);
if (index !== -1) {
const before = this.content.slice(0, index);
const after = this.content.slice(index + search.length);
this.content = before + replace + after;
}
}
}
class TextCommand extends Command {
constructor(editor) {
super();
this.editor = editor;
}
}
class InsertCommand extends TextCommand {
constructor(editor, text, position) {
super(editor);
this.text = text;
this.position = position;
this.deletedText = '';
}
execute() {
if (this.editor.selection.start !== this.editor.selection.end) {
// Delete selected text first
this.deletedText = this.editor.delete();
}
this.editor.insert(this.text, this.position);
}
undo() {
// Remove inserted text
this.editor.delete(this.position, this.position + this.text.length);
// Restore deleted text if any
if (this.deletedText) {
this.editor.insert(this.deletedText, this.position);
}
}
}
class DeleteCommand extends TextCommand {
constructor(editor, start, end) {
super(editor);
this.start = start;
this.end = end;
this.deletedText = '';
}
execute() {
this.deletedText = this.editor.delete(this.start, this.end);
}
undo() {
this.editor.insert(this.deletedText, this.start);
}
}
class ReplaceCommand extends TextCommand {
constructor(editor, search, replace) {
super(editor);
this.search = search;
this.replace = replace;
this.originalText = '';
this.index = -1;
}
execute() {
this.index = this.editor.content.indexOf(this.search);
if (this.index !== -1) {
this.originalText = this.search;
this.editor.replace(this.search, this.replace);
}
}
undo() {
if (this.index !== -1) {
this.editor.replace(this.replace, this.originalText);
}
}
}
// Usage with macro commands
class MacroCommand extends Command {
constructor(commands = []) {
super();
this.commands = commands;
}
add(command) {
this.commands.push(command);
}
execute() {
this.commands.forEach(cmd => cmd.execute());
}
undo() {
this.commands.reverse().forEach(cmd => cmd.undo());
}
}
// Example
const editor = new Editor();
const history = new CommandInvoker();
// Create a macro for formatting text
const formatMacro = new MacroCommand();
formatMacro.add(new InsertCommand(editor, 'Hello, '));
formatMacro.add(new InsertCommand(editor, 'World!', 7));
history.executeCommand(formatMacro);
console.log(editor.content); // "Hello, World!"
history.undo();
console.log(editor.content); // ""
7. Strategy Pattern
Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
Basic Strategy
// Strategy interface
class SortStrategy {
sort(array) {
throw new Error('sort method must be implemented');
}
}
// Concrete strategies
class BubbleSort extends SortStrategy {
sort(array) {
const arr = [...array];
const n = arr.length;
for (let i = 0; i < n - 1; i++) {
for (let j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
console.log('Bubble sort completed');
return arr;
}
}
class QuickSort extends SortStrategy {
sort(array) {
const arr = [...array];
function quickSortHelper(left, right) {
if (left >= right) return;
const pivot = arr[Math.floor((left + right) / 2)];
let i = left;
let j = right;
while (i <= j) {
while (arr[i] < pivot) i++;
while (arr[j] > pivot) j--;
if (i <= j) {
[arr[i], arr[j]] = [arr[j], arr[i]];
i++;
j--;
}
}
quickSortHelper(left, j);
quickSortHelper(i, right);
}
quickSortHelper(0, arr.length - 1);
console.log('Quick sort completed');
return arr;
}
}
class MergeSort extends SortStrategy {
sort(array) {
const arr = [...array];
function merge(left, right) {
const result = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
if (left[i] < right[j]) {
result.push(left[i++]);
} else {
result.push(right[j++]);
}
}
return [...result, ...left.slice(i), ...right.slice(j)];
}
function mergeSortHelper(arr) {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = mergeSortHelper(arr.slice(0, mid));
const right = mergeSortHelper(arr.slice(mid));
return merge(left, right);
}
const result = mergeSortHelper(arr);
console.log('Merge sort completed');
return result;
}
}
// Context
class Sorter {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
sort(array) {
if (!this.strategy) {
throw new Error('No sort strategy set');
}
console.log(`Using ${this.strategy.constructor.name}`);
const start = performance.now();
const result = this.strategy.sort(array);
const end = performance.now();
console.log(`Time taken: ${(end - start).toFixed(2)}ms`);
return result;
}
}
// Usage
const sorter = new Sorter(new BubbleSort());
const data = [5, 2, 8, 1, 9, 3, 7, 4, 6];
console.log('Original:', data);
console.log('Sorted:', sorter.sort(data));
// Switch strategy based on data size
if (data.length > 1000) {
sorter.setStrategy(new QuickSort());
} else {
sorter.setStrategy(new MergeSort());
}
console.log('Resorted:', sorter.sort(data));
Payment Processing Example
// Payment strategies
class PaymentStrategy {
async pay(amount) {
throw new Error('pay method must be implemented');
}
async refund(transactionId) {
throw new Error('refund method must be implemented');
}
}
class CreditCardPayment extends PaymentStrategy {
constructor(cardNumber, expiry, cvv) {
super();
this.cardNumber = cardNumber;
this.expiry = expiry;
this.cvv = cvv;
this.transactions = [];
}
async pay(amount) {
console.log(`Processing credit card payment of $${amount}...`);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
const transaction = {
id: `CC-${Date.now()}`,
amount,
status: 'completed',
timestamp: new Date(),
cardLast4: this.cardNumber.slice(-4)
};
this.transactions.push(transaction);
console.log(`ā
Credit card payment completed: ${transaction.id}`);
return transaction;
}
async refund(transactionId) {
const transaction = this.transactions.find(t => t.id === transactionId);
if (!transaction) {
throw new Error('Transaction not found');
}
console.log(`Processing refund for ${transactionId}...`);
await new Promise(resolve => setTimeout(resolve, 1000));
transaction.status = 'refunded';
console.log(`ā
Refund completed for ${transactionId}`);
return transaction;
}
}
class PayPalPayment extends PaymentStrategy {
constructor(email, password) {
super();
this.email = email;
this.password = password;
this.transactions = [];
}
async pay(amount) {
console.log(`Processing PayPal payment of $${amount}...`);
// Simulate OAuth and payment
await new Promise(resolve => setTimeout(resolve, 1500));
const transaction = {
id: `PP-${Date.now()}`,
amount,
status: 'completed',
timestamp: new Date(),
email: this.email
};
this.transactions.push(transaction);
console.log(`ā
PayPal payment completed: ${transaction.id}`);
return transaction;
}
async refund(transactionId) {
const transaction = this.transactions.find(t => t.id === transactionId);
if (!transaction) {
throw new Error('Transaction not found');
}
console.log(`Processing refund for ${transactionId}...`);
await new Promise(resolve => setTimeout(resolve, 1500));
transaction.status = 'refunded';
console.log(`ā
Refund completed for ${transactionId}`);
return transaction;
}
}
class CryptoPayment extends PaymentStrategy {
constructor(walletAddress, currency = 'BTC') {
super();
this.walletAddress = walletAddress;
this.currency = currency;
this.transactions = [];
}
async pay(amount) {
const cryptoAmount = amount / this.getExchangeRate();
console.log(`Processing ${this.currency} payment of ${cryptoAmount.toFixed(8)}...`);
// Simulate blockchain transaction
await new Promise(resolve => setTimeout(resolve, 2000));
const transaction = {
id: `CR-${Date.now()}`,
amount: cryptoAmount,
fiatAmount: amount,
currency: this.currency,
status: 'pending',
timestamp: new Date(),
wallet: this.walletAddress.slice(0, 8) + '...'
};
// Wait for confirmations
await new Promise(resolve => setTimeout(resolve, 3000));
transaction.status = 'confirmed';
this.transactions.push(transaction);
console.log(`ā
Crypto payment confirmed: ${transaction.id}`);
return transaction;
}
getExchangeRate() {
// Mock exchange rates
const rates = { BTC: 50000, ETH: 3000, USDT: 1 };
return rates[this.currency] || 1;
}
async refund(transactionId) {
const transaction = this.transactions.find(t => t.id === transactionId);
if (!transaction || transaction.status !== 'confirmed') {
throw new Error('Cannot refund this transaction');
}
console.log(`Processing crypto refund for ${transactionId}...`);
await new Promise(resolve => setTimeout(resolve, 2000));
transaction.status = 'refunded';
console.log(`ā
Crypto refund completed for ${transactionId}`);
return transaction;
}
}
// Payment processor context
class PaymentProcessor {
constructor() {
this.strategy = null;
this.transactions = [];
}
setStrategy(strategy) {
this.strategy = strategy;
console.log(`Payment strategy set to ${strategy.constructor.name}`);
}
async processPayment(amount) {
if (!this.strategy) {
throw new Error('No payment strategy selected');
}
try {
const transaction = await this.strategy.pay(amount);
this.transactions.push(transaction);
return transaction;
} catch (error) {
console.error('Payment failed:', error.message);
throw error;
}
}
async processRefund(transactionId) {
if (!this.strategy) {
throw new Error('No payment strategy selected');
}
try {
return await this.strategy.refund(transactionId);
} catch (error) {
console.error('Refund failed:', error.message);
throw error;
}
}
getTransactions() {
return this.transactions;
}
}
// Usage
async function checkoutExample() {
const processor = new PaymentProcessor();
// Customer chooses credit card
processor.setStrategy(
new CreditCardPayment('4111111111111111', '12/25', '123')
);
const transaction1 = await processor.processPayment(99.99);
console.log('Transaction:', transaction1);
// Another customer chooses PayPal
processor.setStrategy(
new PayPalPayment('user@example.com', 'password123')
);
const transaction2 = await processor.processPayment(149.99);
// Refund first transaction
await processor.processRefund(transaction1.id);
}
// checkoutExample();
8. Decorator Pattern
Attaches additional responsibilities to an object dynamically.
Basic Decorator
// Component interface
class Coffee {
cost() {
return 5;
}
description() {
return 'Coffee';
}
}
// Decorator base class
class CoffeeDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost();
}
description() {
return this.coffee.description();
}
}
// Concrete decorators
class Milk extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 1.5;
}
description() {
return `${this.coffee.description()}, Milk`;
}
}
class Sugar extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 0.5;
}
description() {
return `${this.coffee.description()}, Sugar`;
}
}
class WhippedCream extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 2;
}
description() {
return `${this.coffee.description()}, Whipped Cream`;
}
}
class Caramel extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 1.75;
}
description() {
return `${this.coffee.description()}, Caramel`;
}
}
// Usage
let myCoffee = new Coffee();
console.log(`${myCoffee.description()}: $${myCoffee.cost()}`);
myCoffee = new Milk(myCoffee);
console.log(`${myCoffee.description()}: $${myCoffee.cost()}`);
myCoffee = new Sugar(myCoffee);
console.log(`${myCoffee.description()}: $${myCoffee.cost()}`);
myCoffee = new WhippedCream(myCoffee);
console.log(`${myCoffee.description()}: $${myCoffee.cost()}`);
// Output:
// Coffee: $5
// Coffee, Milk: $6.5
// Coffee, Milk, Sugar: $7
// Coffee, Milk, Sugar, Whipped Cream: $9
Advanced Decorator with Validation
class User {
constructor(name, email, age) {
this.name = name;
this.email = email;
this.age = age;
}
async save() {
console.log(`Saving user ${this.name} to database...`);
// Simulate database save
await new Promise(resolve => setTimeout(resolve, 500));
return { id: Date.now(), ...this };
}
validate() {
return true; // Base validation always passes
}
format() {
return {
name: this.name,
email: this.email,
age: this.age
};
}
}
// Validation decorators
class ValidationDecorator {
constructor(user) {
this.user = user;
}
async save() {
if (!this.validate()) {
throw new Error('Validation failed');
}
return this.user.save();
}
validate() {
return this.user.validate();
}
format() {
return this.user.format();
}
}
class EmailValidation extends ValidationDecorator {
validate() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isValid = emailRegex.test(this.user.email);
if (!isValid) {
console.error('Email validation failed');
return false;
}
return this.user.validate();
}
}
class AgeValidation extends ValidationDecorator {
validate() {
const isValid = this.user.age >= 18 && this.user.age <= 120;
if (!isValid) {
console.error('Age validation failed');
return false;
}
return this.user.validate();
}
}
class NameValidation extends ValidationDecorator {
validate() {
const isValid = this.user.name && this.user.name.length >= 2;
if (!isValid) {
console.error('Name validation failed');
return false;
}
return this.user.validate();
}
}
// Logging decorator
class LoggingDecorator extends ValidationDecorator {
async save() {
console.log(`š Attempting to save user: ${this.user.email}`);
const start = Date.now();
try {
const result = await super.save();
const duration = Date.now() - start;
console.log(`ā
User saved successfully in ${duration}ms`);
return result;
} catch (error) {
console.log(`ā Failed to save user: ${error.message}`);
throw error;
}
}
validate() {
console.log('š Running validation...');
return super.validate();
}
format() {
const formatted = super.format();
console.log('š Formatted user data:', formatted);
return formatted;
}
}
// Caching decorator
class CachingDecorator extends ValidationDecorator {
constructor(user, cache = new Map()) {
super(user);
this.cache = cache;
}
async save() {
const cacheKey = `user:${this.user.email}`;
if (this.cache.has(cacheKey)) {
console.log('š¦ Returning cached user');
return this.cache.get(cacheKey);
}
const result = await super.save();
this.cache.set(cacheKey, result);
console.log('š¾ User cached');
return result;
}
}
// Usage with multiple decorators
async function createUser() {
// Create base user
let user = new User('John Doe', 'john@example.com', 25);
// Apply decorators
user = new NameValidation(user);
user = new EmailValidation(user);
user = new AgeValidation(user);
user = new LoggingDecorator(user);
user = new CachingDecorator(user);
// Validate and save
const result = await user.save();
console.log('Final result:', result);
// Try saving again (should use cache)
const cached = await user.save();
console.log('Cached result:', cached);
}
// createUser();
9. Facade Pattern
Provides a simplified interface to a complex subsystem.
Basic Facade
// Complex subsystems
class CPU {
freeze() {
console.log('CPU: Freezing');
}
jump(position) {
console.log(`CPU: Jumping to ${position}`);
}
execute() {
console.log('CPU: Executing');
}
}
class Memory {
load(position, data) {
console.log(`Memory: Loading data at ${position}`);
}
}
class HardDrive {
read(lba, size) {
console.log(`HardDrive: Reading ${size} bytes from ${lba}`);
return 'bootloader data';
}
}
// Facade
class Computer {
constructor() {
this.cpu = new CPU();
this.memory = new Memory();
this.hardDrive = new HardDrive();
this.BOOT_ADDRESS = 0x0000;
this.BOOT_SECTOR = 0x0001;
this.SECTOR_SIZE = 512;
}
start() {
console.log('=== Starting Computer ===');
this.cpu.freeze();
this.memory.load(this.BOOT_ADDRESS,
this.hardDrive.read(this.BOOT_SECTOR, this.SECTOR_SIZE));
this.cpu.jump(this.BOOT_ADDRESS);
this.cpu.execute();
console.log('=== Computer Ready ===');
}
shutdown() {
console.log('=== Shutting Down ===');
// Complex shutdown sequence...
console.log('Computer off');
}
}
// Usage
const computer = new Computer();
computer.start();
// Much simpler than dealing with CPU, Memory, and HardDrive separately!
Real-World Facade: Video Converter
// Complex video processing library
class VideoFile {
constructor(filename) {
this.filename = filename;
this.format = filename.split('.').pop();
this.codec = null;
}
}
class CodecFactory {
static extract(file) {
console.log(`Extracting codec from ${file.filename}`);
return {
type: file.format === 'mp4' ? 'h264' : 'h265',
compression: 'lossy'
};
}
}
class BitrateReader {
static read(file, codec) {
console.log(`Reading ${file.filename} with ${codec.type} codec`);
return 'raw video data';
}
static convert(buffer, codec) {
console.log(`Converting video with ${codec.type} codec`);
return 'converted video data';
}
}
class AudioMixer {
static fix(data) {
console.log('Fixing audio...');
return data + ' with fixed audio';
}
}
class MPEG4Compression {
constructor() {
this.type = 'mp4';
this.compression = 'high';
}
}
class H264Compression {
constructor() {
this.type = 'h264';
this.compression = 'medium';
}
}
class FileSystem {
static save(data, filename) {
console.log(`Saving ${filename}`);
return { filename, size: data.length };
}
}
// Facade
class VideoConverter {
convert(filename, format) {
console.log(`\nš¬ Converting ${filename} to ${format}`);
// Complex conversion process simplified
const file = new VideoFile(filename);
const sourceCodec = CodecFactory.extract(file);
let destinationCodec;
if (format === 'mp4') {
destinationCodec = new MPEG4Compression();
} else {
destinationCodec = new H264Compression();
}
let buffer = BitrateReader.read(file, sourceCodec);
buffer = BitrateReader.convert(buffer, destinationCodec);
buffer = AudioMixer.fix(buffer);
const outputFilename = filename.split('.')[0] + '.' + format;
const result = FileSystem.save(buffer, outputFilename);
console.log(`ā
Conversion complete: ${outputFilename}\n`);
return result;
}
convertBatch(files, format) {
console.log(`š Batch converting ${files.length} files to ${format}`);
return files.map(file => this.convert(file, format));
}
}
// Usage
const converter = new VideoConverter();
// Single conversion
converter.convert('vacation.avi', 'mp4');
// Batch conversion
converter.convertBatch(
['video1.mov', 'video2.wmv', 'video3.flv'],
'mp4'
);
10. Proxy Pattern
Provides a surrogate or placeholder for another object to control access to it.
Basic Proxy
// Real subject
class Image {
constructor(filename) {
this.filename = filename;
this.loadFromDisk();
}
loadFromDisk() {
console.log(`Loading ${this.filename} from disk...`);
// Simulate expensive operation
for (let i = 0; i < 1000000; i++) {}
console.log(`${this.filename} loaded`);
}
display() {
console.log(`Displaying ${this.filename}`);
}
}
// Proxy
class ImageProxy {
constructor(filename) {
this.filename = filename;
this.image = null;
}
display() {
if (!this.image) {
this.image = new Image(this.filename);
}
this.image.display();
}
}
// Usage
const image1 = new ImageProxy('photo1.jpg');
const image2 = new ImageProxy('photo2.jpg');
// Images loaded only when displayed
image1.display(); // Loads and displays
image1.display(); // Only displays (already loaded)
image2.display(); // Loads and displays
Advanced Proxies
// 1. Protection Proxy
class BankAccount {
constructor(owner, balance = 0) {
this.owner = owner;
this.balance = balance;
}
withdraw(amount) {
if (this.balance >= amount) {
this.balance -= amount;
console.log(`Withdrew $${amount}. New balance: $${this.balance}`);
return true;
}
console.log('Insufficient funds');
return false;
}
deposit(amount) {
this.balance += amount;
console.log(`Deposited $${amount}. New balance: $${this.balance}`);
}
}
class BankAccountProxy {
constructor(account, user) {
this.account = account;
this.user = user;
this.accessLog = [];
}
withdraw(amount) {
this.logAccess('withdraw');
if (this.user !== this.account.owner) {
console.log('Access denied: Not account owner');
return false;
}
return this.account.withdraw(amount);
}
deposit(amount) {
this.logAccess('deposit');
// Anyone can deposit
this.account.deposit(amount);
}
logAccess(action) {
const logEntry = {
user: this.user,
action,
timestamp: new Date().toISOString()
};
this.accessLog.push(logEntry);
console.log('Access logged:', logEntry);
}
getAccessLog() {
// Only owner can view logs
if (this.user !== this.account.owner) {
console.log('Access denied: Cannot view logs');
return null;
}
return this.accessLog;
}
}
// 2. Virtual Proxy (for expensive resources)
class ExpensiveResource {
constructor() {
console.log('Creating expensive resource...');
this.data = this.loadHugeData();
}
loadHugeData() {
// Simulate loading huge dataset
console.log('Loading 1GB of data...');
return new Array(1000000).fill('some data');
}
query(index) {
console.log(`Querying data at index ${index}`);
return this.data[index];
}
}
class VirtualProxy {
constructor() {
this.resource = null;
this.requestCount = 0;
}
query(index) {
if (!this.resource) {
console.log('First request - initializing resource');
this.resource = new ExpensiveResource();
}
this.requestCount++;
return this.resource.query(index);
}
getStats() {
return {
initialized: !!this.resource,
requestCount: this.requestCount
};
}
}
// 3. Caching Proxy
class ApiService {
async fetchUser(id) {
console.log(`API: Fetching user ${id}...`);
await new Promise(r => setTimeout(r, 1000)); // Simulate network delay
return {
id,
name: `User ${id}`,
email: `user${id}@example.com`,
timestamp: Date.now()
};
}
}
class CachingProxy {
constructor(apiService, ttl = 60000) {
this.apiService = apiService;
this.cache = new Map();
this.ttl = ttl;
this.metrics = {
hits: 0,
misses: 0
};
}
async fetchUser(id) {
const cached = this.cache.get(id);
if (cached && Date.now() - cached.timestamp < this.ttl) {
console.log(`Cache HIT for user ${id}`);
this.metrics.hits++;
return cached.data;
}
console.log(`Cache MISS for user ${id}`);
this.metrics.misses++;
const user = await this.apiService.fetchUser(id);
this.cache.set(id, {
data: user,
timestamp: Date.now()
});
return user;
}
invalidate(id) {
this.cache.delete(id);
console.log(`Cache invalidated for user ${id}`);
}
clearCache() {
this.cache.clear();
console.log('Cache cleared');
}
getMetrics() {
const total = this.metrics.hits + this.metrics.misses;
const hitRate = total > 0 ? (this.metrics.hits / total * 100).toFixed(2) : 0;
return {
...this.metrics,
total,
hitRate: `${hitRate}%`,
cacheSize: this.cache.size
};
}
}
// 4. Logging Proxy
class LoggingProxy {
constructor(target) {
this.target = target;
}
get(target, property) {
console.log(`Property accessed: ${property}`);
return Reflect.get(this.target, property);
}
set(target, property, value) {
console.log(`Property set: ${property} = ${value}`);
return Reflect.set(this.target, property, value);
}
}
// Usage examples
async function proxyExamples() {
console.log('=== Protection Proxy ===');
const account = new BankAccount('Alice', 1000);
const proxyAccount = new BankAccountProxy(account, 'Alice');
proxyAccount.withdraw(100); // Alice can withdraw
proxyAccount.deposit(200); // Anyone can deposit
const evilProxy = new BankAccountProxy(account, 'Bob');
evilProxy.withdraw(500); // Bob can't withdraw
console.log('\n=== Virtual Proxy ===');
const virtualProxy = new VirtualProxy();
console.log('Stats:', virtualProxy.getStats());
console.log(virtualProxy.query(500)); // Initializes on first query
console.log(virtualProxy.query(600)); // Reuses existing resource
console.log('\n=== Caching Proxy ===');
const api = new CachingProxy(new ApiService());
console.log(await api.fetchUser(1)); // Cache MISS
console.log(await api.fetchUser(1)); // Cache HIT
console.log(await api.fetchUser(2)); // Cache MISS
console.log('Metrics:', api.getMetrics());
console.log('\n=== Logging Proxy ===');
const user = new LoggingProxy({ name: 'John', age: 30 });
console.log(user.name); // Logs access
user.age = 31; // Logs modification
}
// proxyExamples();
Choosing the Right Pattern
Here's a quick guide to help you choose:
| Pattern | Use When |
|---|---|
| Module | You need encapsulation and organization |
| Singleton | You need exactly one instance of a class |
| Factory | Object creation logic is complex or conditional |
| Observer | Objects need to react to state changes |
| Prototype | Object creation is expensive (cloning is cheaper) |
| Command | You need to queue, log, or undo operations |
| Strategy | You have multiple algorithms for a task |
| Decorator | You need to add responsibilities dynamically |
| Facade | You want to simplify a complex interface |
| Proxy | You need to control access to an object |
Best Practices
- Don't over-engineer: Use patterns only when they solve a real problem
- Consider JavaScript's features: Many patterns are built into the language
- Combine patterns wisely: Patterns often work together
- Keep it simple: Sometimes a simple function is all you need
- Document your intent: Make it clear why you chose a pattern
// Example of combining patterns
class Application {
constructor() {
// Singleton for configuration
this.config = Config.getInstance();
// Factory for creating components
this.componentFactory = new ComponentFactory();
// Observer for events
this.eventBus = new EventEmitter();
// Proxy for lazy loading
this.imageLoader = new ImageProxy();
}
}
Conclusion
Design patterns are powerful tools in a developer's arsenal. They:
- Provide proven solutions to common problems
- Create a shared vocabulary for developers
- Make code more maintainable and scalable
- Help avoid common pitfalls
Remember these key takeaways:
- Patterns are guidelines, not rules
- JavaScript's flexibility often provides simpler alternatives
- Understanding patterns helps you recognize them in frameworks
- Practice implementing patterns to internalize them
- Always consider if a pattern is necessary for your use case
Master these patterns, and you'll write more maintainable, scalable, and professional JavaScript code! šØ
The most important thing is to understand the problem you're solving and choose the simplest solution that works. Design patterns are tools to help you, not constraints to follow blindly.
Comments
Comments Coming Soon
Share your thoughts and feedback on this post. Comments section will be available soon.