Chain of Responsibility Pattern in TypeScript — ✨ Behavioral Design Pattern #1

365kim
2 min readJul 8, 2023

--

Photo by Possessed Photography on Unsplash

In software development, managing complex systems with numerous subsystems can often become a challenging task. Fortunately, the Chain of Responsibility Pattern exists to help simplify and streamline these interactions.

🙋🏻‍♀️ What is Chain of Responsibility Pattern?

The Chain of Responsibility Pattern is a behavioral design pattern that allows an object to pass a request along a chain of potential handlers until the request is handled or reaches the end of the chain.

The main idea behind the Chain of Responsibility Pattern is to create a chain of handler objects, where each handler can either handle the request or pass it on to the next handler in the chain. This way, the client making the request is unaware of which specific handler will process it, allowing for loose coupling and flexibility.

✨ Simple TypeScript Example

Your online shopping application needs to handle various types of discount requests. The discount calculation depends on factors such as the total order amount, the customers’ loyalty level, and any ongoing seasonal promotional offers. The Chain of Responsibility Pattern can help manage this complex discount calculation process.

First, define a base handler DiscountHandler, with a method setNext to set the next handler in the chain and a method handle to process the discount request.

abstract class DiscountHandler {
protected next: DiscountHandler | null = null;

setNext(handler: DiscountHandler): void {
this.next = handler;
}

abstract handle(request: DiscountRequest): number;
}

Next, create each concrete handler class that extends DiscountHandler.

class OrderAmountDiscountHandler extends DiscountHandler {
handle(request: DiscountRequest): number {
if (request.orderAmount >= 100) return 20;
if (this.next) return this.next.handle(request);
return 0;
}
}

class LoyaltyDiscountHandler extends DiscountHandler {
handle(request: DiscountRequest): number {
if (request.customerLoyaltyLevel >= 5) return request.orderAmount * 0.05;
if (this.next) return this.next.handle(request);
return 0;
}
}


class PromotionalOfferDiscountHandler extends DiscountHandler {
handle(request: DiscountRequest): number {
if (request.promotionalOfferApplicable) return request.orderAmount * 0.01;
if (this.next) return this.next.handle(request);
return 0;
}
}

To utilize it, create an instance of each handler and set up the chain in sequence. When a discount request is made, it is passed through the chain of handlers.

// Client Code

const orderAmountDiscountHandler = new OrderAmountDiscountHandler();
const loyaltyDiscountHandler = new LoyaltyDiscountHandler();
const promotionalOfferDiscountHandler = new PromotionalOfferDiscountHandler();

const discountHandler = orderAmountDiscountHandler
discountHandler.setNext(loyaltyDiscountHandler);
loyaltyDiscountHandler.setNext(promotionalOfferDiscountHandler);


const request: DiscountRequest = {
orderAmount: 200,
customerLoyaltyLevel: 7,
promotionalOfferApplicable: true,
};

const totalDiscount = discountHandler.handle(request);
console.log(totalDiscount); // 32 (20 + 10 + 2)

🧑🏻‍💻 Use it or Avoid it

When to use it:

  • To decouple the sender of a request from its receivers, providing flexibility in handling requests dynamically.
  • To reduce the coupling between the client and the receiver objects, allowing for easier maintenance and extensibility.

When to avoid it:

  • If the request processing logic is simple and doesn’t require a chain of potential handlers.

--

--

365kim

Web Front-End Developer who believes Every Day Counts!