Explore the Chain of Responsibility design pattern and learn how to implement it in JavaScript to create flexible and decoupled systems.
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 promotes loose coupling and flexibility in assigning responsibilities to objects. In this section, we will explore how to implement the Chain of Responsibility pattern in JavaScript, providing code examples and explanations along the way.
Before diving into the implementation, let’s briefly understand the core concepts of the Chain of Responsibility pattern:
The primary goal of this pattern is to decouple the sender of a request from its receiver, allowing multiple objects to handle the request without the sender needing to know which object will handle it.
Let’s start by defining a simple handler class in JavaScript. We’ll create a Handler
class with a method to handle requests and a reference to the next handler.
// Handler class definition
class Handler {
constructor() {
this.nextHandler = null; // Reference to the next handler in the chain
}
// Method to set the next handler
setNext(handler) {
this.nextHandler = handler;
return handler; // Enable chaining
}
// Method to handle the request
handleRequest(request) {
if (this.nextHandler) {
return this.nextHandler.handleRequest(request);
}
return null;
}
}
In this basic setup, the Handler
class has a setNext
method to set the next handler in the chain and a handleRequest
method to process the request. If the current handler cannot process the request, it passes it to the next handler.
Next, let’s create some concrete handlers that extend the Handler
class. Each concrete handler will implement its own logic to determine whether it can handle the request.
// Concrete handler for logging requests
class LoggerHandler extends Handler {
handleRequest(request) {
console.log(`Logging request: ${request}`);
return super.handleRequest(request);
}
}
// Concrete handler for authentication
class AuthHandler extends Handler {
handleRequest(request) {
if (request === 'AUTH') {
console.log('Authentication successful!');
return true;
}
return super.handleRequest(request);
}
}
// Concrete handler for data processing
class DataHandler extends Handler {
handleRequest(request) {
if (request === 'DATA') {
console.log('Processing data...');
return true;
}
return super.handleRequest(request);
}
}
In this example, we have three concrete handlers: LoggerHandler
, AuthHandler
, and DataHandler
. Each handler checks if it can process the request and, if not, passes it to the next handler in the chain.
Now that we have our handlers, let’s set up the chain by linking them together. We’ll create instances of each handler and use the setNext
method to establish the chain.
// Create instances of handlers
const logger = new LoggerHandler();
const auth = new AuthHandler();
const data = new DataHandler();
// Set up the chain of responsibility
logger.setNext(auth).setNext(data);
In this setup, the LoggerHandler
is the first handler in the chain, followed by the AuthHandler
, and finally the DataHandler
. The setNext
method allows us to chain the handlers together seamlessly.
With the chain set up, we can now process requests by passing them to the first handler in the chain. Each handler will decide whether to process the request or pass it to the next handler.
// Process requests
console.log('Request: AUTH');
logger.handleRequest('AUTH');
console.log('\nRequest: DATA');
logger.handleRequest('DATA');
console.log('\nRequest: UNKNOWN');
logger.handleRequest('UNKNOWN');
Output:
Request: AUTH
Logging request: AUTH
Authentication successful!
Request: DATA
Logging request: DATA
Processing data...
Request: UNKNOWN
Logging request: UNKNOWN
In this example, when we pass the AUTH
request, the LoggerHandler
logs it, and the AuthHandler
processes it. Similarly, the DATA
request is logged and processed by the DataHandler
. The UNKNOWN
request is logged but not processed by any handler.
When implementing the Chain of Responsibility pattern in JavaScript, consider the following:
this
from methods allows for function chaining, which is useful when setting up the chain of handlers.To better understand the flow of requests through the chain, let’s visualize the process using a sequence diagram.
sequenceDiagram participant Client participant LoggerHandler participant AuthHandler participant DataHandler Client->>LoggerHandler: handleRequest('AUTH') LoggerHandler->>LoggerHandler: Log request LoggerHandler->>AuthHandler: handleRequest('AUTH') AuthHandler->>AuthHandler: Process request AuthHandler->>Client: Return success Client->>LoggerHandler: handleRequest('DATA') LoggerHandler->>LoggerHandler: Log request LoggerHandler->>AuthHandler: handleRequest('DATA') AuthHandler->>DataHandler: handleRequest('DATA') DataHandler->>DataHandler: Process request DataHandler->>Client: Return success Client->>LoggerHandler: handleRequest('UNKNOWN') LoggerHandler->>LoggerHandler: Log request LoggerHandler->>AuthHandler: handleRequest('UNKNOWN') AuthHandler->>DataHandler: handleRequest('UNKNOWN') DataHandler->>Client: Return null
Diagram Description: This sequence diagram illustrates how requests are passed along the chain of handlers. Each handler logs or processes the request and passes it to the next handler if necessary.
Now that we’ve covered the basics, try modifying the code examples to experiment with different scenarios:
Before we conclude, let’s reinforce what we’ve learned with a few questions:
setNext
method contribute to the pattern’s implementation?The Chain of Responsibility pattern is a powerful tool for creating flexible and decoupled systems in JavaScript. By allowing multiple handlers to process requests, this pattern promotes scalability and maintainability. Remember, this is just the beginning. As you progress, you’ll build more complex and interactive systems. Keep experimenting, stay curious, and enjoy the journey!