Browse TypeScript Design Patterns & Application Architecture

Dynamic Request Handling with Chain of Responsibility Pattern

Explore how the Chain of Responsibility Pattern enables dynamic request handling in TypeScript, allowing for flexible and scalable software design.

6.1.2 Handling Requests Dynamically

In the realm of software design patterns, the Chain of Responsibility (CoR) pattern stands out for its ability to handle requests dynamically. This pattern is particularly useful when you need to decouple the sender of a request from its receiver, allowing multiple objects the opportunity to handle the request. In this section, we will delve into how the Chain of Responsibility pattern facilitates dynamic request handling, enabling the addition, removal, or rearrangement of handlers without altering client code.

Understanding the 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 one of them handles the request. This pattern is particularly useful in scenarios where multiple handlers might be capable of processing a request, and the exact handler is determined at runtime.

Key Concepts

  • Handler: An object that processes requests. Each handler decides whether to process a request or pass it to the next handler in the chain.
  • Chain: A sequence of handlers. Requests are passed along this chain until they are handled.
  • Decoupling: The pattern decouples the sender of a request from its receivers, promoting flexibility and reusability.

Dynamic Configuration of Handlers

One of the primary benefits of the Chain of Responsibility pattern is its ability to dynamically configure handlers. This flexibility is achieved through several strategies:

Adding, Removing, or Rearranging Handlers

The Chain of Responsibility pattern allows you to modify the chain of handlers without changing the client code. This is particularly beneficial in systems where the handling logic needs to be adjusted frequently based on runtime conditions or configurations.

Example: Dynamic Handler Configuration

 1class Request {
 2  constructor(public type: string) {}
 3}
 4
 5interface Handler {
 6  setNext(handler: Handler): Handler;
 7  handle(request: Request): void;
 8}
 9
10abstract class AbstractHandler implements Handler {
11  private nextHandler: Handler | null = null;
12
13  public setNext(handler: Handler): Handler {
14    this.nextHandler = handler;
15    return handler;
16  }
17
18  public handle(request: Request): void {
19    if (this.nextHandler) {
20      this.nextHandler.handle(request);
21    }
22  }
23}
24
25class ConcreteHandlerA extends AbstractHandler {
26  public handle(request: Request): void {
27    if (request.type === 'A') {
28      console.log('Handler A processed the request.');
29    } else {
30      super.handle(request);
31    }
32  }
33}
34
35class ConcreteHandlerB extends AbstractHandler {
36  public handle(request: Request): void {
37    if (request.type === 'B') {
38      console.log('Handler B processed the request.');
39    } else {
40      super.handle(request);
41    }
42  }
43}
44
45// Client code
46const handlerA = new ConcreteHandlerA();
47const handlerB = new ConcreteHandlerB();
48
49// Dynamically configure the chain
50handlerA.setNext(handlerB);
51
52const request = new Request('B');
53handlerA.handle(request); // Output: Handler B processed the request.

In this example, the chain is configured dynamically by setting the next handler. You can easily add, remove, or rearrange handlers without modifying the client code.

Impact of Handler Order

The order of handlers in the chain can significantly affect request processing. The first handler capable of processing the request will do so, and subsequent handlers will not be invoked.

Example: Order of Handlers

1// Reversing the order of handlers
2handlerB.setNext(handlerA);
3
4const requestA = new Request('A');
5handlerB.handle(requestA); // Output: Handler A processed the request.

By reversing the order of handlers, we change which handler processes a given request. This demonstrates the importance of carefully managing handler order to achieve the desired behavior.

Dynamic Configuration Based on Runtime Conditions

Handlers can be configured dynamically based on runtime conditions or external configurations, such as configuration files or environment variables. This approach enhances the flexibility and adaptability of the system.

Using Configuration Files

Configuration files can be used to define the order and types of handlers in the chain. This allows for easy reconfiguration without modifying the codebase.

Example: Configuration File for Handlers

1{
2  "handlers": ["ConcreteHandlerA", "ConcreteHandlerB"]
3}

Loading Configuration in TypeScript

 1import * as fs from 'fs';
 2
 3function loadHandlersFromConfig(configPath: string): Handler {
 4  const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
 5  const handlers: Handler[] = config.handlers.map((handlerName: string) => {
 6    switch (handlerName) {
 7      case 'ConcreteHandlerA':
 8        return new ConcreteHandlerA();
 9      case 'ConcreteHandlerB':
10        return new ConcreteHandlerB();
11      default:
12        throw new Error(`Unknown handler: ${handlerName}`);
13    }
14  });
15
16  for (let i = 0; i < handlers.length - 1; i++) {
17    handlers[i].setNext(handlers[i + 1]);
18  }
19
20  return handlers[0];
21}
22
23const rootHandler = loadHandlersFromConfig('handlers.json');
24const requestC = new Request('C');
25rootHandler.handle(requestC); // No output, as no handler for 'C'

In this example, handlers are loaded and configured based on a JSON configuration file. This approach allows for flexible and dynamic configuration of the handler chain.

Implementing Handler Registration Methods

Handler registration methods can be used to dynamically register handlers at runtime. This is particularly useful in plugin-based systems where handlers can be added or removed based on available plugins.

Example: Handler Registration

 1class HandlerRegistry {
 2  private handlers: Handler[] = [];
 3
 4  public register(handler: Handler): void {
 5    this.handlers.push(handler);
 6  }
 7
 8  public getChain(): Handler | null {
 9    if (this.handlers.length === 0) return null;
10
11    for (let i = 0; i < this.handlers.length - 1; i++) {
12      this.handlers[i].setNext(this.handlers[i + 1]);
13    }
14
15    return this.handlers[0];
16  }
17}
18
19const registry = new HandlerRegistry();
20registry.register(new ConcreteHandlerA());
21registry.register(new ConcreteHandlerB());
22
23const dynamicChain = registry.getChain();
24const requestD = new Request('D');
25dynamicChain?.handle(requestD); // No output, as no handler for 'D'

This example demonstrates how handlers can be registered dynamically, allowing for flexible and scalable request handling.

Managing Complexity in the Chain

As the number of handlers grows, managing the chain can become complex. It is crucial to implement strategies to ensure that the chain remains manageable and does not become overly complex.

Strategies for Managing Complexity

  • Modularize Handlers: Break down handlers into smaller, reusable components. This promotes reusability and simplifies maintenance.
  • Use Descriptive Names: Ensure that handlers have descriptive names that clearly indicate their purpose. This aids in understanding and managing the chain.
  • Document the Chain: Maintain documentation that describes the purpose and order of handlers in the chain. This is particularly important in large systems with complex chains.
  • Limit Chain Length: Avoid excessively long chains, as they can become difficult to manage and debug. Consider breaking down long chains into smaller, more manageable sub-chains.

Benefits of Dynamic Request Handling

The dynamic handling of requests using the Chain of Responsibility pattern offers several benefits:

  • Flexibility: Handlers can be added, removed, or rearranged without altering client code, allowing for easy adaptation to changing requirements.
  • Scalability: The pattern supports the addition of new handlers as the system grows, promoting scalability.
  • Decoupling: The pattern decouples the sender of a request from its receivers, enhancing modularity and reusability.
  • Maintainability: By organizing request handling logic into separate handlers, the pattern promotes maintainability and simplifies debugging.

Conclusion

The Chain of Responsibility pattern is a powerful tool for handling requests dynamically in TypeScript. By allowing for the dynamic configuration of handlers, the pattern provides flexibility, scalability, and maintainability. By implementing strategies to manage complexity, you can ensure that the chain remains manageable and effective.

Remember, this is just the beginning. As you progress, you’ll build more complex and interactive systems using the Chain of Responsibility pattern. Keep experimenting, stay curious, and enjoy the journey!

Quiz Time!

Loading quiz…
Revised on Thursday, April 23, 2026