DEV Community

Akram Mahmoud
Akram Mahmoud

Posted on • Edited on

[Design Pattern] Chain of responsibility in Typescript with real world example

The Chain of Responsibility design pattern is a behavioral pattern that allows for a chain of objects to handle requests.

Each object in the chain has the ability to either handle the request or pass it along to the next object in the chain.

This pattern is useful when there are multiple objects that can handle a request and the object that handles the request needs to be determined at runtime.

It promotes loose coupling between the sender of the request and the receiver, and it provides a way to reduce the dependency on specific objects.

However, care must be taken to avoid creating very long chains, as this can impact performance and make the code difficult to maintain.b

real-world example of the Chain of Responsibility pattern in TypeScript:

Suppose you're building an online store that sells various products. You want to implement a system for applying discounts to orders based on the type of products being purchased. You have the following product types:

Electronics
Books
Clothing
Toys
You want to apply the following discounts based on the product type:

Electronics: 10% discount
Books: 15% discount
Clothing: 20% discount
Toys: no discount

here's the same example code without using the Chain of Responsibility design pattern:


class DiscountCalculator {
  public calculateDiscount(products: string[], originalPrice: number): number {
    let discount = 0;

    if (products.includes('Electronics')) {
      discount = originalPrice * 0.1;
    } else if (products.includes('Books')) {
      discount = originalPrice * 0.15;
    } else if (products.includes('Clothing')) {
      discount = originalPrice * 0.2;
    }

    return originalPrice - discount;
  }
}

const calculator = new DiscountCalculator();

console.log(calculator.calculateDiscount(['Electronics', 'Clothing'], 100)); // Output: 72
console.log(calculator.calculateDiscount(['Books'], 100)); // Output: 85
console.log(calculator.calculateDiscount(['Toys'], 100)); // Output: 100
console.log(calculator.calculateDiscount(['Books', 'Clothing'], 100)); // Output: 64

Enter fullscreen mode Exit fullscreen mode

To implement this system, you could use the Chain of Responsibility pattern as follows:

interface DiscountHandler {
  setNext(handler: DiscountHandler): DiscountHandler;
  applyDiscount(products: string[], originalPrice: number): number;
}

abstract class AbstractDiscountHandler implements DiscountHandler {
  private nextHandler: DiscountHandler;

  public setNext(handler: DiscountHandler): DiscountHandler {
    this.nextHandler = handler;
    return handler;
  }

  public applyDiscount(products: string[], originalPrice: number): number {
    if (this.canHandle(products)) {
      return this.handleDiscount(originalPrice);
    } else if (this.nextHandler) {
      return this.nextHandler.applyDiscount(products, originalPrice);
    } else {
      return originalPrice;
    }
  }

  protected abstract canHandle(products: string[]): boolean;
  protected abstract handleDiscount(originalPrice: number): number;
}

class ElectronicsDiscountHandler extends AbstractDiscountHandler {
  protected canHandle(products: string[]): boolean {
    return products.some(p => p === 'Electronics');
  }

  protected handleDiscount(originalPrice: number): number {
    return originalPrice * 0.9;
  }
}

class BooksDiscountHandler extends AbstractDiscountHandler {
  protected canHandle(products: string[]): boolean {
    return products.some(p => p === 'Books');
  }

  protected handleDiscount(originalPrice: number): number {
    return originalPrice * 0.85;
  }
}

class ClothingDiscountHandler extends AbstractDiscountHandler {
  protected canHandle(products: string[]): boolean {
    return products.some(p => p === 'Clothing');
  }

  protected handleDiscount(originalPrice: number): number {
    return originalPrice * 0.8;
  }
}

class ToysDiscountHandler extends AbstractDiscountHandler {
  protected canHandle(products: string[]): boolean {
    return products.some(p => p === 'Toys');
  }

  protected handleDiscount(originalPrice: number): number {
    return originalPrice;
  }
}

class DiscountCalculator {
  private discountHandler: DiscountHandler;

  constructor() {
    const electronicsHandler = new ElectronicsDiscountHandler();
    const booksHandler = new BooksDiscountHandler();
    const clothingHandler = new ClothingDiscountHandler();
    const toysHandler = new ToysDiscountHandler();

    electronicsHandler.setNext(booksHandler).setNext(clothingHandler).setNext(toysHandler);

    this.discountHandler = electronicsHandler;
  }

  public calculateDiscount(products: string[], originalPrice: number): number {
    return this.discountHandler.applyDiscount(products, originalPrice);
  }
}

const calculator = new DiscountCalculator();

console.log(calculator.calculateDiscount(['Electronics', 'Clothing'], 100)); // Output: 72
console.log(calculator.calculateDiscount(['Books'], 100)); // Output: 85
console.log(calculator.calculateDiscount(['Toys'], 100)); // Output: 100
console.log(calculator.calculateDiscount(['Books', 'Clothing'], 100)); // Output: 64

Enter fullscreen mode Exit fullscreen mode

why should use chain of responsibility design pattern?

some potential benefits of using the Chain of Responsibility design pattern:

  • Decouples the sender and receiver of a request: The pattern allows for more flexibility and extensibility by reducing the direct coupling between the object that initiates a request and the object that handles it. This can make it easier to add, remove, or modify handlers without affecting the rest of the system.
  • Promotes a more modular design: By breaking up a complex set of processing logic into smaller, more focused handlers, the Chain of Responsibility pattern can promote a more modular and reusable design.
  • Enables dynamic handling of requests: The pattern allows for dynamic assignment and reassignment of handlers at runtime, giving developers more control over how requests are processed.
  • Simplifies the design of client code: With the Chain of Responsibility pattern, client code does not need to know which handler will ultimately process a request, as this is determined at runtime. This can simplify the code required to initiate and process requests.
  • Supports open/closed principle: The Chain of Responsibility pattern can help satisfy the open/closed principle by allowing new handlers to be added without modifying existing code. This can promote a more stable and maintainable system over time.

Top comments (0)