DEV Community

Cover image for Design Patterns in JavaScript: A Comprehensive Guide
Tope Fasasi
Tope Fasasi

Posted on • Updated on

Design Patterns in JavaScript: A Comprehensive Guide

JavaScript, with its widespread adoption and versatility, has become a cornerstone of modern web development. As you delve deeper into JavaScript development, understanding and utilizing patterns becomes crucial. In this article, we will embark on a journey to demystify JavaScript patterns and explore how they can enhance your coding practices.

Prerequisite

To understand the concepts and techniques discussed in this article, you are expected to have an understanding of the fundamentals of JavaScript. Familiarity with concepts like variables, functions, data types, object-oriented programming, etc. is essential.

Before we move on, let's take a moment to understand the importance of JavaScript as a programming language.

JavaScript as a programming language

JavaScript, often referred to as the "language of the web," is a dynamic, high-level programming language. It is primarily used for client-side scripting in web browsers, but it has also gained traction on the server-side with the advent of Node.js. JavaScript's key features include its ability to manipulate the DOM, handle events, provide interactivity, etc. to web pages.

That being said, let's briefly discuss the importance and purpose of Patterns in JavaScript.

Importance of patterns in JavaScript development

Patterns in JavaScript serve as proven solutions to recurring problems encountered during software development. They provide structure, improve code organization, enhance maintainability, and promote reusability. By understanding and applying patterns, developers can write cleaner, more efficient code and effectively tackle complex challenges.

Purpose of understanding JavaScript patterns

Understanding JavaScript patterns goes beyond memorizing syntax or following best practices. It empowers developers to think critically about software design, choose appropriate solutions, and build scalable applications. By mastering JavaScript patterns, you gain valuable insights into the language and its ecosystem, enabling you to write robust and maintainable code.

Now that we know the importance and purpose of JavaScript Patterns, let's delve into the fundamentals of JS Design Patterns.

The Fundamentals of Design Patterns

In this section, we lay the groundwork for understanding design patterns in the context of JavaScript development.

Definition and characteristics of design patterns

Design patterns are reusable templates that encapsulate best practices for solving recurring software design problems. They offer a structured approach to designing software systems and promote modular, flexible, and maintainable code. Common characteristics of design patterns include their purpose, structure, participants, and collaborations.

Types of design patterns

Design patterns can be categorized into three main types:

  • Creational

  • Structural

  • Behavioral

Understanding these categories helps identify the appropriate pattern for a given problem.

  • Creational Patterns

Creational patterns focus on object creation mechanisms, providing ways to instantiate objects in a flexible and controlled manner. Some commonly used creational patterns in JavaScript include:

  • Singleton

  • Factory

  • Constructor

  • Prototype

  • Builder

  • Module

Singleton Pattern

The Singleton Pattern ensures that a class has only one instance and provides a global point of access to it. This pattern is useful when you want to limit the number of instances of a class and ensure a single shared instance is accessible throughout the application.

// Implementation example of the Singleton Pattern
class Singleton {
  constructor() {
    if (!Singleton.instance) {
      // Initialize the instance
      Singleton.instance = this;
    }
    return Singleton.instance;
  }
}

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

console.log(instance1 === instance2); // Output: true
Enter fullscreen mode Exit fullscreen mode

In this example, the Singleton class has a constructor that checks if an instance of the class already exists. If an instance doesn't exist (!Singleton.instance condition), it initializes the instance by assigning this to Singleton.instance. This ensures that subsequent calls to the constructor will return the same instance.

When instance1 and instance2 are created using the new Singleton() syntax, both variables refer to the same instance of the Singleton class. Hence, when comparing instance1 === instance2 using the strict equality operator, it evaluates to true.

Factory Pattern

The Factory Pattern provides a way to create objects without specifying their concrete classes. It encapsulates the object creation logic in a separate factory method, allowing flexibility and decoupling between the creator and the created objects.

// Implementation example of the Factory Pattern
class Car {
  constructor(make, model) {
    this.make = make;
    this.model = model;
  }
}

class CarFactory {
  createCar(make, model) {
    return new Car(make, model);
  }
}

const factory = new CarFactory();
const myCar = factory.createCar("Tope", "Model 1");
Enter fullscreen mode Exit fullscreen mode

In this example, a CarFactory instance is created using new CarFactory(), and then the createCar method is invoked on the factory with the arguments "Tope" and "Model 1". This creates a new Car object with the make "Tope" and model "Model 1", which is assigned to the myCar variable.

Constructor Pattern

The Constructor Pattern creates objects from a constructor function using the new keyword. It allows you to define and initialize object properties within the constructor function.

// Implementation example of the Constructor Pattern
function Person(name, age) {
  this.name = name;
  this.age = age;
}

const tope = new Person("Tope", 24);
Enter fullscreen mode Exit fullscreen mode

The above code defines a constructor function called Person that takes two parameters: name and age. Inside the function, the name and age values are assigned to the respective properties of the newly created object using the this keyword.

Later, a new instance of the Person object is created by invoking the Person function with the arguments "Tope" and 24. This creates a new object with the name property set to "Tope" and the age property set to 24, which is then assigned to the variable tope. The output of this code is that Tope holds an object representing a person with the name "Tope" and the age of 24.

Prototype Pattern

The Prototype pattern in JavaScript focuses on creating objects by cloning or extending existing objects as prototypes. It allows us to create new instances without explicitly defining their classes. In this pattern, objects act as prototypes for creating new objects, enabling inheritance and the sharing of properties and methods among multiple objects.

// Prototype object
const carPrototype = {
  wheels: 4,
  startEngine() {
    console.log("Engine started.");
  },
  stopEngine() {
    console.log("Engine stopped.");
  }
};

// Create new car instance using the prototype
const car1 = Object.create(carPrototype);
car1.make = "Toyota";
car1.model = "Camry";

// Create another car instance using the same prototype
const car2 = Object.create(carPrototype);
car2.make = "Honda";
car2.model = "Accord";

car1.startEngine(); // Output: "Engine started."
car2.stopEngine(); // Output: "Engine stopped."
Enter fullscreen mode Exit fullscreen mode

In this example, car instances car1 and car2 are created using a prototype object carPrototype. car1 has the make "Toyota" and model "Camry", while car2 has the make "Honda" and model "Accord". When car1.startEngine() is called, it outputs "Engine started.", and when car2.stopEngine() is called, it outputs "Engine stopped.". This demonstrates the utilization of a prototype object to share properties and methods among multiple instances.

Builder Pattern

In the Builder pattern, a builder class or object is responsible for constructing the final object. It provides a set of methods to configure and set the properties of the object being built. The construction process typically involves invoking these methods in a specific order to gradually build the object.

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

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

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

  setEngine(engine) {
    this.car.engine = engine;
    return this;
  }

  setWheels(wheels) {
    this.car.wheels = wheels;
    return this;
  }

  build() {
    return this.car;
  }
}

class Car {
  constructor() {
    this.make = "";
    this.model = "";
    this.engine = "";
    this.wheels = 0;
  }

  displayInfo() {
    console.log(`Make: ${this.make}, Model: ${this.model}, Engine: ${this.engine}, Wheels: ${this.wheels}`);
  }
}

// Usage
const carBuilder = new CarBuilder();
const car = carBuilder.setMake("Toyota").setModel("Camry").setEngine("V6").setWheels(4).build();
car.displayInfo(); // Output: Make: Toyota, Model: Camry, Engine: V6, Wheels: 4
Enter fullscreen mode Exit fullscreen mode

In this example, the CarBuilder class allows for the construction of Car objects with different properties. By calling setMake, setModel, setEngine, setWheels methods, the properties of the Car object are set. The build method finalizes the construction and returns the fully built Car object. The Car class represents a car and includes a displayInfo method to log its details. By creating a carBuilder instance and chaining the property-setting methods, a car object is constructed with specific make, model, engine, and wheel values. Invoking car.displayInfo() displays the car's information.

Module Pattern

The Module Pattern encapsulates related methods and properties into a single module, providing a clean way to organize and protect the code. It allows for private and public members, enabling information hiding and preventing global namespace pollution.

const MyModule = (function() {
  // Private members
  let privateVariable = "I am private";

  function privateMethod() {
    console.log("This is a private method");
  }

  // Public members
  return {
    publicVariable: "I am public",

    publicMethod() {
      console.log("This is a public method");
      // Accessing private members within the module
      console.log(privateVariable);
      privateMethod();
    }
  };
})();

// Usage
console.log(MyModule.publicVariable); // Output: "I am public"
MyModule.publicMethod(); // Output: "This is a public method" "I am private" "This is a private method"
Enter fullscreen mode Exit fullscreen mode

In this example, the code uses an immediately invoked function expression (IIFE) to encapsulate private and public members. The module has private variables and methods, as well as public variables and methods. When accessed, the public members provide the expected output. This pattern allows for controlled access to encapsulated private members while exposing selected public members.

  • Structural patterns

Structural patterns focus on organizing and composing objects to form larger structures. They facilitate the composition of objects, defining relationships between them and providing flexible ways to manipulate their structure. Some commonly used structural patterns in JavaScript include:

  • Decorator Pattern

  • Facade Pattern

  • Adapter

  • Bridge

  • Composite

Decorator Pattern

The Decorator Pattern allows you to add behavior or modify the existing behavior of an object dynamically. It enhances the functionality of an object by wrapping it with one or more decorators without modifying its structure.

// Implementation example of the Decorator Pattern
class Coffee {
  getCost() {
    return 1;
  }
}

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

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

const myCoffee = new Coffee();
const coffeeWithMilk = new CoffeeDecorator(myCoffee);
console.log(coffeeWithMilk.getCost()); // Output: 1.5
Enter fullscreen mode Exit fullscreen mode

In this example, the CoffeeDecorator class wraps a base Coffee object and adds additional functionality. It has a getCost method that calculates the total cost by combining the cost of the base coffee with an additional cost of 0.5.

In the usage section, a myCoffee instance of the Coffee class is created. Then, a coffeeWithMilk instance of the CoffeeDecorator class is instantiated, passing myCoffee as an argument. When coffeeWithMilk.getCost() is called, it returns the total cost of the coffee with the added cost from the decorator, resulting in an output of 1.5. This example illustrates how the decorator pattern can extend the functionality of an object by dynamically adding or modifying its properties or methods.

Facade Pattern

The Facade Pattern provides a simplified interface to a complex subsystem, acting as a front-facing interface that hides the underlying implementation details. It offers a convenient way to interact with a complex system by providing a high-level interface.

// Implementation example of the Facade Pattern
class SubsystemA {
  operationA() {
    console.log("Subsystem A operation.");
  }
}

class SubsystemB {
  operationB() {
    console.log("Subsystem B operation.");
  }
}

class Facade {
  constructor() {
    this.subsystemA = new SubsystemA();
    this.subsystemB = new SubsystemB();
  }

  operation() {
    this.subsystemA.operationA();
    this.subsystemB.operationB();
  }
}

const facade = new Facade();
facade.operation(); // Output: "Subsystem A operation." "Subsystem B operation."
Enter fullscreen mode Exit fullscreen mode

In this example, the code consists of three classes: SubsystemA, SubsystemB, and Facade. The SubsystemA and SubsystemB classes represent independent subsystems and have their respective operationA and operationB methods. The Facade class serves as a simplified interface that aggregates the functionality of the subsystems.

In the usage section, a facade instance of the Facade class is created. Invoking facade.operation() triggers the execution of operationA from SubsystemA and operationB from SubsystemB. As a result, the output displays "Subsystem A operation." followed by "Subsystem B operation." This demonstrates how the Facade pattern provides a unified and simplified interface to interact with complex subsystems, abstracting their complexities and making them easier to use.

Adapter Pattern

The Adapter pattern is a structural design pattern that allows objects with incompatible interfaces to collaborate by acting as a bridge between them. It provides a way to convert the interface of one object into another interface that clients expect.

// Implementation 
class LegacyPrinter {
  printLegacy(text) {
    console.log(`Legacy Printing: ${text}`);
  }
}

// Target interface
class Printer {
  print(text) {}
}

// Adapter
class PrinterAdapter extends Printer {
  constructor() {
    super();
    this.legacyPrinter = new LegacyPrinter();
  }

  print(text) {
    this.legacyPrinter.printLegacy(text);
  }
}

// Usage
const printer = new PrinterAdapter();
printer.print("Hello, World!"); // Output: "Legacy Printing: Hello, World!"
Enter fullscreen mode Exit fullscreen mode

In this code, the Adapter pattern is used to bridge the gap between the LegacyPrinter class and a desired Printer interface. The PrinterAdapter extends the Printer class and internally utilizes the LegacyPrinter to adapt the print method. When printer.print("Hello, World!") is called, it effectively triggers the legacy printing functionality with the output "Legacy Printing: Hello, World!". This shows how the Adapter pattern enables the integration of incompatible components by providing a standardized interface.

Bridge Pattern

The Bridge pattern is a structural design pattern that separates the abstraction and implementation of a system, allowing it to evolve independently. It introduces a bridge between the two by using an interface or abstract class. Here's an example code snippet to illustrate the Bridge pattern:

// Example 
class Shape {
  constructor(color) {
    this.color = color;
  }

  draw() {}
}

// Concrete Abstractions
class Circle extends Shape {
  draw() {
    console.log(`Drawing a ${this.color} circle`);
  }
}

class Square extends Shape {
  draw() {
    console.log(`Drawing a ${this.color} square`);
  }
}

// Implementor
class Color {
  getColor() {}
}

// Concrete Implementors
class RedColor extends Color {
  getColor() {
    return "red";
  }
}

class BlueColor extends Color {
  getColor() {
    return "blue";
  }
}

// Usage
const redCircle = new Circle(new RedColor());
redCircle.draw(); // Output: "Drawing a red circle"

const blueSquare = new Square(new BlueColor());
blueSquare.draw(); // Output: "Drawing a blue square"
Enter fullscreen mode Exit fullscreen mode

In this example, we have the Abstraction represented by the Shape class, which has a color property and a draw method. The Concrete Abstractions, Circle and Square, inherit from the Shape class and implement their specific draw behavior. The Implementor is represented by the Color class, which declares the getColor method. The Concrete Implementors, RedColor, and BlueColor, inherit from the Color class and provide their respective color implementations.

In the usage section, we create instances of the Concrete Abstractions, passing the appropriate Concrete Implementor objects. This allows the Abstraction to delegate the color-related functionality to the Implementor. When we invoke the draw method, it accesses the color from the Implementor and performs the drawing operation accordingly.

Composite Pattern

The Composite pattern is a structural design pattern that allows you to treat individual objects and compositions of objects uniformly. It enables you to create hierarchical structures where each element can be treated as a single object or a collection of objects. The pattern uses a common interface to represent both individual objects (leaf nodes) and compositions (composite nodes), allowing clients to interact with them uniformly.

// Implementation 
class Employee {
  constructor(name) {
    this.name = name;
  }

  print() {
    console.log(`Employee: ${this.name}`);
  }
}

// Composite
class Manager extends Employee {
  constructor(name) {
    super(name);
    this.employees = [];
  }

  add(employee) {
    this.employees.push(employee);
  }

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

  print() {
    console.log(`Manager: ${this.name}`);
    for (const employee of this.employees) {
      employee.print();
    }
  }
}

// Usage
const john = new Employee("John Doe");
const jane = new Employee("Jane Smith");

const mary = new Manager("Mary Johnson");
mary.add(john);
mary.add(jane);

const peter = new Employee("Peter Brown");

const bob = new Manager("Bob Williams");
bob.add(peter);
bob.add(mary);

bob.print();
Enter fullscreen mode Exit fullscreen mode

In this example, we have the Component class Employee, which represents individual employees. The Composite class Manager extends the Employee class and can contain a collection of employees. It provides methods to add and remove employees from the collection and overrides the print method to display the manager's name and the employees under them.

In the usage section, we create a composite hierarchy where Manager objects can contain both individual employees (Employee) and other managers (Manager). We add employees to managers, constructing a hierarchical structure. Finally, we invoke the print method on the top-level manager, which recursively prints the hierarchy, showing the managers and their respective employees.

  • Behavioral patterns

Behavioral patterns focus on the interaction between objects and the distribution of responsibilities. They provide solutions for communication, coordination, and collaboration among objects. The following are types of behavioral patterns.

  • Observer Pattern

  • Strategy Pattern

  • Command Pattern

  • Iterator Pattern

  • Mediator Pattern

Observer Pattern

The Observer Pattern establishes a one-to-many relationship between objects, where multiple observers are notified of changes in the subject's state. It enables loose coupling between objects and promotes event-driven communication.

// Implementation example of the Observer Pattern
class Subject {
  constructor() {
    this.observers = [];
  }

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

  removeObserver(observer) {
    const index = this.observers.indexOf(observer);
    if (index !== -1) {
      this.observers.splice(index, 1);
    }
  }

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

class Observer {
  update() {
    console.log("Observer is notified of changes.");
  }
}

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

subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notifyObservers(); // Output: "Observer is notified of changes." "Observer is notified of changes."
Enter fullscreen mode Exit fullscreen mode

In this example, the Subject class represents a subject that maintains a list of observers and provides methods to add, remove, and notify observers. The Observer class defines the behavior of an observer with its update method. In the usage section, a subject instance of the Subject class is created. Two observer instances are also created and added to the subject using the addObserver method.

When subject.notifyObservers() is invoked, it triggers the update method for each observer. As a result, the output "Observer is notified of changes." is logged twice, indicating that the observers have been notified of the changes in the subject.

Strategy Pattern

The Strategy Pattern allows you to encapsulate interchangeable algorithms within separate strategy objects. It enables dynamic selection of algorithms at runtime, promoting flexibility and extensibility.

// Implementation example of the Strategy Pattern
class Context {
  constructor(strategy) {
    this.strategy = strategy;
  }

  executeStrategy() {
    this.strategy.execute();
  }
}

class ConcreteStrategyA {
  execute() {
    console.log("Strategy A is executed.");
  }
}

class ConcreteStrategyB {
  execute() {
    console.log("Strategy B is executed.");
  }
}

const contextA = new Context(new ConcreteStrategyA());
contextA.executeStrategy(); // Output: "Strategy A is executed."

const contextB = new Context(new ConcreteStrategyB());
contextB.executeStrategy(); // Output: "Strategy B is executed."
Enter fullscreen mode Exit fullscreen mode

In this example, the Context class represents a context that encapsulates different strategies, with a strategy property and an executeStrategy method. There are two concrete strategy classes, ConcreteStrategyA and ConcreteStrategyB, each with its own execute method that outputs a specific message.

In the usage section, a contextA instance of the Context class is created with ConcreteStrategyA as the strategy. Calling contextA.executeStrategy() invokes the execute method of ConcreteStrategyA, resulting in the output "Strategy A is executed." Similarly, a contextB instance is created with ConcreteStrategyB as the strategy, and invoking contextB.executeStrategy() triggers the execute method of ConcreteStrategyB, resulting in the output "Strategy B is executed." This demonstrates how the Strategy pattern allows for dynamic selection of behavior at runtime by encapsulating it in different strategy objects.

Command Pattern

The Command Pattern encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undo operations. It decouples the sender of a request from the receiver, promoting loose coupling and flexibility.

// Implementation 
class Receiver {
  execute() {
    console.log("Receiver executes the command.");
  }
}

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

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

class Invoker {
  setCommand(command) {
    this.command = command;
  }

  executeCommand() {
    this.command.execute();
  }
}

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

invoker.setCommand(command);
invoker.executeCommand(); // Output: "Receiver executes the command."
Enter fullscreen mode Exit fullscreen mode

In this example, the Receiver class executes the command when called, and the Command class encapsulates a command and delegates execution to the receiver. The Invoker class sets and executes a command. In the usage section, a receiver, command, and invoker are created. The command is set for the invoker, and invoking invoker.executeCommand() executes the command, resulting in the output "Receiver executes the command."

Iterator Pattern

The Iterator pattern is a behavioral design pattern that provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. It allows you to traverse a collection of objects in a uniform manner, regardless of the specific implementation of the collection. The pattern separates the traversal logic from the collection, promoting a clean and flexible approach to iterating over elements.

// Implementation 
class Collection {
  constructor() {
    this.items = [];
  }

  addItem(item) {
    this.items.push(item);
  }

  createIterator() {}
}

// Concrete Aggregate
class ConcreteCollection extends Collection {
  createIterator() {
    return new ConcreteIterator(this);
  }
}

// Iterator
class Iterator {
  constructor(collection) {
    this.collection = collection;
    this.index = 0;
  }

  hasNext() {}

  next() {}
}

// Concrete Iterator
class ConcreteIterator extends Iterator {
  hasNext() {
    return this.index < this.collection.items.length;
  }

  next() {
    return this.collection.items[this.index++];
  }
}

// Usage
const collection = new ConcreteCollection();
collection.addItem("Item 1");
collection.addItem("Item 2");
collection.addItem("Item 3");

const iterator = collection.createIterator();
while (iterator.hasNext()) {
  console.log(iterator.next());
}
Enter fullscreen mode Exit fullscreen mode

In this code, we have the Aggregate represented by the Collection class, which defines the interface for creating an iterator object. The Concrete Aggregate, ConcreteCollection, extends the Collection class and provides a concrete implementation of the iterator creation.

The Iterator is represented by the Iterator class, which defines the interface for accessing and traversing elements. The Concrete Iterator, ConcreteIterator, extends the Iterator class and provides a concrete implementation of the iteration logic. In the usage section, we create an instance of the Concrete Aggregate, ConcreteCollection, and add items to it. We then create an iterator using the createIterator method. By using the iterator's hasNext and next methods, we iterate over the collection and print each item.

Mediator Pattern

The Mediator pattern simplifies object communication by introducing a mediator object that serves as a central hub for coordinating interactions between objects. It encapsulates the communication logic and provides methods for objects to register, send, and receive messages.

// Implementation 
class Mediator {
  constructor() {
    this.colleague1 = null;
    this.colleague2 = null;
  }

  setColleague1(colleague) {
    this.colleague1 = colleague;
  }

  setColleague2(colleague) {
    this.colleague2 = colleague;
  }

  notifyColleague1(message) {
    this.colleague1.receive(message);
  }

  notifyColleague2(message) {
    this.colleague2.receive(message);
  }
}

class Colleague {
  constructor(mediator) {
    this.mediator = mediator;
  }

  send(message) {
    // Send a message to the mediator
    this.mediator.notifyColleague2(message);
  }

  receive(message) {
    console.log(`Received message: ${message}`);
  }
}

// Usage
const mediator = new Mediator();

const colleague1 = new Colleague(mediator);
const colleague2 = new Colleague(mediator);

mediator.setColleague1(colleague1);
mediator.setColleague2(colleague2);

colleague1.send("Hello Colleague 2!"); // Output: "Received message: Hello Colleague 2!"
Enter fullscreen mode Exit fullscreen mode

In this example, we have a Mediator class that acts as an intermediary between two Colleague objects. The Mediator holds references to the colleagues and provides methods to send messages between them.

Each Colleague object has a reference to the mediator and can send messages by notifying the mediator. The mediator, in turn, relays the messages to the appropriate colleagues. In this case, Colleague 1 sends a message to Colleague 2, and the latter receives and logs the message.

Conclusion

We have explored a range of essential design patterns in JavaScript, encompassing creational, structural, and behavioral patterns. The creational patterns allow us to create objects in a flexible and efficient manner. Structural patterns aid in organ flexibility and scalability. Behavioral patterns enable effective communication and interaction between JavaScript objects. By leveraging these design patterns, JavaScript developers can improve code reusability, maintainability, and overall system performance. Armed with this knowledge, we can architect robust and efficient JavaScript applications that meet the demands of modern software development.

Top comments (20)

Collapse
 
framemuse profile image
Valery Zinchenko

That's not just JS patterns, it's widely known Gang of Four Patterns.
en.m.wikipedia.org/wiki/Design_Pat...

If you want to know more about Software Design:
en.m.wikipedia.org/wiki/Software_d...
Comprehensive read about System design patterns:
en.m.wikipedia.org/wiki/Category:S...

There is only one downgrade in Wikipedia, the language may be complicated to understand.

Collapse
 
raydot profile image
raydot

I came here to say this exact same thing. These are the typical GoF patterns.

Collapse
 
db325 profile image
db325

Thank you, this found me right in time. I live the way you break down reach pattern and explain the use case asking with the example. This is by far one of the most helpful articles I've read all year!

Collapse
 
jedwards1211 profile image
Andy Edwards • Edited

The singleton example isn’t very idiomatic JS if you ask me…it seems like you were thinking within the conventions of some other language you’re experienced in rather than a natural way of doing things in JS. Two calls to new returning the same object is very very unusual behavior, thus a nasty surprise. JS programmers I know just export a const, or a getter function that returns the same value.

Collapse
 
dev_geos profile image
Dev Geos

I also found this strange but good to know as possibility.

Collapse
 
megathiago profile image
Thiago Marques

The example of Bridge Pattern is not printing the colors as expected.

Collapse
 
pirueto2004 profile image
Julio

I resolved this by invoking the getColor() method for each Color class implementor object:

const redColor = new RedColor().getColor();
const redCircle = new Circle(redColor);
redCircle.draw(); // Output: "Drawing a red circle"

const blueColor = new BlueColor().getColor();
const blueSquare = new Square(blueColor);
blueSquare.draw(); // Output: "Drawing a blue square"

Collapse
 
alex_at_dev profile image
Alex

As already pointed out, these are GoF or general OOP patterns. What I would also like to point out, that -at least for me- this ist not how you would solve the underlying problems in JS / TS, as it's not inherently an OOP language, but a prototype-based language with first-class functions, it's unique way of what you can do with objects and the way the module-system works. For example:

Singleton Pattern
Moules are effectively singletons, so no need to use any other pattern

// module.js
let foo = 'bar';
const singleton = {
  foo,
  logFoo: () => console.log(foo),
}

export const singleton;

// main.js
import {singleton} from './module.js'

singleton.foo = 'baz'; // changed for all uses of "singleton"
singleton.logFoo() // -> 'baz'

// main2.js
import {singleton} from './module.js'
singleton.logFoo() // -> 'baz'

Enter fullscreen mode Exit fullscreen mode

Generally I rarely use classes at all, instead I usually use (typed) objects. My favorite example is the Command pattern.

Command Pattern
This example also uses TypeScript to make sure a command always looks the same. You could also use it with vanilla JS and manually assert command objects always having the same fields. I also added a simple logic to implement undoing commands which in my opinion is one of the biggest real-world strengths / use-cases of this pattern.

type UndoFn<T> = (args: T) => void;

interface Command<T> {
  execute: (args: T) => UndoFn<T>;
}

const logMessageCmd: Command<string> = {
  execute: (msg) => {
    console.log('Message:', msg);
    return () => console.log('Revoked message', msg);
  }
}

const undo = logMessageCmd.execute('Hello World'); // -> 'Message: Hello World'
undo(); // 'Revoked message: Hello World'

Enter fullscreen mode Exit fullscreen mode
Collapse
 
natescode profile image
Nathan Hedglin

Seems like a copy paste from other articles.

"Mediator patterns are a type of behavioral design pattern that define an object that acts as a central hub for coordinating the interactions between a group of objects. The mediator encapsulates the logic and rules for how the objects should communicate, and reduces the coupling between them. The objects only need to know about the mediator, and not about each other. This simplifies the object interfaces and promotes modularity and reusability."

https://www.linkedin.com/advice/how-can-you-coordinate-object-interactions?utm_source=share&utm_medium=member_android&utm_campaign=share_via

Collapse
 
summanerd profile image
Mauricia Ragland • Edited

Great article. I've been doing this for so long that sometimes I forget which patterns I'm using.

The Colleague class in the Mediator example does not look like it will work if colleague2 sends a message.

Your mediator would benefit from a generic send that can determine which objects receive the message:

class Mediator {
    sendMessage(message, sender) {
        for (const colleague of this.colleagues) {
            if (colleague !== sender) {
                colleague.receive(message);
            }
        }
    }
}
class Colleague {
    send(message) {
        this.mediator.sendMessage(message, this);
    }
}
Enter fullscreen mode Exit fullscreen mode

Thanks for this post!

Collapse
 
iamrommel profile image
Rommel C. Manalo • Edited

The title "JS Design Patterns" is misleading, it is not specific to JS. Most programming language that uses OOP can do that. You just write the example in JS.

I think it better to title as Design Patterns with JS examples..

Collapse
 
changwoolab profile image
Changwoo Yoo

I'm not sure but it seems like using ChatGPT to write this article 😂

Collapse
 
bridgerbrowndev profile image
Bridger Brown

Thats a lot of patterns to read, bookmarking lol

Collapse
 
raibtoffoletto profile image
Raí B. Toffoletto

And... you can achieve all of those without using the word CLASS in JS. 😉

Collapse
 
codepraycode profile image
codepraycode

Thank you for this

Collapse
 
james0r profile image
James Auble

Like all too often on dev.to, I think the comments of this article are more helpful and educational than the article itself.

Collapse
 
wakywayne profile image
wakywayne

Why does he not need to call super() in the bridge patter example?

Collapse
 
wakywayne profile image
wakywayne

God tier article