Explore the implementation of the Chain of Responsibility pattern using TypeScript's type system for enhanced reliability and error checking.
In this section, we will delve into the implementation of the Chain of Responsibility pattern using TypeScript. This pattern is particularly useful for scenarios where multiple handlers can process a request, and the request is passed along a chain until a suitable handler is found. TypeScript’s static typing provides an added layer of reliability, ensuring that handlers conform to expected interfaces and catching errors at compile time.
The Chain of Responsibility pattern allows an object to send a command without knowing which object will handle it. This is achieved by passing the command along a chain of potential handlers until one of them handles the command. This pattern promotes loose coupling and enhances flexibility in assigning responsibilities to objects.
We start by defining an interface for our handlers. This interface will include a method for handling requests and a method for setting the next handler in the chain.
interface Handler {
setNext(handler: Handler): Handler;
handle(request: string): void;
}
setNext(handler: Handler): Handler: This method sets the next handler in the chain and returns the handler to allow chaining.handle(request: string): void: This method processes the request. If the handler cannot process it, it passes the request to the next handler.Next, we implement concrete handlers that will process specific types of requests. Each handler will decide whether to handle the request or pass it to the next handler.
class ConcreteHandlerA implements Handler {
private nextHandler: Handler | null = null;
public setNext(handler: Handler): Handler {
this.nextHandler = handler;
return handler;
}
public handle(request: string): void {
if (request === 'A') {
console.log('ConcreteHandlerA handled the request.');
} else if (this.nextHandler) {
this.nextHandler.handle(request);
}
}
}
class ConcreteHandlerB implements Handler {
private nextHandler: Handler | null = null;
public setNext(handler: Handler): Handler {
this.nextHandler = handler;
return handler;
}
public handle(request: string): void {
if (request === 'B') {
console.log('ConcreteHandlerB handled the request.');
} else if (this.nextHandler) {
this.nextHandler.handle(request);
}
}
}
Handler interface. They check if they can handle the request; if not, they pass it to the next handler.Now, we set up the chain of handlers. The client will initiate the request, and it will be passed along the chain until a handler processes it.
const handlerA = new ConcreteHandlerA();
const handlerB = new ConcreteHandlerB();
handlerA.setNext(handlerB);
// Client code
const request = 'B';
handlerA.handle(request);
setNext method.In some cases, a handler might need to handle requests differently based on additional parameters. TypeScript allows us to define optional parameters and default values to handle such scenarios.
class ConcreteHandlerC implements Handler {
private nextHandler: Handler | null = null;
public setNext(handler: Handler): Handler {
this.nextHandler = handler;
return handler;
}
public handle(request: string, context?: any): void {
if (request === 'C' && context?.priority === 'high') {
console.log('ConcreteHandlerC handled the high-priority request.');
} else if (this.nextHandler) {
this.nextHandler.handle(request, context);
}
}
}
handle method includes an optional context parameter, allowing handlers to make decisions based on additional information.To better understand how the Chain of Responsibility pattern works, let’s visualize the flow of a request through the chain of handlers.
graph LR
A[Client] --> B[ConcreteHandlerA]
B -->|Passes request| C[ConcreteHandlerB]
C -->|Passes request| D[ConcreteHandlerC]
D -->|Handles request| E[End]
ConcreteHandlerA. If ConcreteHandlerA cannot handle it, the request is passed to ConcreteHandlerB, and so on, until a handler processes the request or it reaches the end of the chain.Now that we’ve covered the basics, try modifying the code to add more handlers or change the conditions under which each handler processes requests. Experiment with different request types and see how the chain adapts.
Before we wrap up, let’s reinforce what we’ve learned with a few questions and exercises.
ConcreteHandlerC to handle requests based on a different context parameter.Remember, mastering design patterns takes practice and experimentation. As you continue to explore and implement different patterns, you’ll gain a deeper understanding of how to structure your code for maintainability and scalability. Keep experimenting, stay curious, and enjoy the journey!