Design patterns are reusable solutions to common programming problems. They help developers write code that is more modular, maintainable, and efficient. Do you know all of these 5?

1. Singleton Pattern

This pattern ensures that there is only one instance of a class and provides a global point of access to that instance.

For example, a database connection object can be created as a Singleton to ensure that only one connection is made to the database throughout the entire application.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const DatabaseConnection = (function() {
  let instance = null;

  function createInstance() {
    // code to create a database connection
    return new Object("Database connection");
  }

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

const connection1 = DatabaseConnection.getInstance();
const connection2 = DatabaseConnection.getInstance();

console.log(connection1 === connection2); // true

In this example, DatabaseConnection is a singleton object that ensures only one instance of the object is created.

2. Factory Pattern

This pattern is used to create objects of different classes without exposing the creation logic to the client. It provides a central place to create objects and separates the object creation from the object usage.

For example, let’s say you’re building a game and you have different types of characters, such as warriors, mages, and archers. Instead of creating each character individually and repeating the same code, you can use the Factory pattern to create the characters based on their type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Character {
  constructor(name, type) {
    this.name = name;
    this.type = type;
  }
}

class Warrior extends Character {
  constructor(name) {
    super(name, 'warrior');
  }
}

class Mage extends Character {
  constructor(name) {
    super(name, 'mage');
  }
}

class CharacterFactory {
  createCharacter(name, type) {
    switch(type) {
      case 'warrior':
        return new Warrior(name);
      case 'mage':
        return new Mage(name);
      default:
        throw new Error('Invalid character type');
    }
  }
}

// Usage
const factory = new CharacterFactory();
const warrior = factory.createCharacter('Aragorn', 'warrior');
const mage = factory.createCharacter('Gandalf', 'mage');

In this example, Character is a factory object that creates different characters based on the name and type passed to its createCharacter() method.

3. Observer Pattern

This pattern defines a one-to-many dependency between objects. When one object changes its state, all the dependent objects are notified and updated automatically.

For example, let’s say you have an online store and you want to notify users when a product they’re interested in becomes available. You can use the Observer pattern to implement this functionality.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
    this.observers = [];
  }

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

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

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

  setPrice(price) {
    this.price = price;
    this.notifyObservers();
  }
}

class User {
  constructor(name) {
    this.name = name;
  }

  update(product) {
    console.log(`${this.name}, the price of ${product.name} has changed to ${product.price}`);
  }
}

// Usage
const product = new Product('iPhone', 1000);
const user1 = new User('John');
const user2 = new User('Mary');
product.addObserver(user1);
product.addObserver(user2);
product.setPrice(900);

In this example, we have a Product class that represents a product in our store. It has a list of observers (users) that are interested in it, and methods to add, remove, and notify them. We also have a User class that represents a user that wants to be notified when the price of a product changes. Finally, we create a Product instance, add two observers to it, and change its price. This triggers the notifyObservers method, which in turn calls the update method on each observer, notifying them of the price change. In this way, the Observer pattern allows us to decouple the product and the users, making it easy to add or remove observers without affecting the product class.

4. Decorator Pattern

This pattern allows you to add new functionality to an existing object without modifying its structure. It dynamically adds or overrides behavior in an existing method of an object.

For example, let’s say you have a website that sells different types of pizzas. Each pizza has a base price, but customers can add different toppings to their liking. You can use the Decorator pattern to implement this functionality.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Pizza {
  constructor() {
    this.description = 'Unknown Pizza';
    this.price = 0;
  }

  getDescription() {
    return this.description;
  }

  getPrice() {
    return this.price;
  }
}

class Cheese extends Pizza {
  constructor(pizza) {
    super();
    this.pizza = pizza;
    this.description = `${pizza.getDescription()}, Cheese`;
    this.price = pizza.getPrice() + 2;
  }

  getDescription() {
    return this.description;
  }

  getPrice() {
    return this.price;
  }
}

class Pepperoni extends Pizza {
  constructor(pizza) {
    super();
    this.pizza = pizza;
    this.description = `${pizza.getDescription()}, Pepperoni`;
    this.price = pizza.getPrice() + 3;
  }

  getDescription() {
    return this.description;
  }

  getPrice() {
    return this.price;
  }
}

// Usage
let pizza = new Pizza();
pizza = new Cheese(pizza);
pizza = new Pepperoni(pizza);
console.log(pizza.getDescription()); // Unknown Pizza, Cheese, Pepperoni
console.log(pizza.getPrice()); // 5

In this example, we have a Pizza class that represents a pizza in our store. It has a base price and a description. We also have two decorator classes, Cheese and Pepperoni, which add cheese and pepperoni toppings to the pizza, respectively. Each decorator has a reference to the original pizza object, and it overrides the getDescription and getPrice methods to add its own description and price. Finally, we create a Pizza instance, add cheese and pepperoni toppings to it, and print its description and price. This allows us to create pizzas with different combinations of toppings without having to create a separate class for each one.

5. Module Pattern

This pattern allows you to encapsulate a group of related functions, variables, and objects into a single entity, making it easy to manage and organize your code. It provides a way to create private and public variables and methods.

For example, let’s say you have an application with a feature that serves as a simple counter. With it, you can increase, reset, or get the counter. The Module pattern lets you organize all of this related functionality very effectively.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const MyModule = (function() {
  // Private variables and functions
  let counter = 0;

  const incrementCounter = function() {
    counter++;
  };

  // Public API
  return {
    getCount: function() {
      return counter;
    },

    increment: function() {
      incrementCounter();
    },

    reset: function() {
      counter = 0;
    }
  };
})();

// Usage
console.log(MyModule.getCount()); // 0
MyModule.increment();
MyModule.increment();
console.log(MyModule.getCount()); // 2
MyModule.reset();
console.log(MyModule.getCount()); // 0

In this example, we have a self-executing (IIFE) function that returns an object with three methods: getCount, increment, and reset. These methods access a private variable called counter, which is not visible outside of the function. The function itself is called immediately, and its return value is assigned to the variable MyModule. This creates a module that has a private state and a public API, allowing us to use it to manage the counter without polluting the global namespace. Finally, we use the module’s methods to increment and reset the counter, and to retrieve its value.

There are many more design patterns that I haven’t listed. Let me know in the comments if you want me to make a part 2 to this list. Cheers!