The Chain of Responsibility pattern is a behavioral design pattern that allows a request to be passed along a chain of handlers until it is processed. This pattern is particularly powerful in scenarios where multiple objects can process a request, but the handler is not determined until runtime. In this blog, we will be unveiling the powerful structure of the Chain of Responsibility pattern, breaking down its key components and flow. By the end, you’ll have a solid understanding of how this pattern can improve flexibility and scalability in your application design.
What is the Chain of Responsibility (CoR) Pattern?
The Chain of Responsibility design pattern is a behavioral design pattern that allows passing a request along a chain of handlers, where each handler has a chance to process the request or pass it along to the next handler in the chain. The main goal is to decouple the sender of a request from its receivers, giving multiple objects a chance to handle the request.
That means the CoR pattern allows multiple objects to handle a request without the sender needing to know which object handled it. The request is passed along a chain of objects (handlers), where each handler has the opportunity to process it or pass it to the next one.
Think of a company where a request, such as budget approval, must go through several levels of management. At each level, the manager can either address the request or escalate it to the next level.
Now imagine another situation: an employee submits a leave application. Depending on the duration of leave, it might need approval from different authorities, such as a team leader, department head, or higher management.
Why Use the Chain of Responsibility Pattern?
Before we delve into the structure and implementation of the Chain of Responsibility (CoR) pattern, let’s first understand why it’s important.
Consider a situation where multiple objects are involved in processing a request, and the handling varies depending on the specific conditions. For example, in online shopping platforms like Myntra or Amazon, or food delivery services such as Zomato or Swiggy, a customer might use a discount code or coupon. The system needs to determine if the code is valid or decide which discount should apply based on the circumstances.
This is where the Chain of Responsibility pattern becomes highly useful. Rather than linking the request to a specific handler, it enables the creation of a chain of handlers, each capable of managing the request in its unique way. This makes the system more adaptable, allowing developers to easily add, remove, or modify handlers without affecting the core logic.
So, the Chain of Responsibility pattern offers several advantages:
- Decouples the sender and receiver: The sender doesn’t need to know which object in the chain will handle the request.
- Simplifies the code: It eliminates complex conditionals and decision trees by delegating responsibility to handlers in the chain.
- Adds flexibility: New handlers can be seamlessly added to the chain without impacting the existing implementation.
These scenarios capture the essence of the Chain of Responsibility design pattern, where a request is passed along a series of handlers, each with the choice to process it or forward it.
Structure of the Chain of Responsibility Pattern
The Chain of Responsibility pattern consists of:
- Handler Interface: Declares a method to process requests and optionally set the next handler.
- Concrete Handlers: Implements the interface and processes the request.
- Client Code: Creates and configures the chain.
Handler (Abstract Class or Interface)
Defines the interface for handling requests and the reference to the next handler in the chain.
abstract class Handler {
protected var nextHandler: Handler? = null
abstract fun handleRequest(request: String)
fun setNextHandler(handler: Handler) {
nextHandler = handler
}
}
- This defines an interface for handling requests, usually with a method like
handleRequest()
. It may also have a reference to the next handler in the chain. - The handler may choose to process the request or pass it on to the next handler.
ConcreteHandler
Implement the handleRequest()
method to either process the request or pass it to the next handler.
class ConcreteHandlerA : Handler() {
override fun handleRequest(request: String) {
if (request == "A") {
println("Handler A processed request: $request")
} else {
nextHandler?.handleRequest(request)
}
}
}
class ConcreteHandlerB : Handler() {
override fun handleRequest(request: String) {
if (request == "B") {
println("Handler B processed request: $request")
} else {
nextHandler?.handleRequest(request)
}
}
}
- These are the actual handler classes that implement the
handleRequest()
method. Each concrete handler will either process the request or pass it to the next handler in the chain. - If a handler is capable of processing the request, it does so; otherwise, it forwards the request to the next handler in the chain.
Client
Interacts only with the first handler in the chain, unaware of the specific handler processing the request.
fun main() {
val handlerA = ConcreteHandlerA()
val handlerB = ConcreteHandlerB()
handlerA.setNextHandler(handlerB)
// Client sends the request to the first handler
handlerA.handleRequest("A") // Handler A processes the request
handlerA.handleRequest("B") // Handler B processes the request
}
- The client sends the request to the first handler in the chain. The client does not need to know which handler will eventually process the request.
As we can see, this structure allows requests to pass through multiple handlers in the chain, with each handler having the option to process the request or delegate it.
Advantages and Disadvantages
Advantages
- Flexibility: Easily add or remove handlers in the chain.
- Decoupling: The sender doesn’t know which handler processes the request.
- Open/Closed Principle: New handlers can be added without modifying existing code.
Disadvantages
- No Guarantee of Handling: If no handler processes the request, it may go unhandled.
- Debugging Complexity: Long chains can be hard to trace.
Conclusion
The Chain of Responsibility pattern offers a robust solution for handling requests in a decoupled and flexible way, allowing for easier maintenance and scalability. By understanding the structure and key components of this pattern, you can effectively apply it to scenarios where multiple handlers are required to process requests in a dynamic and streamlined manner. Whether you’re developing complex systems or optimizing existing architectures, this pattern is a valuable tool that can enhance the efficiency and adaptability of your software design.