Lesson 14-JavaScript Design Patterns

Singleton Pattern

Ensures a class has only one instance and provides a global access point to it.

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

  // Other methods...
}

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

Factory Pattern

Provides an interface for creating objects while hiding the creation logic, making the code more flexible.

function createShape(type) {
  switch (type) {
    case 'circle':
      return new Circle();
    case 'rectangle':
      return new Rectangle();
    default:
      throw new Error('Invalid shape type');
  }
}

class Shape {}
class Circle extends Shape {}
class Rectangle extends Shape {}

const circle = createShape('circle');
const rectangle = createShape('rectangle');

Builder Pattern

Separates the construction of a complex object from its representation, allowing the same construction process to create different representations.

class UserBuilder {
  constructor() {
    this.user = {};
  }

  setName(name) {
    this.user.name = name;
    return this;
  }

  setEmail(email) {
    this.user.email = email;
    return this;
  }

  build() {
    return this.user;
  }
}

const user = new UserBuilder()
  .setName('John Doe')
  .setEmail('john.doe@example.com')
  .build();

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.

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(data) {
    this.observers.forEach((observer) => observer.update(data));
  }
}

class Observer {
  update(data) {
    console.log('Received update:', data);
  }
}

const subject = new Subject();
const observer = new Observer();

subject.addObserver(observer);
subject.notifyObservers({ message: 'Hello, observers!' }); // Output: Received update: { message: 'Hello, observers!' }

Strategy Pattern

Defines a family of algorithms, encapsulates each one in a separate class, and makes them interchangeable, allowing the algorithm to vary independently from the clients that use it.

class PaymentStrategy {
  calculate(price) {
    throw new Error('Not implemented');
  }
}

class CashPayment extends PaymentStrategy {
  calculate(price) {
    return price;
  }
}

class CreditCardPayment extends PaymentStrategy {
  calculate(price) {
    return price * 1.05; // Add 5% surcharge for credit card payments
  }
}

class ShoppingCart {
  constructor(paymentStrategy) {
    this.paymentStrategy = paymentStrategy;
  }

  setPaymentStrategy(strategy) {
    this.paymentStrategy = strategy;
  }

  checkout(totalPrice) {
    const amountToPay = this.paymentStrategy.calculate(totalPrice);
    console.log(`Total amount to pay: ${amountToPay}`);
  }
}

const cart = new ShoppingCart(new CashPayment());
cart.checkout(100); // Output: Total amount to pay: 100

cart.setPaymentStrategy(new CreditCardPayment());
cart.checkout(100); // Output: Total amount to pay: 105

Decorator Pattern

Dynamically adds new functionality to an object while keeping its interface unchanged, without modifying the original code.

class Coffee {
  constructor(description) {
    this.description = description;
  }

  getCost() {
    return 1;
  }

  getDescription() {
    return this.description;
  }
}

class MilkDecorator extends Coffee {
  constructor(coffee) {
    super(coffee.getDescription());
    this.coffee = coffee;
  }

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

  getDescription() {
    return `${this.coffee.getDescription()}, with milk`;
  }
}

const coffee = new Coffee('Regular coffee');
console.log(coffee.getCost()); // Output: 1
console.log(coffee.getDescription()); // Output: Regular coffee

const milkCoffee = new MilkDecorator(coffee);
console.log(milkCoffee.getCost()); // Output: 1.7
console.log(milkCoffee.getDescription()); // Output: Regular coffee, with milk

Adapter Pattern

Converts the interface of a class into another interface that a client expects, allowing classes with incompatible interfaces to work together.

class TargetInterface {
  request() {
    throw new Error('Not implemented');
  }
}

class Adaptee {
  specificRequest() {
    return 'Adaptee response';
  }
}

class Adapter extends TargetInterface {
  constructor(adaptee) {
    super();
    this.adaptee = adaptee;
  }

  request() {
    return `Adapter: ${this.adaptee.specificRequest()}`;
  }
}

const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
console.log(adapter.request()); // Output: Adapter: Adaptee response

Proxy Pattern

Provides a surrogate or placeholder for another object to control access to it, allowing additional functionality like access control, lazy loading, caching, or logging.

class RealSubject {
  expensiveOperation() {
    console.log('Performing expensive operation...');
    return 'RealSubject result';
  }
}

class ProxySubject {
  constructor(realSubject) {
    this.realSubject = realSubject;
  }

  expensiveOperation() {
    if (this.checkAccess()) {
      return this.realSubject.expensiveOperation();
    } else {
      console.log('Access denied');
      return null;
    }
  }

  checkAccess() {
    // Simulate access check
    return Math.random() > 0.5;
  }
}

const realSubject = new RealSubject();
const proxySubject = new ProxySubject(realSubject);

proxySubject.expensiveOperation(); // May output: Performing expensive operation..., or Access denied

Facade Pattern

Provides a simplified, unified interface to a complex subsystem, making it easier for external code to interact with it.

class SubsystemA {
  methodA() {
    console.log('Subsystem A method called');
  }
}

class SubsystemB {
  methodB() {
    console.log('Subsystem B method called');
  }
}

class Facade {
  constructor(subsystemA, subsystemB) {
    this.subsystemA = subsystemA;
    this.subsystemB = subsystemB;
  }

  executeComplexWorkflow() {
    this.subsystemA.methodA();
    this.subsystemB.methodB();
    console.log('Additional logic in Facade');
  }
}

const subsystemA = new SubsystemA();
const subsystemB = new SubsystemB();
const facade = new Facade(subsystemA, subsystemB);

facade.executeComplexWorkflow();
// Output:
// Subsystem A method called
// Subsystem B method called
// Additional logic in Facade

Mediator Pattern

Defines a mediator object that encapsulates interactions between a set of objects, reducing coupling and promoting loose coupling between them.

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

  send(message) {
    this.mediator.send(message, this);
  }

  receive(message, sender) {
    console.log(`${this.constructor.name} received: ${message} from ${sender.constructor.name}`);
  }
}

class ConcreteColleagueA extends Colleague {
  doSomething() {
    this.send('Action A');
  }
}

class ConcreteColleagueB extends Colleague {
  doSomething() {
    this.send('Action B');
  }
}

class Mediator {
  constructor() {
    this.colleagues = [];
  }

  register(colleague) {
    this.colleagues.push(colleague);
  }

  send(message, sender) {
    this.colleagues.forEach((colleague) => {
      if (colleague !== sender) {
        colleague.receive(message, sender);
      }
    });
  }
}

const mediator = new Mediator();
const colleagueA = new ConcreteColleagueA(mediator);
const colleagueB = new ConcreteColleagueB(mediator);

mediator.register(colleagueA);
mediator.register(colleagueB);

colleagueA.doSomething();
// Output: ConcreteColleagueB received: Action A from ConcreteColleagueA

colleagueB.doSomething();
// Output: ConcreteColleagueA received: Action B from ConcreteColleagueB

Iterator Pattern

Provides a way to sequentially access the elements of an aggregate object without exposing its internal representation.

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

  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.items.length) {
          return { value: this.items[index++], done: false };
        } else {
          return { done: true };
        }
      },
    };
  }
}

const collection = new Collection([1, 2, 3, 4, 5]);

for (const item of collection) {
  console.log(item);
}
// Output:
// 1
// 2
// 3
// 4
// 5
Share your love