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!
Enjoyed this post? Check out more articles on my blog.
View all posts
Comments