Design Patterns in Modern JavaScript and TypeScript


Design Patterns in Modern JavaScript and TypeScript




Prefer to listen?


Design patterns are proven solutions to common problems in software design. They provide a template for writing maintainable, scalable, and robust code. Let's take a look at several design patterns and their implementation in modern JavaScript and TypeScript.

1. Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.

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

  getInstance() {
    return this;
  }
}

const singleton1 = new Singleton();
const singleton2 = new Singleton();

console.log(singleton1 === singleton2); // Output: true
TypeScript Implementation:
class Singleton {
  private static instance: Singleton;

  private constructor() {}

  static getInstance(): Singleton {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

const singleton1 = Singleton.getInstance();
const singleton2 = Singleton.getInstance();

console.log(singleton1 === singleton2); // Output: true

2. Factory Pattern

The Factory pattern provides a way to create objects without specifying the exact class of the object that will be created.

JavaScript Implementation:
class Car {
  constructor() {
    this.type = 'Car';
  }
}

class Truck {
  constructor() {
    this.type = 'Truck';
  }
}

class VehicleFactory {
  static createVehicle(vehicleType) {
    switch (vehicleType) {
      case 'car':
        return new Car();
      case 'truck':
        return new Truck();
      default:
        throw new Error('Unknown vehicle type');
    }
  }
}

const car = VehicleFactory.createVehicle('car');
const truck = VehicleFactory.createVehicle('truck');

console.log(car.type); // Output: Car
console.log(truck.type); // Output: Truck
TypeScript Implementation:
interface Vehicle {
  type: string;
}

class Car implements Vehicle {
  type = 'Car';
}

class Truck implements Vehicle {
  type = 'Truck';
}

class VehicleFactory {
  static createVehicle(vehicleType: 'car' | 'truck'): Vehicle {
    switch (vehicleType) {
      case 'car':
        return new Car();
      case 'truck':
        return new Truck();
      default:
        throw new Error('Unknown vehicle type');
    }
  }
}

const car = VehicleFactory.createVehicle('car');
const truck = VehicleFactory.createVehicle('truck');

console.log(car.type); // Output: Car
console.log(truck.type); // Output: Truck

3. Observer Pattern

The Observer pattern defines a one-to-many relationship between objects so that when one object changes state, all its dependents are notified and updated automatically.

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

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

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

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

class Observer {
  update() {
    console.log('Observer has been notified');
  }
}

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

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

subject.notifyObservers();
// Output: 
// Observer has been notified
// Observer has been notified
TypeScript Implementation:
class Subject {
  private observers: Observer[] = [];

  addObserver(observer: Observer): void {
    this.observers.push(observer);
  }

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

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

interface Observer {
  update(): void;
}

class ConcreteObserver implements Observer {
  update(): void {
    console.log('Observer has been notified');
  }
}

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

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

subject.notifyObservers();
// Output: 
// Observer has been notified
// Observer has been notified

4. Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern allows the algorithm to vary independently from clients that use it.

JavaScript Implementation:
class Context {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  executeStrategy() {
    return this.strategy.doAlgorithm();
  }
}

class ConcreteStrategyA {
  doAlgorithm() {
    return 'Algorithm A';
  }
}

class ConcreteStrategyB {
  doAlgorithm() {
    return 'Algorithm B';
  }
}

const context = new Context(new ConcreteStrategyA());
console.log(context.executeStrategy()); // Output: Algorithm A

context.setStrategy(new ConcreteStrategyB());
console.log(context.executeStrategy()); // Output: Algorithm B
TypeScript Implementation:
interface Strategy {
  doAlgorithm(): string;
}

class ConcreteStrategyA implements Strategy {
  doAlgorithm(): string {
    return 'Algorithm A';
  }
}

class ConcreteStrategyB implements Strategy {
  doAlgorithm(): string {
    return 'Algorithm B';
  }
}

class Context {
  private strategy: Strategy;

  constructor(strategy: Strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy: Strategy): void {
    this.strategy = strategy;
  }

  executeStrategy(): string {
    return this.strategy.doAlgorithm();
  }
}

const context = new Context(new ConcreteStrategyA());
console.log(context.executeStrategy()); // Output: Algorithm A

context.setStrategy(new ConcreteStrategyB());
console.log(context.executeStrategy()); // Output: Algorithm B

5. Decorator Pattern

The Decorator pattern allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class.

JavaScript Implementation:
class Coffee {
  cost() {
    return 5;
  }
}

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

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

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

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

let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);

console.log(coffee.cost()); // Output: 6.5
TypeScript Implementation:
interface Coffee {
  cost(): number;
}

class SimpleCoffee implements Coffee {
  cost(): number {
    return 5;
  }
}

class MilkDecorator implements Coffee {
  constructor(private coffee: Coffee) {}

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

class SugarDecorator implements Coffee {
  constructor(private coffee: Coffee) {}

  cost(): number {
    return this.coffee.cost() + 0.5;
  }
}

let coffee: Coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);

console.log(coffee.cost()); // Output: 6.5

Design patterns are essential tools for building robust, scalable, and maintainable software. By understanding and applying these patterns in JavaScript and TypeScript, you can solve common design problems more efficiently and improve the overall quality of your code. Each pattern has its use case and knowing when to apply them is key to leveraging their full potential.


FAQs about Design Patterns in Modern JavaScript and TypeScript

What are design patterns and why are they important in software development?

Design patterns are proven solutions to common problems in software design. They provide templates for writing maintainable, scalable, and robust code, helping developers solve recurring issues more efficiently.

How does the Singleton pattern ensure a class has only one instance?

The Singleton pattern restricts the instantiation of a class to a single object. It achieves this by maintaining a private static instance of the class and providing a global access method that returns this instance.

What is the main advantage of using the Factory pattern?

The Factory pattern abstracts the instantiation process, allowing the creation of objects without specifying the exact class of the object that will be created. This promotes loose coupling and enhances code maintainability.

How does the Observer pattern work and where is it commonly used?

The Observer pattern defines a one-to-many relationship between objects. When one object (subject) changes state, all its dependents (observers) are notified and updated automatically. It is commonly used in event-driven programming, such as in UI frameworks.

Why would you use the Decorator pattern in your code?

The Decorator pattern allows you to dynamically add behavior to an object without modifying its class. This provides a flexible alternative to subclassing for extending functionality, enabling more modular and maintainable code.





Comments
Subscribe for Updates
Subscribe and get tech info sent right to your mailbox!



What's in the newsletter?

Here's More