DEV Community

Cover image for Understanding JavaScript Design Patterns: A Blueprint for Efficient Coding
Omor Faruk
Omor Faruk

Posted on

Understanding JavaScript Design Patterns: A Blueprint for Efficient Coding

Imagine a group of architects tasked with designing a skyscraper. During the design stage, they face numerous considerations, such as:

  • Architectural Style: Should the building be brutalist, minimalist, or take on another design?
  • Width of the Base: What dimensions are required to prevent collapse on windy days?
  • Protection Against Natural Disasters: What structural measures must be in place to guard against earthquakes, flooding, and other natural threats?

The array of factors to consider is vast, but one thing is clear: a blueprint is necessary to guide the construction of the skyscraper. Without a cohesive design plan, architects would have to reinvent the wheel, leading to confusion and inefficiency.

In the programming realm, developers often rely on design patterns to create software that adheres to clean code principles. These patterns are prevalent, allowing programmers to concentrate on shipping new features rather than repeatedly solving the same problems.

In this article, you will discover several common JavaScript design patterns, accompanied by practical Node.js projects to illustrate each pattern's application.

What Are Design Patterns in Software Engineering?

Design patterns serve as pre-made blueprints that developers can adapt to tackle recurring design problems in coding. It’s important to note that these patterns are not merely code snippets; they represent general concepts for addressing challenges.

Benefits of Design Patterns:

  • Tried and Tested: Design patterns effectively solve numerous issues in software design. By knowing and applying these patterns, developers can leverage object-oriented design principles to tackle various problems.

  • Common Language: Design patterns facilitate efficient communication among team members. For instance, if a teammate suggests using the factory method, everyone understands the implication and rationale behind the recommendation.

In this article, we will explore three categories of design patterns:

  1. Creational: Used for creating objects.
  2. Structural: Focused on assembling these objects into a functional structure.
  3. Behavioral: Concerned with the responsibilities assigned to those objects.

Let’s dive into these design patterns in action!

Creational Design Patterns

Creational patterns encompass various techniques that assist developers in object creation.

Factory Pattern

The factory method is a pattern for creating objects that allows for greater control over the object creation process. This method is ideal when you want to centralize the logic for object instantiation.

// File: factory-pattern.js
const createCar = ({ company, model, size }) => ({
  company,
  model,
  size,
  showDescription() {
    console.log(
      "The all new ",
      model,
      " is built by ",
      company,
      " and has an engine capacity of ",
      size,
      " CC "
    );
  },
});

const challenger = createCar({
  company: "Dodge",
  model: "Challenger",
  size: 6162,
});
challenger.showDescription();
Enter fullscreen mode Exit fullscreen mode

Breakdown:

  • The createCar function serves as an interface for generating Car objects.
  • Each Car has properties: company, model, and size, alongside a showDescription method.
  • We instantiate a challenger object and invoke its showDescription method to display its attributes.

Builder Pattern

The builder method enables step-by-step construction of objects, allowing for flexibility in creating objects with only necessary functions.

// File: builder-pattern.js
class Car {
  constructor({ model, company, size }) {
    this.model = model;
    this.company = company;
    this.size = size;
  }
}

Car.prototype.showDescription = function () {
  console.log(
    this.model +
      " is made by " +
      this.company +
      " and has an engine capacity of " +
      this.size +
      " CC "
  );
};
Car.prototype.reduceSize = function () {
  this.size -= 2; // Reduce engine size
};

const challenger = new Car({
  company: "Dodge",
  model: "Challenger",
  size: 6162,
});
challenger.showDescription();
console.log('reducing size...');
challenger.reduceSize();
challenger.reduceSize();
challenger.showDescription();
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • We define a Car class to instantiate objects and extend it using prototypes for added functionalities.
  • After instantiating the challenger object, we log its details before and after reducing its size.

Structural Design Patterns

Structural design patterns focus on how various components of our program collaborate.

Adapter Pattern

The adapter pattern allows objects with incompatible interfaces to work together. This is particularly useful when adapting old code to a new codebase without causing breaking changes.

// File: adapter-pattern.js
const groupsWithSoldAlbums = [
  { name: "Twice", sold: 23 },
  { name: "Blackpink", sold: 23 },
  { name: "Aespa", sold: 40 },
  { name: "NewJeans", sold: 45 },
];

console.log("Before:", groupsWithSoldAlbums);

let illit = { name: "Illit", revenue: 300 };

const COST_PER_ALBUM = 30;
const convertToAlbumsSold = (group) => {
  return { name: group.name, sold: parseInt(group.revenue / COST_PER_ALBUM) };
};

illit = convertToAlbumsSold(illit);
groupsWithSoldAlbums.push(illit);

console.log("After:", groupsWithSoldAlbums);
Enter fullscreen mode Exit fullscreen mode

Overview:

  • An array groupsWithSoldAlbums stores objects with name and sold properties.
  • We create an illit object with revenue and convert it using the convertToAlbumsSold adapter function to maintain compatibility.

Decorator Pattern

The decorator design pattern allows the addition of new methods and properties to objects after creation, enabling runtime capabilities.

// File: decorator-pattern.js
class MusicArtist {
  constructor({ name, members }) {
    this.name = name;
    this.members = members;
  }
  displayMembers() {
    console.log("Group name", this.name, " has", this.members.length, " members:");
    this.members.forEach((item) => console.log(item));
  }
}

class PerformingArtist extends MusicArtist {
  constructor({ name, members, eventName, songName }) {
    super({ name, members });
    this.eventName = eventName;
    this.songName = songName;
  }
  perform() {
    console.log(
      this.name + " is now performing at " + this.eventName + " with their hit song " + this.songName
    );
  }
}

const akmu = new PerformingArtist({
  name: "Akmu",
  members: ["Suhyun", "Chanhyuk"],
  eventName: "MNET",
  songName: "Hero",
});

akmu.displayMembers();
akmu.perform();
Enter fullscreen mode Exit fullscreen mode

Summary:

  • The MusicArtist class defines the base structure, while PerformingArtist extends it to introduce new properties and methods.
  • We instantiate the akmu object and log its details along with the performance information.

Behavioral Design Patterns

This category focuses on the interaction and communication between different components within a program.

Chain of Responsibility

The Chain of Responsibility design pattern allows requests to pass through a chain of components until one is found capable of processing it.

// File: chain-of-responsibility-pattern.js
class Leader {
  constructor(responsibility, name) {
    this.responsibility = responsibility;
    this.name = name;
  }

  setNext(handler) {
    this.nextHandler = handler;
    return handler;
  }

  handle(responsibility) {
    if (this.nextHandler) {
      console.log(this.name + " cannot handle operation: " + responsibility);
      return this.nextHandler.handle(responsibility);
    }
  }
}

// Example usage of the Chain of Responsibility
const leader1 = new Leader("manage budget", "Alice");
const leader2 = new Leader("approve project", "Bob");

leader1.setNext(leader2);
leader1.handle("approve project");
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The Leader class processes responsibilities, passing requests along the chain until a capable handler is found.
  • In the example, if Alice cannot manage the operation, the request is forwarded to Bob.

Conclusion

Design patterns serve as essential tools for software developers, providing tested solutions for common programming challenges. By understanding and applying these patterns, developers can create more efficient, scalable, and maintainable code. The next time you're faced with a coding challenge, remember that a well-established design pattern might just be the blueprint you need!

Top comments (0)