🟨 JavaScript Q98 / 121

Explain design patterns in JavaScript.

AI-Powered Answer ✓ Answered

Design patterns are reusable solutions to common problems in software design. In JavaScript, they provide a structured approach to writing maintainable, scalable, and efficient code, helping developers solve recurring challenges and improve collaboration.

What are Design Patterns?

Design patterns are formalized best practices that a developer can use to solve common problems when designing an application or system. They are not specific implementations but rather templates or blueprints that can be adapted to various situations. While they don't solve every problem, they offer proven solutions for frequently encountered challenges, promoting good architectural principles.

Why Use Design Patterns in JavaScript?

Adopting design patterns in JavaScript offers several benefits: improved code readability and maintainability, enhanced scalability, reduced debugging time, and a common vocabulary among developers. They help in creating more robust and flexible applications, especially as projects grow in complexity.

Common Design Patterns in JavaScript

1. Creational Patterns

These patterns deal with object creation mechanisms, trying to create objects in a manner suitable for the situation. They provide flexibility in deciding which objects need to be created, how, and when.

Singleton Pattern

Ensures a class has only one instance and provides a global point of access to that instance. It's useful for managing a single resource, like a database connection or a configuration object.

javascript
const Singleton = (() => {
  let instance;

  function createInstance() {
    const object = new Object("I am the instance");
    return object;
  }

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

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

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

Factory Method Pattern

Defines an interface for creating an object, but lets subclasses decide which class to instantiate. The Factory Method lets a class defer instantiation to subclasses. It encapsulates object creation logic.

javascript
class Car {
  constructor(model) {
    this.model = model;
  }
}

class Truck {
  constructor(model) {
    this.model = model;
  }
}

class VehicleFactory {
  createVehicle(type, model) {
    switch (type) {
      case 'car':
        return new Car(model);
      case 'truck':
        return new Truck(model);
      default:
        return null;
    }
  }
}

const factory = new VehicleFactory();
const myCar = factory.createVehicle('car', 'Tesla Model 3');
const myTruck = factory.createVehicle('truck', 'Ford F-150');

console.log(myCar);   // Car { model: 'Tesla Model 3' }
console.log(myTruck); // Truck { model: 'Ford F-150' }

2. Structural Patterns

These patterns deal with the composition of classes and objects. They help in forming larger structures by combining objects and classes, ensuring flexibility and efficiency.

Module Pattern

A very common pattern in JavaScript for encapsulating a set of related state and behavior into a single unit. It uses closures to keep private state and methods, exposing only a public interface.

javascript
const ShoppingCart = (() => {
  let items = []; // Private variable

  function calculateTotal() { // Private method
    return items.reduce((sum, item) => sum + item.price, 0);
  }

  return { // Public interface
    addItem: (item) => {
      items.push(item);
      console.log(`Added ${item.name}. Current items: `, items);
    },
    getTotal: () => {
      return calculateTotal();
    },
    getItems: () => [...items] // Return a copy to prevent external modification
  };
})();

ShoppingCart.addItem({ name: 'Laptop', price: 1200 });
ShoppingCart.addItem({ name: 'Mouse', price: 25 });
console.log('Total:', ShoppingCart.getTotal()); // Total: 1225
console.log('Items:', ShoppingCart.getItems());

Adapter Pattern

Allows objects with incompatible interfaces to collaborate. It acts as a wrapper between two objects, converting the interface of one object into another interface that a client expects.

javascript
class OldPrinter {
  print(text) {
    return `Printing (Old way): ${text}`;
  }
}

class NewPrinter {
  printHighQuality(data) {
    return `Printing (New way, HQ): ${data}`;
  }
}

// Adapter to make NewPrinter compatible with OldPrinter's interface
class NewPrinterAdapter {
  constructor(newPrinter) {
    this.newPrinter = newPrinter;
  }

  print(text) {
    return this.newPrinter.printHighQuality(text);
  }
}

const oldPrinter = new OldPrinter();
console.log(oldPrinter.print('Document A'));

const newPrinter = new NewPrinter();
// console.log(newPrinter.print('Document B')); // This would fail, wrong method name

const adapter = new NewPrinterAdapter(newPrinter);
console.log(adapter.print('Document B (via Adapter)'));

3. Behavioral Patterns

These patterns are concerned with algorithms and the assignment of responsibilities between objects. They describe how objects and classes interact and distribute responsibilities.

Observer Pattern

Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. Common in event handling systems.

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

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

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

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

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

  update(data) {
    console.log(`${this.name} received update: ${data}`);
  }
}

const newsFeed = new Subject();
const user1 = new Observer('User 1');
const user2 = new Observer('User 2');

newsFeed.addObserver(user1);
newsFeed.addObserver(user2);

newsFeed.notify('Breaking News: AI takes over the world!');
// User 1 received update: Breaking News: AI takes over the world!
// User 2 received update: Breaking News: AI takes over the world!

newsFeed.removeObserver(user2);
newsFeed.notify('Local News: New coffee shop opened!');
// User 1 received update: Local News: New coffee shop opened!

Strategy Pattern

Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it. Useful when you have multiple ways to perform a task and want to switch between them at runtime.

javascript
class ShippingStrategy {
  calculate(order) {
    throw new Error("calculate method must be implemented by subclasses");
  }
}

class StandardShipping extends ShippingStrategy {
  calculate(order) {
    return 5.00; // Flat rate for standard
  }
}

class ExpressShipping extends ShippingStrategy {
  calculate(order) {
    return 15.00; // Flat rate for express
  }
}

class InternationalShipping extends ShippingStrategy {
  calculate(order) {
    return 25.00; // Flat rate for international
  }
}

class Order {
  constructor(amount, strategy) {
    this.amount = amount;
    this.shippingStrategy = strategy;
  }

  setShippingStrategy(strategy) {
    this.shippingStrategy = strategy;
  }

  calculateTotal() {
    return this.amount + this.shippingStrategy.calculate(this);
  }
}

const order1 = new Order(100, new StandardShipping());
console.log('Order 1 (Standard):', order1.calculateTotal()); // 105

const order2 = new Order(200, new ExpressShipping());
console.log('Order 2 (Express):', order2.calculateTotal());   // 215

const order3 = new Order(50, new StandardShipping());
order3.setShippingStrategy(new InternationalShipping());
console.log('Order 3 (International):', order3.calculateTotal()); // 75

Conclusion

Design patterns are powerful tools for JavaScript developers to build robust, scalable, and maintainable applications. By understanding and applying these patterns, developers can leverage proven solutions to common problems, write cleaner code, and foster better collaboration within their teams. While not a silver bullet, judicious use of design patterns significantly elevates the quality of software design.