By Wenkong
In software development, we often encounter situations where a request needs to pass through multiple processing nodes to obtain the final result. This is where the chain of responsibility pattern comes in, allowing us to handle the request gracefully. The chain of responsibility is a behavioral design pattern that constructs a chain of multiple processing nodes, giving each node a chance to handle a request or pass it along the chain to the next node. This pattern decouples the request sender and receiver, improving the flexibility and maintainability of the code.
The chain of responsibility pattern is typically used in the following scenarios:
(1) Multiple objects can handle a request, but the specific object that will handle the request can only be determined at runtime.
(2) You want to submit a request to one of multiple objects without explicitly specifying the receiver.
(3) You want to dynamically specify a collection of objects to process the request.
This pattern allows multiple objects to process requests, avoiding coupling between the request sender and its handlers. These objects are linked into a chain, and the request is passed along this chain until an object handles it.
It is a type of object behavioral pattern.
The handler on the chain is responsible for processing the request, while the client only needs to send the request to the chain of responsibility without worrying about the processing details of the request or its delivery, thus decoupling the request sender from the request handler.
The benefits and objectives of using CoR are as follows:
(1) Decoupling of nodes on the CoR: The pattern decouples the request sender from the request receiver, reducing object coupling.
(2) Improved code flexibility and maintainability: The CoR pattern allows for dynamic modification of processing nodes at runtime, making it flexible for handling different requests and facilitating code maintenance and scalability.
(3) Easy addition of new processing nodes: Due to node decoupling, new nodes can be easily added to the chain without modifying existing code.
(4) Dynamic request processing: The CoR pattern enables dynamic handler combination and sorting at runtime, implementing flexible request-handling logic.
By using the CoR pattern, we can handle requests more flexibly and elegantly, reduce code coupling, and improve code maintainability and scalability. In scenarios with complex business logic or dynamic request processing needs, the CoR pattern is a good choice. Next, we will provide a detailed example to demonstrate how to use the CoR pattern in business code.
Use the Chain of Responsibility pattern when your program is expected to process different kinds of requests in various ways, but the exact types of requests and their sequences are unknown beforehand.
The pattern lets you link several handlers into one chain and, upon receiving a request, "ask" each handler whether it can process it. This way all handlers get a chance to process the request.
Use the pattern when it's essential to execute several handlers in a particular order.
Since you can link the handlers in the chain in any order, all requests will get through the chain exactly as you planned.
Use the CoR pattern when the set of handlers and their order are supposed to change at runtime.
If you provide setters for a reference field inside the handler classes, you'll be able to insert, remove, or reorder handlers dynamically.
(1) Declare the handler interface and describe the signature of a method for handling requests.
Determine how the client will pass the request data into the method. The most flexible way is to convert the request into an object and pass it to the handling method as an argument.
(2) To eliminate duplicate boilerplate code in concrete handlers, it might be worth creating an abstract base handler class, derived from the handler interface.
This class should have a field for storing a reference to the next handler in the chain. Consider making the class immutable. However, if you plan to modify chains at runtime, you need to define a setter for altering the value of the reference field.
You can also implement the convenient default behavior for the handling method, which is to forward the request to the next object unless there's none left. Concrete handlers will be able to use this behavior by calling the parent method.
(3) One by one create concrete handler subclasses and implement their handling methods. Each handler should make two decisions when receiving a request:
(4) The client may either assemble chains on its own or receive pre-built chains from other objects. In the latter case, you must implement some factory classes to build chains according to the configuration or environment settings.
(5) The client may trigger any handler in the chain, not just the first one. The request will be passed along the chain until some handler refuses to pass it further or until it reaches the end of the chain.
(6) Due to the dynamic nature of the chain, the client should be ready to handle the following scenarios:
Let's take the scenario of requesting a leave as an example. The number of days off can vary, such as 3 days, 5 days, or 7 days, with different approvers for different durations. For instance, a 3-day leave might only require approval from your team leader, while a 5-day leave may need to be approved by the department manager. In this scenario, the chain of responsibility pattern is highly suitable. While it's possible to use numerous if-else statements to handle this, the chain of responsibility pattern offers some advantages:
(1) Compared to if-else statements, the chain of responsibility pattern has lower coupling because it distributes the condition determination to each processing class. The priority order of these processing classes can be freely set, and it's simple to add new handler classes, which conforms to the open/closed principle (OCP).
(2) The chain of responsibility pattern offers flexibility, but it's crucial to avoid circular references within the chain when setting up the relationships between handler classes.
Using a for
loop to implement the chain of responsibility pattern can provide a more intuitive understanding of how it works.
Let's say we have a chain of responsibility with multiple handlers: Processor1, Processor2, and Processor3. Each handler can process a request, and if one handler can handle it, it will process the request and terminate the chain of responsibility. If a handler can't process the request, it passes it to the next handler.
We can use a for loop to iterate through the handlers in the chain and process them one by one. When we encounter the first handler that can handle the request, we process the request and exit the loop.
Request request = new Request(); // Create the request object
Processor[] processors = {new Processor1(), new Processor2(), new Processor3()};
for (Processor processor : processors) {
if (processor.canHandle(request)) {
processor.process(request);
break; // Process the request and end the loop
}
}
In the above code, we traverse the handlers in the chain of responsibility in turn and determine whether the request can be processed by calling the canHandle
method. If it can be processed, the process
method is called for processing and the loop ends with a break
statement. If all handlers are unable to process the request, the corresponding processing logic is performed after the loop ends.
Using a for loop to handle the chain of responsibility can express the execution process of the CoR more clearly. We traverse the handlers in order and process as needed. If multiple handlers can process the request, we can prioritize the processing by adjusting the order of handlers. Additionally, we can perform specific operations based on actual needs after the loop ends, such as reporting an error or providing a default handling option.
The core idea of the CoR is to pass a request along a chain, where each node has the opportunity to handle the request or pass it to the next node. This design is similar to a workflow on a pipeline, where each node is responsible for handling a specific part of the process and then passing the result to the next node. We can use a flowchart to illustrate the execution process of the CoR. Each node can be represented as a step in the flowchart with arrows indicating the direction to pass the request. The entire flowchart can clearly display the work sequence and process of each node in the chain of responsibility pattern.
By the abstract representation of the flowchart, we can better understand the execution process of the CoR. This facilitates communication and discussion and also aids in the design and implementation of the pattern. Additionally, the flowchart can help developers better understand and maintain the code logic of the pattern.
The chain of responsibility pattern in the leave request scenario can be implemented through a LeaveRequestProcessor interface that each concrete handler implements to determine whether the request can be processed and whether the request is passed to the next handler based on the number of days.
Using a for loop to implement the chain of responsibility pattern can more intuitively demonstrate the execution process of the chain:
First, define the LeaveRequestProcessor interface:
public interface LeaveRequestProcessor {
void processLeaveRequest(LeaveRequest request);
}
(1) TeamLeaderProcessor:
public class TeamLeaderProcessor implements LeaveRequestProcessor {
public void processLeaveRequest(LeaveRequest request) {
if (request.getDays() <= 3) {
// Process the leave request
System.out.println("The team leader processor approves the leave request, and the number of days is: " + request.getDays() + "days");
} else {
System.out.println("The team leader processor is unable to process the leave request");
}
}
}
(2) DepartmentManagerProcessor:
public class DepartmentManagerProcessor implements LeaveRequestProcessor {
public void processLeaveRequest(LeaveRequest request) {
if (request.getDays() > 3 && request.getDays() <= 5) {
// Process the leave request
System.out.println("The department manager processor approves the leave request, and the number of days is: " + request.getDays() + "days");
} else {
System.out.println("The department manager processor is unable to process the leave request");
}
}
}
(3) CEOProcessor:
public class CEOProcessor implements LeaveRequestProcessor {
public void processLeaveRequest(LeaveRequest request) {
if (request.getDays() > 5 && request.getDays() <= 7) {
// Process the leave request
System.out.println("The CEO processor approves the leave request, and the number of days is: " + request.getDays() + "days");
} else {
System.out.println("The CEO processor is unable to process the leave request");
}
}
}
Then, create a list of handlers and use a for loop to traverse the list of handlers to process the leave request:
List<LeaveRequestProcessor> processors = new ArrayList<>();
processors.add(new TeamLeaderProcessor());
processors.add(new DepartmentManagerProcessor());
processors.add(new CEOProcessor());
// Create a leave request
LeaveRequest request = new LeaveRequest("John", 5);
for (LeaveRequestProcessor processor : processors) {
processor.processLeaveRequest(request);
}
In the above code, we create a list of handlers and add three concrete handlers: the Team Leader Processor, the Department Manager Processor, and the CEO Processor. We then use a for loop to iterate through the list of handlers and call the process Leave Request method of each handler to process the leave request.
Each handler determines whether the request can be processed based on the number of days. If the request can be processed, the handler processes it and the loop ends. If the request cannot be processed, the next handler continues. This way, the request is passed sequentially along the list of handlers until it finds a handler that can process the request.
Using a for loop to implement the chain of responsibility pattern clearly expresses the execution process of the CoR. We can also prioritize the processing by adjusting the order of handlers in the handler list. When the business logic changes, we only need to add or modify the corresponding handler without modifying the existing handler or the code of the request sender, which improves the maintainability and scalability of the code.
In this way, through the chain of responsibility pattern, we can flexibly process leave requests and determine which handler to approve the request based on the number of days. When the business logic changes, we only need to add or modify the corresponding handler without modifying the existing handler or the code of the request sender, which improves the maintainability and scalability of the code.
Disclaimer: The views expressed herein are for reference only and don't necessarily represent the official views of Alibaba Cloud.
1,037 posts | 255 followers
FollowAlibaba Cloud Community - December 21, 2023
Alibaba Cloud Community - September 12, 2023
Alibaba Cloud Community - December 22, 2023
linear.zw - December 19, 2023
Adrian Peng - February 1, 2021
淘系技术 - April 14, 2020
1,037 posts | 255 followers
FollowA low-code development platform to make work easier
Learn MoreHelp enterprises build high-quality, stable mobile apps
Learn MoreAlibaba Cloud (in partnership with Whale Cloud) helps telcos build an all-in-one telecommunication and digital lifestyle platform based on DingTalk.
Learn MoreOffline SDKs for visual production, such as image segmentation, video segmentation, and character recognition, based on deep learning technologies developed by Alibaba Cloud.
Learn MoreMore Posts by Alibaba Cloud Community