Design Patterns in JavaScript


Introduction

In software development, design patterns are repeatable and tested solutions to common problems. Using the right design patterns in a flexible and dynamic language like JavaScript enhances code readability, maintainability, and scalability.

In this comprehensive guide, we will explore the most common design patterns in JavaScript, explain each with example codes, and discuss when to use them.


1. Creational Design Patterns

1.1 Singleton Pattern

Purpose: Ensure that only one instance of a class is created and provide a global access point to it.

const Singleton = (function () {
  let instance;

  function createInstance() {
    return { name: "Singleton Instance" };
  }

  return {
    getInstance: function () {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    }
  };
})();

// Usage
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 === instance2); // true

When to Use?

  • When a single global instance is required (like a configuration manager).

1.2 Factory Pattern

Purpose: Abstract the process of object creation to dynamically determine which class instance should be created.

class Car {
  constructor() {
    this.type = 'Car';
  }
}

class Truck {
  constructor() {
    this.type = 'Truck';
  }
}

class VehicleFactory {
  static createVehicle(type) {
    switch (type) {
      case 'car':
        return new Car();
      case 'truck':
        return new Truck();
      default:
        throw new Error('Invalid vehicle type');
    }
  }
}

const myCar = VehicleFactory.createVehicle('car');
console.log(myCar.type); // Car

When to Use?

  • When the object creation process needs to be abstracted.

2. Structural Design Patterns

2.1 Module Pattern

Purpose: Encapsulate variables and functions, avoiding pollution of the global scope.

const Module = (function () {
  let privateVar = 'Private Data';

  return {
    getPrivateVar: function () {
      return privateVar;
    },
    setPrivateVar: function (value) {
      privateVar = value;
    }
  };
})();

console.log(Module.getPrivateVar()); // Private Data
Module.setPrivateVar('New Data');
console.log(Module.getPrivateVar()); // New Data

When to Use?

  • To prevent global scope pollution.

2.2 Decorator Pattern

Purpose: Dynamically extend the behavior of an existing object without subclassing.

function car() {
  this.cost = function () {
    return 20000;
  };
}

function sunroof(carInstance) {
  const originalCost = carInstance.cost();
  carInstance.cost = function () {
    return originalCost + 1500;
  };
}

const myCar = new car();
sunroof(myCar);
console.log(myCar.cost()); // 21500

When to Use?

  • When an object's behavior needs to be changed dynamically.

3. Behavioral Design Patterns

3.1 Observer Pattern

Purpose: Automatically notify other objects of any state changes.

class Subject {
  constructor() {
    this.observers = [];
  }

  subscribe(observer) {
    this.observers.push(observer);
  }

  unsubscribe(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  update(data) {
    console.log(`Observer received data: ${data}`);
  }
}

const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.subscribe(observer1);
subject.subscribe(observer2);

subject.notify('New Data'); // Both observers receive the notification.

When to Use?

  • When one object’s change should be automatically notified to others.

3.2 Strategy Pattern

Purpose: Enable algorithms to be interchangeable.

class Shipping {
  setStrategy(strategy) {
    this.strategy = strategy;
  }

  calculate(package) {
    return this.strategy.calculate(package);
  }
}

class FedEx {
  calculate(package) {
    return package.weight * 10;
  }
}

class UPS {
  calculate(package) {
    return package.weight * 12;
  }
}

const package = { weight: 5 };
const shipping = new Shipping();

shipping.setStrategy(new FedEx());
console.log(shipping.calculate(package)); // 50

shipping.setStrategy(new UPS());
console.log(shipping.calculate(package)); // 60

When to Use?

  • When you need to select an algorithm dynamically.

4. Tips for Using Design Patterns in JavaScript

  • Follow the DRY Principle: Avoid code repetition.
  • Ensure Encapsulation: Avoid unnecessary external access.
  • Think Modular: Design each pattern as an independent module.
  • Write Testable Code: Create appropriate units for each design pattern.

Conclusion

Using design patterns in JavaScript improves the maintainability and scalability of your applications. Applying the right patterns in the right context allows you to write cleaner, more maintainable, and professional code.

The design patterns we covered in this guide will provide a solid foundation for writing more effective code in your JavaScript projects.


Which is Your Favorite Design Pattern?

Share in the comments! ????