5 JavaScript Design Patterns You Should Know

Emma Delaney
4 min readJul 9, 2024

--

As a JavaScript developer, learning design patterns means you have a set of tools that provide proven solutions to common programming problems. Design patterns help you write clean, efficient, and maintainable code by providing a structured approach to solving various challenges. In this blog post, we’ll explore five key design patterns that every JavaScript developer should be aware of and provide practical examples of each.

Singleton Pattern

The Singleton pattern ensures that there is only one instance of a class and provides a global entry point for that instance. This is very useful if you want to limit the existence of a class to a single object.

Problem: You need to create a logger that can be accessed from anywhere in your application, but you only need one logger instance.

Solution:

// Singleton pattern implementation
class Logger {
private static instance: Logger;

private constructor() {}

public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}

public log(message: string): void {
console.log(`[${new Date().toISOString()}] ${message}`);
}
}

// Usage
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();

logger1.log("Hello, world!"); // [2023-02-20T14:30:00.000Z] Hello, world!
logger2.log("Hello again!"); // [2023-02-20T14:30:00.000Z] Hello again!

console.log(logger1 === logger2); // true

Factory Pattern

The factory pattern provides an interface for creating objects without specifying the exact class of the object being created. This is useful when you want to create an object based on certain conditions or parameters.

Problem: Build different types of vehicles (eg cars, trucks, motorcycles) without revealing the underlying build logic.

Solution:

// Factory pattern implementation
interface Vehicle {
drive(): void;
}

class Car implements Vehicle {
drive(): void {
console.log("Driving a car...");
}
}

class Truck implements Vehicle {
drive(): void {
console.log("Driving a truck...");
}
}

class Motorcycle implements Vehicle {
drive(): void {
console.log("Driving a motorcycle...");
}
}

class VehicleFactory {
public static createVehicle(type: string): Vehicle {
switch (type) {
case "car":
return new Car();
case "truck":
return new Truck();
case "motorcycle":
return new Motorcycle();
default:
throw new Error(`Unknown vehicle type: ${type}`);
}
}
}

// Usage
const car = VehicleFactory.createVehicle("car");
const truck = VehicleFactory.createVehicle("truck");
const motorcycle = VehicleFactory.createVehicle("motorcycle");

car.drive(); // Driving a car...
truck.drive(); // Driving a truck...
motorcycle.drive(); // Driving a motorcycle...

3 . Observer Pattern

The Observer pattern defines a one-to-many relationship between objects. When the state of an object (entity) changes, all dependent objects (observers) are notified and updated automatically.

Problem: When one object changes, multiple objects need to be updated.

Solution:

// Observer pattern implementation
interface Observer {
update(data: any): void;
}

class Subject {
private observers: Observer[] = [];
private data: any;

public addObserver(observer: Observer): void {
this.observers.push(observer);
}

public removeObserver(observer: Observer): void {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}

public notifyObservers(): void {
this.observers.forEach((observer) => observer.update(this.data));
}

public setData(data: any): void {
this.data = data;
this.notifyObservers();
}
}

class ConcreteObserver implements Observer {
public update(data: any): void {
console.log(`Received update: ${data}`);
}
}

// Usage
const subject = new Subject();
const observer1 = new ConcreteObserver();
const observer2 = new ConcreteObserver();

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.setData("Hello, world!"); // Received update: Hello, world!

Decorator Pattern

The decorator pattern allows you to add behaviour to individual objects without affecting the behaviour of other objects in the same class. This is a flexible alternative to sub-classing.

Problem: You need to add logging functionality to an existing object without changing its implementation.

Solution:

class Coffee {
cost() {
return 5;
}
}

class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}

cost() {
return this.coffee.cost() + 2;
}
}

class SugarDecorator {
constructor(coffee) {
this.coffee = coffee;
}

cost() {
return this.coffee.cost() + 1;
}
}

let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);

console.log(coffee.cost()); // Output: 8

5. Module Pattern

Module provides the ability to create embedded, reusable components in JavaScript. This ensures clean code and avoids global namespace pollution.

const CalculatorModule = (function () {
let result = 0;

function add(a, b) {
result = a + b;
}

function subtract(a, b) {
result = a - b;
}

function getResult() {
return result;
}

return {
add,
subtract,
getResult
};
})();

CalculatorModule.add(5, 3);
console.log(CalculatorModule.getResult()); // Output: 8

After understanding five design patterns: Singleton, Factory, Observer, Decorator and Module can significantly improve your ability to write clean, maintainable and efficient JavaScript code. By integrating these patterns into your developer toolset, you’ll be well equipped to tackle a variety of programming challenges elegantly and efficiently.

--

--