Design Patterns 101: Understanding Chain of Responsibility Pattern in TypeScript
The Chain of Responsibility (CoR) is a behavioral design pattern that enables objects to forward requests along a chain of handlers. This approach decouples senders from receivers while providing flexibility in handling various request types.
The fundamental concept: “Each handler in the chain decides either to process the request or pass it to the next handler in the chain.”
Understanding the Basics
Participants in the Pattern
- Handler Interface/Abstract Class — Establishes the contract for handling requests
- Concrete Handlers — Implement the actual processing logic and determine whether to handle or forward requests
- Client — Initiates the request and starts the chain flow
Implementation in TypeScript
Setting Up the Handlers
Let’s start by creating a base handler interface and abstract class. Each concrete handler extends this base and implements its own handling logic:
interface Handler {
setNext(handler: Handler): Handler;
handleRequest(request: string): string | null;
}
abstract class AbstractHandler implements Handler {
private nextHandler: Handler | null = null;
public setNext(handler: Handler): Handler {
this.nextHandler = handler;
return handler;
}
public handleRequest(request: string): string | null {
if (this.nextHandler) {
return this.nextHandler.handleRequest(request);
}
return null;
}
}
Creating Concrete Handlers
Now let’s implement specific handlers that process different types of requests:
class ConcreteHandlerA extends AbstractHandler {
public handleRequest(request: string): string | null {
if (request === 'A') {
return `Handler A is processing the request: ${request}`;
}
return super.handleRequest(request);
}
}
class ConcreteHandlerB extends AbstractHandler {
public handleRequest(request: string): string | null {
if (request === 'B') {
return `Handler B is processing the request: ${request}`;
}
return super.handleRequest(request);
}
}
Using the Chain
Here’s how to wire up the chain and process requests:
const handlerA = new ConcreteHandlerA();
const handlerB = new ConcreteHandlerB();
// Build the chain
handlerA.setNext(handlerB);
// Client initiates requests
const resultA = handlerA.handleRequest('A');
// Output: "Handler A is processing the request: A"
const resultB = handlerA.handleRequest('B');
// Output: "Handler B is processing the request: B"
const resultC = handlerA.handleRequest('C');
// Output: null (request not handled by any handler)
Real-World Use Cases
The Chain of Responsibility pattern is particularly useful in scenarios requiring flexible request handling:
1. Multi-Step Form Submissions
class ValidationHandler extends AbstractHandler {
handleRequest(formData: FormData): FormData | null {
if (this.isValid(formData)) {
return super.handleRequest(formData);
}
throw new Error("Validation failed");
}
}
class SanitizationHandler extends AbstractHandler {
handleRequest(formData: FormData): FormData | null {
const sanitized = this.sanitize(formData);
return super.handleRequest(sanitized);
}
}
class SubmissionHandler extends AbstractHandler {
handleRequest(formData: FormData): FormData | null {
this.submitToDatabase(formData);
return formData;
}
}
2. Authentication/Authorization Workflows
class AuthenticationHandler extends AbstractHandler {
handleRequest(request: Request): Response | null {
if (this.isAuthenticated(request)) {
return super.handleRequest(request);
}
return new Response("Unauthorized", { status: 401 });
}
}
class AuthorizationHandler extends AbstractHandler {
handleRequest(request: Request): Response | null {
if (this.isAuthorized(request)) {
return super.handleRequest(request);
}
return new Response("Forbidden", { status: 403 });
}
}
3. Data Processing Pipelines
class DataFetchHandler extends AbstractHandler {
handleRequest(query: Query): Data | null {
const data = this.fetchFromDatabase(query);
return super.handleRequest(data);
}
}
class DataTransformHandler extends AbstractHandler {
handleRequest(data: Data): Data | null {
const transformed = this.transform(data);
return super.handleRequest(transformed);
}
}
class DataCacheHandler extends AbstractHandler {
handleRequest(data: Data): Data | null {
this.cacheData(data);
return data;
}
}
Benefits
- Decoupling — Sender doesn’t need to know which handler will process the request
- Flexibility — Easy to add or remove handlers from the chain
- Single Responsibility — Each handler focuses on one specific task
- Open/Closed Principle — Add new handlers without modifying existing code
Considerations
- Performance — Long chains can impact performance
- Debugging — Tracing the flow through multiple handlers can be complex
- Guaranteed Handling — No guarantee a request will be handled (might reach end of chain)
Conclusion
The Chain of Responsibility pattern facilitates decoupling request senders from receivers. It’s an excellent choice for scenarios requiring:
- Multi-step processing
- Flexible request routing
- Conditional handling based on request type
By understanding and implementing this pattern, you can create more maintainable and flexible systems that adapt easily to changing requirements.
Next in the series: Strategy Pattern - choosing algorithms at runtime
Have questions about implementing Chain of Responsibility in your project? Let’s discuss in the comments!
Written by Abisoye Alli-Balogun
Full Stack Product Engineer building scalable distributed systems and high-performance applications. Passionate about microservices, cloud architecture, and creating delightful user experiences.
Enjoyed this post? Check out more articles on my blog.
View all posts
Comments