whenever life put's you in a tough situtation, never say why me! but, try me!

Chapter 16: Design Patterns

Design patterns are typical solutions to common problems in software design. They provide reusable templates and best practices that can be applied to solve various design challenges. This chapter introduces some common design patterns used in JavaScript.

Common Design Patterns

1. Singleton Pattern

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

Example

class Singleton {
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    Singleton.instance = this;
  }

  getInstance() {
    return Singleton.instance;
  }
}

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

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

In this example, the Singleton class only allows one instance to be created. Subsequent attempts to create new instances return the same existing instance.

2. Observer Pattern

The Observer pattern is a behavioral design pattern where an object (known as the subject) maintains a list of dependents (observers) that are notified of any changes to the subject’s state. This is commonly used in event-driven systems.

Example

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

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

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

class Observer {
  update(message) {
    console.log(`Observer received message: ${message}`);
  }
}

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

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.notifyObservers("Hello Observers!");
// Output:
// Observer received message: Hello Observers!
// Observer received message: Hello Observers!

In this example, the Subject class maintains a list of observers. When the subject's state changes, it notifies all registered observers.

3. Module Pattern

The Module pattern provides a way to encapsulate private data and expose only the public API. It helps in organizing code and managing dependencies by creating modular and self-contained units.

Example

const Module = (function () {
  let privateData = "I am private";

  function privateMethod() {
    console.log("I am a private method");
  }

  return {
    publicMethod: function () {
      console.log("I am a public method");
      privateMethod();
    },
    getPrivateData: function () {
      return privateData;
    },
  };
})();

Module.publicMethod(); // Output: I am a public method \n I am a private method
console.log(Module.getPrivateData()); // Output: I am private

In this example, the Module is an immediately invoked function expression (IIFE) that encapsulates private data and methods while exposing public methods through the returned object.

4. Factory Pattern

The Factory pattern provides a method for creating objects in a super class but allows subclasses to alter the type of objects that will be created.

Example

class Dog {
  speak() {
    console.log("Woof!");
  }
}

class Cat {
  speak() {
    console.log("Meow!");
  }
}

class AnimalFactory {
  createAnimal(type) {
    if (type === "dog") {
      return new Dog();
    } else if (type === "cat") {
      return new Cat();
    } else {
      throw new Error("Unknown animal type");
    }
  }
}

const factory = new AnimalFactory();
const dog = factory.createAnimal("dog");
const cat = factory.createAnimal("cat");

dog.speak(); // Output: Woof!
cat.speak(); // Output: Meow!

In this example, AnimalFactory is used to create objects of type Dog or Cat based on the provided type.

5. Decorator Pattern

The Decorator pattern allows adding new behavior to an object dynamically without altering its structure. It is typically used to extend functionalities of classes.

Example

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

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

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

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

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

const coffee = new Coffee();
const milkCoffee = new MilkDecorator(coffee);
const milkAndSugarCoffee = new SugarDecorator(milkCoffee);

console.log(coffee.cost()); // Output: 5
console.log(milkCoffee.cost()); // Output: 7
console.log(milkAndSugarCoffee.cost()); // Output: 8

In this example, MilkDecorator and SugarDecorator are used to dynamically add additional cost to the Coffee object.

6. Prototype Pattern

The Prototype pattern creates new objects by copying an existing object, which allows for efficient object creation. It is useful when object creation is expensive and you want to avoid repeated instantiation.

Example

const carPrototype = {
  drive() {
    console.log("Driving a car");
  },
  clone() {
    const newCar = Object.create(this);
    return newCar;
  },
};

const myCar = carPrototype.clone();
myCar.drive(); // Output: Driving a car

In this example, carPrototype serves as a prototype object that can be cloned to create new car objects.

Summary

In this chapter, we covered several common design patterns in JavaScript:

  • Singleton Pattern: Ensures a single instance of a class.
  • Observer Pattern: Allows objects to listen and react to events or changes.
  • Module Pattern: Encapsulates private data and exposes public methods.
  • Factory Pattern: Creates objects without specifying the exact class of the object.
  • Decorator Pattern: Adds new behavior to objects dynamically.
  • Prototype Pattern: Creates new objects by copying an existing prototype.

Understanding and applying these design patterns will help you create more robust, maintainable, and scalable JavaScript applications.