Understanding the Chain of Responsibility Pattern
The Chain of Responsibility pattern is a behavioral design pattern that passes a request along a chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain.
Problem
Imagine you're developing an authentication and authorization system for a web application. The system needs to perform several checks before granting access:
- Authentication: Verifying the user's identity
- Authorization: Checking if the authenticated user has permission to access a resource
- Validation: Ensuring the request contains valid data
- Rate limiting: Preventing too many requests from the same source
Each of these checks is independent but must be executed in a specific order. Adding or removing checks should not disrupt the system.
Solution
The Chain of Responsibility pattern suggests organizing these checks into a chain of objects, each with a reference to the next check in the chain. The request enters at the beginning of the chain and is passed along until a handler processes it or until it reaches the end without being handled.
Structure
Participants
- Handler: The interface or abstract class that defines how client requests will be handled. It typically includes a method to set the next handler in the chain and a method to handle the request.
- BaseHandler: An optional abstract class that implements the default chain behavior (setting the next handler and passing requests to it).
- ConcreteHandlers: Classes that handle requests they're responsible for and pass others along the chain.
- Client: Initiates the request to a handler in the chain.
When to Use
Use the Chain of Responsibility Pattern when:
- More than one object can handle a request, but the handler isn't known in advance
- You want to issue a request to one of several objects without specifying the receiver explicitly
- The set of objects that can handle a request should be specified dynamically
- You want to decouple the sender and receiver of a request
Benefits
- Decoupling: The pattern decouples the sender of a request from its receiver.
- Flexibility: You can change the chain at runtime by adding or removing responsibilities.
- Single Responsibility Principle: Classes focus only on their specific handling logic.
- Open/Closed Principle: You can introduce new handlers without changing existing code.
Real-World Uses
- Request Processing: HTTP request middleware in web frameworks like Express.js
- Event Handling: DOM event propagation in browsers (capturing and bubbling)
- Logging Systems: Different log levels handled by different components
- Authentication & Authorization: Sequential checks for user credentials and permissions
- Data Validation: Multiple validation rules applied sequentially to input data
Implementation Example
Here's a JavaScript implementation of the Chain of Responsibility pattern for a request validation system:
// Handler interface (abstract class in JavaScript)
class Handler {
setNext(handler) {
this.nextHandler = handler;
return handler;
}
handle(request) {
if (this.nextHandler) {
return this.nextHandler.handle(request);
}
return null; // If there's no next handler
}
}
// Concrete Handlers
class AuthenticationHandler extends Handler {
handle(request) {
// Check if the request has valid authentication
if (!request.token) {
return "AuthenticationHandler: Request lacks authentication token";
}
console.log("AuthenticationHandler: Request has a token, passing to next handler");
return super.handle(request);
}
}
class AuthorizationHandler extends Handler {
handle(request) {
// Check if the user has the permission
if (!request.permissions || !request.permissions.includes('access')) {
return "AuthorizationHandler: User doesn't have access permission";
}
console.log("AuthorizationHandler: User has permission, passing to next handler");
return super.handle(request);
}
}
class ValidationHandler extends Handler {
handle(request) {
// Check if the request data is valid
if (!request.data || Object.keys(request.data).length === 0) {
return "ValidationHandler: Request has no data";
}
// Additional validation logic could go here
console.log("ValidationHandler: Request data is valid, passing to next handler");
return super.handle(request);
}
}
class RateLimitHandler extends Handler {
constructor() {
super();
this.requestCount = 0;
this.maxRequests = 5;
}
handle(request) {
this.requestCount++;
if (this.requestCount > this.maxRequests) {
return "RateLimitHandler: Too many requests, try again later";
}
console.log(`RateLimitHandler: Request count ${this.requestCount}/${this.maxRequests}, passing to next handler`);
return super.handle(request);
}
}
class ResourceHandler extends Handler {
handle(request) {
// This is the final handler that processes the actual request
return `ResourceHandler: Request processed successfully, fetched data for ID: ${request.data.id}`;
}
}
// Client code
function clientCode() {
// Create handler instances
const authentication = new AuthenticationHandler();
const authorization = new AuthorizationHandler();
const validation = new ValidationHandler();
const rateLimit = new RateLimitHandler();
const resource = new ResourceHandler();
// Build the chain
authentication
.setNext(authorization)
.setNext(validation)
.setNext(rateLimit)
.setNext(resource);
// Create some sample requests
const validRequest = {
token: "valid-token",
permissions: ["access", "read"],
data: { id: 123 }
};
const noTokenRequest = {
permissions: ["access"],
data: { id: 456 }
};
const noPermissionRequest = {
token: "valid-token",
permissions: ["read"],
data: { id: 789 }
};
const noDataRequest = {
token: "valid-token",
permissions: ["access", "read"]
};
// Process the requests
console.log("Processing valid request:");
console.log(authentication.handle(validRequest));
console.log("\nProcessing request without token:");
console.log(authentication.handle(noTokenRequest));
console.log("\nProcessing request without permission:");
console.log(authentication.handle(noPermissionRequest));
console.log("\nProcessing request without data:");
console.log(authentication.handle(noDataRequest));
// Test rate limiting by sending multiple valid requests
console.log("\nTesting rate limiting:");
for (let i = 0; i < 6; i++) {
console.log(`Request ${i + 1}: ${authentication.handle(validRequest)}`);
}
}
clientCode();
In this example:
- The
Handler
class defines the interface with methods to set the next handler and handle requests. - Concrete handlers (
AuthenticationHandler
,AuthorizationHandler
, etc.) implement specific validation logic. - Each handler decides whether to process the request or pass it to the next handler in the chain.
- The
clientCode
function demonstrates how different requests are processed by the chain, with some being rejected at different points and others passing through all handlers. - The
RateLimitHandler
demonstrates how handlers can maintain state between requests.
Interactive Demo
Experience the Chain of Responsibility pattern in action with this interactive demo of a request processing system. See how requests flow through different handlers and how each handler makes independent decisions.
Interactive Demo: Request Processing Chain
Create requests with different characteristics and see how they're processed by various handlers in the chain.
Build Your Request
Current Request
{ "token": "valid-token", "permissions": ["access", "read"], "data": { "id": 123 } }