The Chain of Responsibility Pattern is a powerful design pattern that helps streamline the process of handling requests in a system. By allowing multiple handlers to process a request, this pattern ensures that the request is passed through a chain until it’s appropriately dealt with. In this blog, we will explore essential Chain of Responsibility Pattern examples, diving into its structure and providing key insights on how this pattern can be effectively used to create more flexible and maintainable code. Whether you’re new to design patterns or looking to expand your knowledge, this guide will help you understand the full potential of this pattern.
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.
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.
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.
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.
Structure of the Chain of Responsibility Pattern
Key Idea
- Decouple the sender and receiver.
- Each handler in the chain determines if it can handle the request.
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.
Chain of Responsibility Pattern Examples : Real-World Use Cases
Now, let’s roll up our sleeves and dive into real-world use case code.
Handling Employee Request
Let’s revisit our employee leave request scenario, where we need to approve a leave request in a company. The leave request should be processed by different authorities depending on the amount of leave being requested. Here’s the hierarchy:
- Employee: Initiates the leave request by submitting it to their immediate supervisor or system.
- Manager (up to 5 days): Approves short leaves to handle minor requests efficiently.
- Director (up to 15 days): Approves extended leaves, ensuring alignment with organizational policies.
- HR (more than 15 days): Handles long-term leave requests, requiring policy compliance or special considerations.
Using the Chain of Responsibility, we can chain the approval process such that if one handler (e.g., Manager) cannot process the request, it is passed to the next handler (e.g., Director).
Define the Handler Interface
The handler interface is a blueprint for the handlers that will process requests. Each handler can either process the request or pass it along to the next handler in the chain.
// Create the Handler interface
interface LeaveRequestHandler {
fun handleRequest(request: LeaveRequest)
}
In this case, the handleRequest
function takes a LeaveRequest
object, which holds the details of the leave request, and processes it.
Define the Request Object
The request object contains all the information related to the request. Here, we’ll create a simple LeaveRequest
class.
// Create the LeaveRequest object
data class LeaveRequest(val employeeName: String, val numberOfDays: Int)
Create Concrete Handlers
Now, we’ll implement different concrete handlers for each authority: Manager
, Director
, and HR
. Each handler will check if it can approve the leave request based on the number of days requested.
// Implement the Manager Handler
class Manager(private val nextHandler: LeaveRequestHandler? = null) : LeaveRequestHandler {
override fun handleRequest(request: LeaveRequest) {
if (request.numberOfDays <= 5) {
println("Manager approved ${request.employeeName}'s leave for ${request.numberOfDays} days.")
} else {
nextHandler?.handleRequest(request)
}
}
}
// Implement the Director Handler
class Director(private val nextHandler: LeaveRequestHandler? = null) : LeaveRequestHandler {
override fun handleRequest(request: LeaveRequest) {
if (request.numberOfDays <= 15) {
println("Director approved ${request.employeeName}'s leave for ${request.numberOfDays} days.")
} else {
nextHandler?.handleRequest(request)
}
}
}
// Implement the HR Handler
class HR : LeaveRequestHandler {
override fun handleRequest(request: LeaveRequest) {
println("HR approved ${request.employeeName}'s leave for ${request.numberOfDays} days.")
}
}
Each handler checks whether the request can be processed. If it cannot, the request is passed to the next handler in the chain.
Set Up the Chain
Next, we’ll set up the chain of responsibility. We will link the handlers so that each handler knows who to pass the request to if it can’t handle it.
// Setup the chain
fun createLeaveApprovalChain(): LeaveRequestHandler {
val hr = HR()
val director = Director(hr)
val manager = Manager(director)
return manager // The chain starts with the Manager
}
- If the
Manager
can’t approve the leave (i.e., the request is for more than 5 days), it passes the request to theDirector
. - If the
Director
can’t approve the leave (i.e., the request is for more than 15 days), it passes the request toHR
, which will handle it.
Test the Chain of Responsibility
Now, let’s create a LeaveRequest
and pass it through the chain.
fun main() {
val leaveRequest = LeaveRequest("amol pawar", 10)
val approvalChain = createLeaveApprovalChain()
approvalChain.handleRequest(leaveRequest)
}
// OUTPUT
// Director approved amol pawar's leave for 10 days.
Now, one more familiar use case that many of us have likely worked with in past projects is the Logging System.
Logging System
Let’s implement a logging system where log messages are passed through a chain of loggers. Each logger decides whether to handle the log or pass it along. We’ll include log levels: INFO, DEBUG, and ERROR.
Define a Base Logger Class
First, we’ll create an abstract Logger
class. This will act as the base for all specific loggers.
abstract class Logger(private val level: Int) {
private var nextLogger: Logger? = null
// Set the next logger in the chain
fun setNext(logger: Logger): Logger {
this.nextLogger = logger
return logger
}
// Handle the log request
fun logMessage(level: Int, message: String) {
if (this.level == level) {
write(message)
return // To stop further propagation
}
nextLogger?.logMessage(level, message)
}
// Abstract method for handling the log
protected abstract fun write(message: String)
}
Here,
level
: Defines the log level this logger will handle.nextLogger
: Points to the next handler in the chain.setNext
: Configures the next logger in the chain, enabling chaining.logMessage
: Checks if the logger should process the message based on its level. If not, it delegates the task to the next logger in the chain.return
: Why usedreturn
here..? Thereturn
is used to prevent the log message from being passed to the next logger after it has been processed. It ensures that once a logger handles the message, it won’t be forwarded further, avoiding redundant output.
Create Specific Logger Implementations
Let’s create concrete loggers for INFO, DEBUG, and ERROR levels.
class InfoLogger : Logger(1) {
override fun write(message: String) {
println("INFO: $message")
}
}
class DebugLogger : Logger(2) {
override fun write(message: String) {
println("DEBUG: $message")
}
}
class ErrorLogger : Logger(3) {
override fun write(message: String) {
println("ERROR: $message")
}
}
- Each logger overrides the
write
method to handle messages at its respective log level. - For instance, the
InfoLogger
handles logs with a level of 1, while higher levels (DEBUG or ERROR) are passed down the chain.
Setting Up the Chain
Now, let’s set up a chain of loggers: INFO → DEBUG → ERROR.
fun getLoggerChain(): Logger {
val errorLogger = ErrorLogger()
val debugLogger = DebugLogger()
val infoLogger = InfoLogger()
infoLogger.setNext(debugLogger).setNext(errorLogger)
return infoLogger
}
- We instantiate the loggers and chain them together using
setNext
. - The chain starts with
InfoLogger
and ends withErrorLogger
.
Using the Logger Chain
Now, let’s test our chain.
fun main() {
val loggerChain = getLoggerChain()
println("Testing Chain of Responsibility:")
loggerChain.logMessage(1, "This is an info message.")
loggerChain.logMessage(2, "This is a debug message.")
loggerChain.logMessage(3, "This is an error message.")
}
Output
Testing Chain of Responsibility:
INFO: This is an info message.
DEBUG: This is a debug message.
ERROR: This is an error message.
Basically, what happens here is,
- A log with level
1
is processed byInfoLogger
. - A log with level
2
is handled byDebugLogger
. - A log with level
3
is processed byErrorLogger
.
If no logger in the chain can handle a request, it simply passes through without being processed.
Extending the Chain
What if we wanted to add more log levels, like TRACE
? Easy..! Just implement a TraceLogger
and link it to the chain without modifying the existing code.
class TraceLogger : Logger(0) {
override fun write(message: String) {
println("TRACE: $message")
}
}
We also need to adjust the chain setup.
val traceLogger = TraceLogger()
traceLogger.setNext(infoLogger)
Some Other Real-Life Applications
- Middleware in Web Frameworks: Processing HTTP requests in frameworks like Spring Boot or Ktor.
- Authorization Systems: Sequential permission checks.
- Form Validation: Sequential checks for input validation.
- Payment Processing: Delegating payment methods to appropriate processors.
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.
Cocnlusion
The Chain of Responsibility Pattern offers a flexible and scalable solution to handling requests, making it an essential tool for developers looking to decouple their system’s components. Through the examples and insights shared, it becomes clear how this pattern can simplify complex workflows and enhance maintainability. By mastering the structure and application of the Chain of Responsibility pattern, you can ensure your systems are more adaptable to future changes, ultimately improving both code quality and overall project efficiency.