JavaScript Design Patterns
Design patterns are reusable solutions to common problems in software design. They represent best practices evolved over time by experienced developers. In JavaScript, design patterns help create maintainable, flexible, and robust code.
Creational Patterns
Creational patterns focus on object creation mechanisms, trying to create objects in a manner suitable to the situation.
Singleton Pattern
The Singleton pattern ensures a class has only one instance and provides a global point of access to it.
// Singleton using a module pattern
const DatabaseConnection = (function() {
let instance;
function createInstance() {
// Private members
const connection = {
connect: function() {
console.log('Connected to database');
},
query: function(sql) {
console.log(`Executing query: ${sql}`);
return ['result1', 'result2'];
},
disconnect: function() {
console.log('Disconnected from database');
}
};
return connection;
}
return {
getInstance: function() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
// Usage
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true - both variables reference the same instance
db1.connect();
db1.query('SELECT * FROM users');
db2.disconnect(); // Still works because db1 and db2 reference the same object
Factory Pattern
The Factory pattern provides an interface for creating objects without specifying their concrete classes.
// Simple Factory
class UserFactory {
createUser(type) {
switch(type) {
case 'admin':
return new AdminUser();
case 'regular':
return new RegularUser();
case 'guest':
return new GuestUser();
default:
throw new Error(`User type ${type} is not recognized.`);
}
}
}
class AdminUser {
constructor() {
this.type = 'admin';
this.permissions = ['read', 'write', 'delete', 'manage'];
}
describe() {
return `Admin user with permissions: ${this.permissions.join(', ')}`;
}
}
class RegularUser {
constructor() {
this.type = 'regular';
this.permissions = ['read', 'write'];
}
describe() {
return `Regular user with permissions: ${this.permissions.join(', ')}`;
}
}
class GuestUser {
constructor() {
this.type = 'guest';
this.permissions = ['read'];
}
describe() {
return `Guest user with permissions: ${this.permissions.join(', ')}`;
}
}
// Usage
const factory = new UserFactory();
const admin = factory.createUser('admin');
const regular = factory.createUser('regular');
const guest = factory.createUser('guest');
console.log(admin.describe()); // Admin user with permissions: read, write, delete, manage
console.log(regular.describe()); // Regular user with permissions: read, write
console.log(guest.describe()); // Guest user with permissions: read
Builder Pattern
The Builder pattern separates the construction of a complex object from its representation.
// Builder pattern
class RequestBuilder {
constructor() {
this.method = 'GET';
this.url = '';
this.data = null;
this.headers = {};
this.timeout = 30000;
}
setMethod(method) {
this.method = method;
return this;
}
setURL(url) {
this.url = url;
return this;
}
setData(data) {
this.data = data;
return this;
}
setHeader(key, value) {
this.headers[key] = value;
return this;
}
setTimeout(timeout) {
this.timeout = timeout;
return this;
}
build() {
return {
method: this.method,
url: this.url,
data: this.data,
headers: this.headers,
timeout: this.timeout
};
}
}
// Usage
const request = new RequestBuilder()
.setMethod('POST')
.setURL('https://api.example.com/users')
.setData({ name: 'John', email: 'john@example.com' })
.setHeader('Content-Type', 'application/json')
.setHeader('Authorization', 'Bearer token123')
.setTimeout(5000)
.build();
console.log(request);
// {
// method: 'POST',
// url: 'https://api.example.com/users',
// data: { name: 'John', email: 'john@example.com' },
// headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer token123' },
// timeout: 5000
// }
Structural Patterns
Structural patterns deal with object composition, creating relationships between objects to form larger structures.
Module Pattern
The Module pattern encapsulates private functionality and exposes a public API.
// Module pattern
const ShoppingCart = (function() {
// Private variables and methods
let items = [];
function calculateTotal() {
return items.reduce((total, item) => total + item.price * item.quantity, 0);
}
// Public API
return {
addItem: function(item) {
items.push(item);
},
removeItem: function(id) {
items = items.filter(item => item.id !== id);
},
getItemCount: function() {
return items.length;
},
getTotal: function() {
return calculateTotal();
},
getItems: function() {
// Return a copy to prevent direct manipulation
return [...items];
}
};
})();
// Usage
ShoppingCart.addItem({ id: 1, name: 'Product 1', price: 10, quantity: 2 });
ShoppingCart.addItem({ id: 2, name: 'Product 2', price: 15, quantity: 1 });
console.log(ShoppingCart.getItemCount()); // 2
console.log(ShoppingCart.getTotal()); // 35
console.log(ShoppingCart.getItems()); // Array of items
ShoppingCart.removeItem(1);
console.log(ShoppingCart.getItemCount()); // 1
console.log(ShoppingCart.getTotal()); // 15
Decorator Pattern
The Decorator pattern attaches additional responsibilities to an object dynamically.
// Base component
class Coffee {
getCost() {
return 5;
}
getDescription() {
return 'Regular coffee';
}
}
// Decorator
class CoffeeDecorator {
constructor(coffee) {
this.coffee = coffee;
}
getCost() {
return this.coffee.getCost();
}
getDescription() {
return this.coffee.getDescription();
}
}
// Concrete decorators
class MilkDecorator extends CoffeeDecorator {
getCost() {
return this.coffee.getCost() + 1;
}
getDescription() {
return `${this.coffee.getDescription()}, with milk`;
}
}
class WhippedCreamDecorator extends CoffeeDecorator {
getCost() {
return this.coffee.getCost() + 2;
}
getDescription() {
return `${this.coffee.getDescription()}, with whipped cream`;
}
}
class CaramelDecorator extends CoffeeDecorator {
getCost() {
return this.coffee.getCost() + 3;
}
getDescription() {
return `${this.coffee.getDescription()}, with caramel`;
}
}
// Usage
let coffee = new Coffee();
console.log(coffee.getDescription()); // Regular coffee
console.log(coffee.getCost()); // 5
coffee = new MilkDecorator(coffee);
console.log(coffee.getDescription()); // Regular coffee, with milk
console.log(coffee.getCost()); // 6
coffee = new WhippedCreamDecorator(coffee);
console.log(coffee.getDescription()); // Regular coffee, with milk, with whipped cream
console.log(coffee.getCost()); // 8
coffee = new CaramelDecorator(coffee);
console.log(coffee.getDescription()); // Regular coffee, with milk, with whipped cream, with caramel
console.log(coffee.getCost()); // 11
Facade Pattern
The Facade pattern provides a simplified interface to a complex subsystem.
// Complex subsystem components
class AudioPlayer {
turnOn() {
console.log('Audio player turned on');
}
setVolume(level) {
console.log(`Volume set to ${level}`);
}
setSource(source) {
console.log(`Source set to ${source}`);
}
play() {
console.log('Playing audio');
}
}
class Projector {
turnOn() {
console.log('Projector turned on');
}
setInput(input) {
console.log(`Input set to ${input}`);
}
setAspectRatio(ratio) {
console.log(`Aspect ratio set to ${ratio}`);
}
}
class Lights {
dim(level) {
console.log(`Lights dimmed to ${level}%`);
}
}
class Screen {
lower() {
console.log('Screen lowered');
}
}
// Facade
class HomeTheaterFacade {
constructor() {
this.audioPlayer = new AudioPlayer();
this.projector = new Projector();
this.lights = new Lights();
this.screen = new Screen();
}
watchMovie() {
console.log('Get ready to watch a movie...');
this.lights.dim(10);
this.screen.lower();
this.projector.turnOn();
this.projector.setInput('HDMI');
this.projector.setAspectRatio('16:9');
this.audioPlayer.turnOn();
this.audioPlayer.setVolume(50);
this.audioPlayer.setSource('Surround');
this.audioPlayer.play();
}
endMovie() {
console.log('Shutting down the movie...');
this.lights.dim(100);
this.screen.lower();
this.projector.turnOn();
this.audioPlayer.turnOn();
}
}
// Usage
const homeTheater = new HomeTheaterFacade();
homeTheater.watchMovie();
// Get ready to watch a movie...
// Lights dimmed to 10%
// Screen lowered
// Projector turned on
// Input set to HDMI
// Aspect ratio set to 16:9
// Audio player turned on
// Volume set to 50
// Source set to Surround
// Playing audio
Behavioral Patterns
Behavioral patterns focus on communication between objects, how objects interact and distribute responsibility.
Observer Pattern
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
// Subject (Observable)
class NewsPublisher {
constructor() {
this.subscribers = [];
this.latestNews = null;
}
subscribe(observer) {
this.subscribers.push(observer);
}
unsubscribe(observer) {
this.subscribers = this.subscribers.filter(subscriber => subscriber !== observer);
}
notify() {
this.subscribers.forEach(subscriber => subscriber.update(this.latestNews));
}
publishNews(news) {
this.latestNews = news;
this.notify();
}
}
// Observer
class NewsSubscriber {
constructor(name) {
this.name = name;
}
update(news) {
console.log(`${this.name} received news: ${news}`);
}
}
// Usage
const publisher = new NewsPublisher();
const subscriber1 = new NewsSubscriber('John');
const subscriber2 = new NewsSubscriber('Alice');
const subscriber3 = new NewsSubscriber('Bob');
publisher.subscribe(subscriber1);
publisher.subscribe(subscriber2);
publisher.subscribe(subscriber3);
publisher.publishNews('Breaking news: JavaScript is awesome!');
// John received news: Breaking news: JavaScript is awesome!
// Alice received news: Breaking news: JavaScript is awesome!
// Bob received news: Breaking news: JavaScript is awesome!
publisher.unsubscribe(subscriber2);
publisher.publishNews('More news: New JavaScript framework released!');
// John received news: More news: New JavaScript framework released!
// Bob received news: More news: New JavaScript framework released!
Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.
// Strategy interface (implicit in JavaScript)
// Each strategy should have a calculate method
// Concrete strategies
class RegularPricingStrategy {
calculate(amount) {
return amount;
}
}
class PremiumPricingStrategy {
calculate(amount) {
return amount * 0.9; // 10% discount
}
}
class GoldPricingStrategy {
calculate(amount) {
return amount * 0.7; // 30% discount
}
}
// Context
class ShoppingCart {
constructor(pricingStrategy) {
this.pricingStrategy = pricingStrategy;
this.items = [];
}
addItem(item) {
this.items.push(item);
}
setPricingStrategy(pricingStrategy) {
this.pricingStrategy = pricingStrategy;
}
calculateTotal() {
const subtotal = this.items.reduce((total, item) => total + item.price * item.quantity, 0);
return this.pricingStrategy.calculate(subtotal);
}
}
// Usage
const regularCart = new ShoppingCart(new RegularPricingStrategy());
regularCart.addItem({ name: 'Product 1', price: 100, quantity: 1 });
regularCart.addItem({ name: 'Product 2', price: 50, quantity: 2 });
console.log('Regular customer total:', regularCart.calculateTotal()); // 200
// Change strategy for premium customer
regularCart.setPricingStrategy(new PremiumPricingStrategy());
console.log('Premium customer total:', regularCart.calculateTotal()); // 180
// Change strategy for gold customer
regularCart.setPricingStrategy(new GoldPricingStrategy());
console.log('Gold customer total:', regularCart.calculateTotal()); // 140
Command Pattern
The Command pattern encapsulates a request as an object, allowing for parameterization of clients with different requests, queuing of requests, and logging of operations.
// Receiver
class Light {
turnOn() {
console.log('Light turned on');
}
turnOff() {
console.log('Light turned off');
}
}
// Command interface (implicit in JavaScript)
// Each command should have an execute method
// Concrete commands
class TurnOnCommand {
constructor(light) {
this.light = light;
}
execute() {
this.light.turnOn();
}
}
class TurnOffCommand {
constructor(light) {
this.light = light;
}
execute() {
this.light.turnOff();
}
}
// Invoker
class RemoteControl {
constructor() {
this.commands = {};
}
setCommand(buttonName, command) {
this.commands[buttonName] = command;
}
pressButton(buttonName) {
if (this.commands[buttonName]) {
this.commands[buttonName].execute();
} else {
console.log(`Button ${buttonName} not programmed`);
}
}
}
// Usage
const light = new Light();
const turnOnCommand = new TurnOnCommand(light);
const turnOffCommand = new TurnOffCommand(light);
const remote = new RemoteControl();
remote.setCommand('on', turnOnCommand);
remote.setCommand('off', turnOffCommand);
remote.pressButton('on'); // Light turned on
remote.pressButton('off'); // Light turned off
remote.pressButton('dim'); // Button dim not programmed
Modern JavaScript Design Patterns
Modern JavaScript introduces new patterns and variations of traditional patterns.
Module Pattern with ES Modules
// file: cart.js
// Private variables and functions
let items = [];
function calculateTotal() {
return items.reduce((total, item) => total + item.price * item.quantity, 0);
}
// Public API
export function addItem(item) {
items.push(item);
}
export function removeItem(id) {
items = items.filter(item => item.id !== id);
}
export function getItemCount() {
return items.length;
}
export function getTotal() {
return calculateTotal();
}
export function getItems() {
return [...items];
}
// Usage in another file:
// import { addItem, removeItem, getTotal, getItems } from './cart.js';
//
// addItem({ id: 1, name: 'Product 1', price: 10, quantity: 2 });
// console.log(getTotal()); // 20
Provider Pattern
The Provider pattern is commonly used in modern JavaScript frameworks to share state across components.
// Simple implementation of a Provider pattern
class ThemeContext {
constructor(initialTheme = 'light') {
this.theme = initialTheme;
this.listeners = [];
}
getTheme() {
return this.theme;
}
setTheme(newTheme) {
this.theme = newTheme;
this.notifyListeners();
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
notifyListeners() {
this.listeners.forEach(listener => listener(this.theme));
}
}
// Usage
const themeContext = new ThemeContext();
// Component 1
function Header() {
const unsubscribe = themeContext.subscribe(theme => {
console.log(`Header: Theme changed to ${theme}`);
// Update UI based on theme
});
// Call unsubscribe when component is destroyed
}
// Component 2
function Sidebar() {
const unsubscribe = themeContext.subscribe(theme => {
console.log(`Sidebar: Theme changed to ${theme}`);
// Update UI based on theme
});
// Call unsubscribe when component is destroyed
}
// Initialize components
Header();
Sidebar();
// Change theme
themeContext.setTheme('dark');
// Header: Theme changed to dark
// Sidebar: Theme changed to dark
Best Practices
- Choose the right pattern: Select patterns based on your specific problem, not just because they're popular
- Keep it simple: Don't over-engineer your solution with unnecessary patterns
- Document your patterns: Make sure other developers understand why and how you're using a particular pattern
- Consider performance: Some patterns can introduce overhead, so evaluate their impact
- Adapt patterns: Modify patterns to fit your specific needs rather than forcing your code to fit a pattern
When to Use Design Patterns
- Singleton: When exactly one instance of a class is needed
- Factory: When object creation logic should be separate from the client code
- Builder: When constructing complex objects step by step
- Module: When you need to encapsulate private functionality
- Decorator: When you need to add responsibilities to objects dynamically
- Facade: When you need to provide a simple interface to a complex subsystem
- Observer: When objects need to be notified of changes to other objects
- Strategy: When you have multiple algorithms that can be interchanged
- Command: When you need to parameterize objects with operations
Next Steps
Now that you understand design patterns in JavaScript, you might want to explore:
- Implementing design patterns in TypeScript
- Applying design patterns in frameworks like React, Vue, or Angular
- Exploring architectural patterns like MVC, MVVM, and Flux
- Learning anti-patterns to avoid in JavaScript