Design patterns in JavaScript

Created: Feb 20th 2023 - Updated : Feb 20th 2023

Introduction

Web developers are always looking for new design patterns to use in their projects. These days, JavaScript is one of the most popular languages for web development, so it should come as no surprise that there are many different design patterns that can be used with this language. In this article, we will go over some basic types of design patterns and how you might use them in your own projects:

Abstract class

An abstract class is a class that cannot be instantiated. It's used to define the interface of a group of classes, but it doesn't have any implementation details. Abstract classes are also useful for defining common methods and properties that subclasses should share.

For example, let's say you have an Animal class with methods like speak() and eat(). You might want all animals to have these behaviors so you make them part of the Animal interface (abstract class). Then when making more specific types of animals, such as dogs or cats, those new classes could inherit from Animal instead of having their own implementations for those methods--they would just need to add any additional functionality they needed within their own subclass(es).

class Animal {
    constructor(name) {
        this.name = name
    }

    speak() {
        console.log(`${this.name} starts making noises`)
    }

    eat() {
        console.log(`${this.name} starts eating.`)
    }

    run() {
        console.log(`${this.name} starts running.`)
    }
}

let myAnimal = new Animal('Filo')

myAnimal.speak() // Filo starts making noises

class Rabbit extends Animal{
    hide() {
        console.log(`${this.name} hides!`)
    }
}

let myRabbit = new Rabbit('Lilo')

myRabbit.hide() // Lilo hides!

Adapter pattern

The adapter pattern is a design pattern that allows you to wrap an existing class with another class and thus change the interface of that class. This is done so that you can provide a common interface to clients, who will not need to know about the difference between classes. For example, if you have an existing class named car used throughout your application, but after some time you have to refactor a part of it and the new data schema is no longer compatible with the existing class, you can use the Adapter Pattern.

// The Target interface that the Adapter will conform to
class CarInterface {
    startEngine() {
      throw new Error("startEngine() must be implemented by a concrete adapter");
    }
  }
  
  // The Adaptee that needs to be adapted to the Target interface
  class OldCar {
    ignite() {
      return "Igniting the old car's engine.";
    }
  }
  
  // The Adapter that adapts the Adaptee to the Target interface
  class CarAdapter extends CarInterface {
    constructor(oldCar) {
      super();
      this.oldCar = oldCar;
    }
    
    startEngine() {
      return this.oldCar.ignite();
    }
  }
  
  // Usage example
  const oldCar = new OldCar();
  const carAdapter = new CarAdapter(oldCar);
  
  console.log(carAdapter.startEngine()); // Output: Igniting the old car's engine.

In this example, the CarInterface defines the method that the client expects to call, which is startEngine(). The OldCar class is the class that the client wants to use, but it has a different method than the CarInterface. The CarAdapter class adapts the OldCar to the CarInterface by implementing the startEngine method of the CarInterface, and using the ignite method of the OldCar to fulfill the client's request.

You can also create a Car class that implements the CarInterface, and use the CarAdapter to adapt the OldCar to the CarInterface as follows:

class Car extends CarInterface {
  startEngine() {
    return "Starting the car's engine.";
  }
}

// Usage example
const car = new Car();
const carAdapter = new CarAdapter(new OldCar());

console.log(car.startEngine()); // Output: Starting the car's engine.
console.log(carAdapter.startEngine()); // Output: Igniting the old car's engine.

Builder pattern

The Builder pattern is one of the twenty-three well-known Gangs of Four (GoF) Design Patterns that describes how to build objects in steps.

Using the Builder pattern in JavaScript can make your code more modular and reusable. It allows you to create complex objects step by step, and gives you the flexibility to change the construction process without affecting the client code that uses the builder.

It is useful when you need to create complex objects that have multiple parts, and you want to avoid having a complex constructor function or many constructor parameters.

The Builder pattern is implemented using classes and methods and usually consists of four components:

  1. Product: The complex object that the Builder will construct.
  2. Builder: An interface that defines the steps for building the product.
  3. Concrete Builder: A class that implements the Builder interface to construct a specific type of product.
  4. Director: A class that uses the builder to construct the product.
// The Product class that the Builder will construct
class Car {
  constructor() {
    this.engine = "";
    this.seats = "";
    this.wheels = "";
  }
  
  setEngine(engine) {
    this.engine = engine;
  }
  
  setSeats(seats) {
    this.seats = seats;
  }
  
  setWheels(wheels) {
    this.wheels = wheels;
  }
  
  getInfo() {
    return `This car has a ${this.engine} engine, ${this.seats} seats, and ${this.wheels} wheels.`;
  }
}

// The Builder interface that defines the steps for building the product
class CarBuilder {
  constructor() {
    this.car = new Car();
  }
  
  setEngine(engine) {}
  
  setSeats(seats) {}
  
  setWheels(wheels) {}
  
  getCar() {}
}

// A concrete builder that implements the Builder interface to construct a specific type of product
class SportCarBuilder extends CarBuilder {
  setEngine(engine) {
    this.car.setEngine(engine);
  }
  
  setSeats(seats) {
    this.car.setSeats(seats);
  }
  
  setWheels(wheels) {
    this.car.setWheels(wheels);
  }
  
  getCar() {
    return this.car;
  }
}

// A director that uses the builder to construct the product
class CarDirector {
  constructor(builder) {
    this.builder = builder;
  }
  
  construct() {
    this.builder.setEngine("V8");
    this.builder.setSeats("2");
    this.builder.setWheels("4");
    return this.builder.getCar();
  }
}

// Usage example
const sportCarBuilder = new SportCarBuilder();
const carDirector = new CarDirector(sportCarBuilder);
const sportCar = carDirector.construct();

console.log(sportCar.getInfo()); // Output: This car has a V8 engine, 2 seats, and 4 wheels.

Composite pattern

The Composite pattern is a structural design pattern that allows you to compose objects into tree structures and then work with these structures as if they were individual objects. It is useful when you have a hierarchy of objects that can be treated as a group or as individual objects, and you want to perform the same operation on each object in the hierarchy.

In JavaScript, the Composite pattern can be implemented using classes and objects. The pattern usually consists of two main components:

Component: An interface or abstract class that defines the common methods that can be performed on the group of objects. Composite: A class that represents the group of objects and implements the Component interface. It contains a collection of child components and can perform the same operation on each child component. Here's an example of how the Composite pattern can be implemented in JavaScript to represent a tree structure:

class TreeNode {
  constructor(name) {
    this.name = name;
    this.children = [];
  }

  add(child) {
    this.children.push(child);
  }

  remove(child) {
    const index = this.children.indexOf(child);
    if (index !== -1) {
      this.children.splice(index, 1);
    }
  }

  getChild(index) {
    return this.children[index];
  }

  getName() {
    return this.name;
  }

  display() {
    console.log(this.name);
    this.children.forEach(child => child.display());
  }
}

// Usage example
const root = new TreeNode("Root");
const child1 = new TreeNode("Child 1");
const child2 = new TreeNode("Child 2");
const child3 = new TreeNode("Child 3");
root.add(child1);
root.add(child2);
child2.add(child3);
root.display(); // Output: Root Child 1 Child 2 Child 3

In this example, the TreeNode class represents the Component and Composite in the Composite pattern. Each TreeNode can have child TreeNode objects, and can perform the same operation (i.e. display) on each child component recursively.

You can use the Composite pattern to create any tree-like structures, such as directory structures, menu structures, or nested lists, and perform the same operation on each node in the tree.

Decorator pattern

The decorator pattern is used to add new functionality to existing objects without modifying the original object. It's a great way to extend an existing class and customize it with additional features that you need.

The decorator pattern can be implemented using objects and functions. The pattern usually consists of three main components:

  • Component: An interface or abstract class that defines the common methods that can be performed on the object and the decorators.
  • Concrete Component: A class that implements the Component interface and provides the basic functionality.
  • Decorator: A class that implements the Component interface and wraps the Concrete Component. It adds or modifies the behavior of the Concrete Component.

Here's an example of how the Decorator pattern can be implemented in JavaScript:


class Car {
  constructor() {
    this.description = "Basic car";
  }

  cost() {
    return 20000;
  }

  getDescription() {
    return this.description;
  }
}

class Decorator {
  constructor(car) {
    this.car = car;
  }

  cost() {
    return this.car.cost();
  }

  getDescription() {
    return this.car.getDescription();
  }
}

class LeatherSeats extends Decorator {
  constructor(car) {
    super(car);
    this.description = "Leather seats";
  }

  cost() {
    return this.car.cost() + 3000;
  }

  getDescription() {
    return `${this.car.getDescription()}, ${this.description}`;
  }
}

class AlloyWheels extends Decorator {
  constructor(car) {
    super(car);
    this.description = "Alloy wheels";
  }

  cost() {
    return this.car.cost() + 2000;
  }

  getDescription() {
    return `${this.car.getDescription()}, ${this.description}`;
  }
}

// Usage example
let myCar = new Car();
myCar = new LeatherSeats(myCar);
myCar = new AlloyWheels(myCar);
console.log(myCar.getDescription()); // Output: Basic car, Leather seats, Alloy wheels
console.log(myCar.cost()); // Output: 25000

In this example, the Car class represents the Concrete Component, and the LeatherSeats and AlloyWheels classes represent the Decorators. The Decorator class is an abstract class that extends the Component and serves as a base for all Concrete Decorators. The Decorators wrap the Car object and add new functionality to it. This allows us not only to keep our code DRY but it also makes our code easily extendable without having too much knowledge about how everything works behind-the-scenes since most changes will happen here rather than inside each subclass individually.

Facade pattern

The facade pattern is a way to hide complex details from the user, while still allowing them to interact with it. This can be done by providing a simplified interface that hides the actual structure behind it.

The main advantage of this pattern is that it allows you to change your code without affecting other parts of your application. For example, if we have an object that holds information about different users in our system (like name, email address etc.), then we can change its structure without affecting any other part of our codebase because all interaction between those objects happens through one single place: The Facade class!

here is an example implementing the Facade pattern:

class Bank {
  constructor() {
    this.balance = 0;
  }

  deposit(amount) {
    this.balance += amount;
  }

  withdraw(amount) {
    if (this.balance >= amount) {
      this.balance -= amount;
    }
  }

  getBalance() {
    return this.balance;
  }
}

class Customer {
  constructor(name) {
    this.name = name;
    this.bank = new Bank();
  }

  deposit(amount) {
    this.bank.deposit(amount);
  }

  withdraw(amount) {
    this.bank.withdraw(amount);
  }

  getBalance() {
    return this.bank.getBalance();
  }
}

// Facade
class BankService {
  constructor() {
    this.customer = new Customer("John");
  }

  deposit(amount) {
    this.customer.deposit(amount);
  }

  withdraw(amount) {
    this.customer.withdraw(amount);
  }

  getBalance() {
    return this.customer.getBalance();
  }
}

// Usage example
const bankService = new BankService();
bankService.deposit(1000);
console.log(bankService.getBalance()); // Output: 1000
bankService.withdraw(500);
console.log(bankService.getBalance()); // Output: 500


In this example, the Bank class and the Customer class represent the Complex Subsystem, while the BankService class represents the Facade. The BankService class provides a simplified interface to the Bank and Customer classes, and allows clients to perform common banking operations, such as depositing and withdrawing money.

Conclusion

I hope you found this article helpful in learning about the various design patterns that are used in web development. I have only covered a few here, but there are many more out there! As you continue your journey as a developer, it will be important to keep an eye out for new patterns that emerge or old ones that have been reimagined with modern technology.