Design patterns are proven solutions to common problems that arise in software design. They provide a reusable template to address challenges that occur frequently during the development process. In this article, we’ll explore popular design patterns, explain their purpose, and provide examples in JavaScript.

Singleton

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This is useful when you need a single object to coordinate actions across a system.

class Singleton {
  static instance;

  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }

    Singleton.instance = this;
  }
}

const instance1 = new Singleton();
const instance2 = new Singleton();

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

Factory Method

The Factory Method pattern defines an interface for creating an object, but allows subclasses to decide which class to instantiate. It promotes loose coupling by eliminating the need to bind application-specific classes into the code.

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

class Car {
  drive() {
    console.log("Driving a car");
  }
}

class Truck {
  drive() {
    console.log("Driving a truck");
  }
}

const factory = new VehicleFactory();
const car = factory.createVehicle("car");
car.drive(); // "Driving a car"

Observer

The Observer pattern defines a one-to-many dependency between objects, allowing an object to notify its dependents automatically when its state changes.

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

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

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

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

class Observer {
  update(data) {
    console.log("Data received:", data);
  }
}

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

subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notify("New data!"); // "Data received: New data!" x2

Strategy

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. This allows the algorithm to vary independently from clients that use it.

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

  calculateCost(weight) {
    return this.strategy.calculate(weight);
  }
}

class UPS {
  calculate(weight) {
    return weight * 2;
  }
}

class FedEx {
  calculate(weight) {
    return weight * 1.5;
  }
}

const ups = new UPS();
const fedex = new FedEx();

const shipping = new Shipping(ups);
console.log(shipping.calculateCost(10)); // 20

shipping.strategy = fedex;
console.log(shipping.calculateCost(10)); // 15

Decorator

The Decorator pattern dynamically adds responsibilities to objects without affecting the behavior of other objects from the same class. It’s a structural pattern that involves a set of decorator classes that mirror the type of the objects they extend, but add or override behavior.

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

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

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

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

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

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

console.log(coffee.cost()); // 6.5

Adapter

The Adapter pattern allows incompatible interfaces to work together by converting the interface of one class into another interface that the client expects. It’s often used to make existing classes work with others without modifying their source code.

class OldAPI {
  fetchData() {
    return "data from old API";
  }
}

class NewAPI {
  getData() {
    return "data from new API";
  }
}

class Adapter {
  constructor(oldAPI) {
    this.oldAPI = oldAPI;
  }

  getData() {
    return this.oldAPI.fetchData();
  }
}

const oldAPI = new OldAPI();
const adapter = new Adapter(oldAPI);

console.log(adapter.getData()); // "data from old API"

Command

The Command pattern encapsulates a request as an object, allowing clients to parameterize other objects with different requests, queue or log requests, and support undoable operations.

class Invoker {
  constructor() {
    this.commands = [];
  }

  execute(command) {
    this.commands.push(command);
    command.execute();
  }

  undo() {
    const command = this.commands.pop();
    if (command) {
      command.undo();
    }
  }
}

class Command {
  constructor(receiver) {
    this.receiver = receiver;
  }

  execute() {
    this.receiver.action();
  }

  undo() {
    this.receiver.reverseAction();
  }
}

class Receiver {
  action() {
    console.log("Action executed");
  }

  reverseAction() {
    console.log("Action reversed");
  }
}

const receiver = new Receiver();
const command = new Command(receiver);
const invoker = new Invoker();

invoker.execute(command); // "Action executed"
invoker.undo(); // "Action reversed"

Builder

The Builder pattern is a creational pattern that separates the construction of a complex object from its representation. It allows the same construction process to create different representations. This pattern is particularly useful when an object has many optional properties or requires a multi-step process to create.

class Car {
  constructor() {
    this.make = "";
    this.model = "";
    this.color = "";
  }

  setMake(make) {
    this.make = make;
  }

  setModel(model) {
    this.model = model;
  }

  setColor(color) {
    this.color = color;
  }
}

class CarBuilder {
  constructor() {
    this.car = new Car();
  }

  setMake(make) {
    this.car.setMake(make);
    return this;
  }

  setModel(model) {
    this.car.setModel(model);
    return this;
  }

  setColor(color) {
    this.car.setColor(color);
    return this;
  }

  build() {
    return this.car;
  }
}

const carBuilder = new CarBuilder();
const car = carBuilder
  .setMake("Toyota")
  .setModel("Corolla")
  .setColor("Red")
  .build();

console.log(car); // Car { make: 'Toyota', model: 'Corolla', color: 'Red' }

These are just a few examples of popular design patterns. Understanding and implementing design patterns can help you write cleaner, more efficient, and maintainable code. Practice using these patterns and explore additional patterns to further enhance your programming skills.