Master the Top 5 Essential JavaScript Design Patterns Every Developer Should Know

Learn Javascript with Nick

1. Singleton Pattern

What is it?

A design pattern that restricts the instantiation of a class to a single instance.

When was it created?

Introduced as part of the GoF (Gang of Four) design patterns in 1994.

Node.js Support:

All versions of Node.js.

Why use it?

Prevents multiple instances and manages global state efficiently.

Best Practices:

• Use it for shared configurations or resource-heavy classes.

Example:

class Singleton {
  static instance;
  constructor() {
    if (!Singleton.instance) {
      Singleton.instance = this;
    }
    return Singleton.instance;
  }
}
const singletonA = new Singleton();
const singletonB = new Singleton();
console.log(singletonA === singletonB); // true

Pros:

• Global access point.

• Easy to manage shared state.

Cons:

• Harder to test and refactor due to global reliance.

2. Factory Pattern

What is it?

Encapsulates object creation logic, returning objects from a shared interface.

When was it created?

Another GoF pattern from the 1990s.

Node.js Support:

Supported across all versions.

Why use it?

Simplifies complex object creation.

Best Practices:

• Useful when dealing with large-scale applications needing multiple object types.

Example:

class Car {
  constructor(model) { this.model = model; }
}
class CarFactory {
  createCar(type) {
    switch(type) {
      case 'sedan': return new Car('Sedan');
      case 'suv': return new Car('SUV');
      default: return null;
    }
  }
}
const factory = new CarFactory();
const sedan = factory.createCar('sedan');
console.log(sedan.model); // 'Sedan'

Pros:

• Encapsulates object creation logic.

• Supports easy object extension.

Cons:

• Overhead for simple objects.

3. Observer Pattern

What is it?

Allows one object (subject) to notify observers about state changes.

When was it created?

First formalized in the 1970s; adopted into JS in event-driven systems.

Node.js Support:

Supported by event-driven architecture.

Why use it?

Ideal for decoupling objects in event-based systems.

Best Practices:

• Use it in pub/sub messaging systems.

Example:

class Observer {
  update(data) { console.log(`Observer received: ${data}`); }
}
class Subject {
  constructor() { this.observers = []; }
  addObserver(observer) { this.observers.push(observer); }
  notify(data) { this.observers.forEach(o => o.update(data)); }
}
const subject = new Subject();
const observer = new Observer();
subject.addObserver(observer);
subject.notify('Event Fired'); // Observer received: Event Fired

Pros:

• Promotes loose coupling.

Cons:

• Can be complex to manage with many observers.

4. Strategy Pattern

What is it?

Encapsulates algorithms and allows them to be interchangeable within a class.

When was it created?

Part of GoF’s 1994 design patterns.

Node.js Support:

Supported in all modern versions.

Why use it?

Allows flexibility by changing algorithms dynamically.

Best Practices:

• Ideal for scenarios requiring multiple approaches to the same problem.

Example:

class Context {
  setStrategy(strategy) { this.strategy = strategy; }
  executeStrategy(a, b) { return this.strategy.doOperation(a, b); }
}
class Add {
  doOperation(a, b) { return a + b; }
}
class Subtract {
  doOperation(a, b) { return a - b; }
}
const context = new Context();
context.setStrategy(new Add());
console.log(context.executeStrategy(5, 3)); // 8
context.setStrategy(new Subtract());
console.log(context.executeStrategy(5, 3)); // 2

Pros:

• Easy to switch algorithms.

Cons:

• Increases class complexity.

5. Decorator Pattern

What is it?

Dynamically adds responsibilities to objects.

When was it created?

From GoF’s pattern library.

Node.js Support:

Supported in all versions with ES6 classes.

Why use it?

Provides flexible object functionality without subclassing.

Best Practices:

• Use it for adding extra features or responsibilities to objects.

Example:

class Car {
  getDescription() { return 'Car'; }
}
class SportsCarDecorator {
  constructor(car) { this.car = car; }
  getDescription() { return `${this.car.getDescription()} with sports package`; }
}
const car = new Car();
const sportsCar = new SportsCarDecorator(car);
console.log(sportsCar.getDescription()); // 'Car with sports package'

Pros:

• Extends functionality dynamically.

Cons:

• Can make the code harder to read if overused.