Chain of Responsibility Pattern
The Chain of Responsibility pattern is a behavioral design pattern that allows you to decouple senders of requests from their handlers. This is achieved by giving multiple objects a chance to handle the request. The request is passed along a chain of handlers until one of them handles it. This pattern avoids coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. The chain can be composed dynamically at run-time and can be extended or modified without impacting the client code.
What is Chain of Responsibility Pattern
The Chain of Responsibility pattern addresses the problem of having multiple objects that can potentially handle a request, but the sender doesnāt know which object will ultimately handle it. Instead of the sender knowing about all possible handlers, a chain of handlers is established. Each handler examines the request and either handles it or passes it along to the next handler in the chain.
In-depth Explanation:
The core idea is to create a chain of handler objects. Each handler has a reference to the next handler in the chain. When a request comes in, the first handler in the chain receives it. The handler then decides whether it can handle the request. If it can, it processes the request and the chain ends. If it cannot, it passes the request to the next handler in the chain. This process continues until a handler handles the request or the end of the chain is reached (in which case the request is typically dropped or handled by a default handler).
Edge Cases:
- Empty Chain: What happens if the chain is empty? This needs to be handled gracefully, either by providing a default handler or by explicitly checking for an empty chain before processing.
- No Handler Handles the Request: What if none of the handlers can process the request? You should design for this scenario. Options include:
- A default handler at the end of the chain.
- Returning an error code or throwing an exception to indicate that the request could not be processed.
- Logging the unhandled request for debugging.
- Circular Chain: Ensure that the chain doesnāt become circular, as this can lead to infinite loops.
- Order of Handlers: The order of handlers in the chain is crucial. Incorrect ordering can lead to inefficient processing or even incorrect results.
Performance Considerations:
- Overhead: The Chain of Responsibility pattern introduces overhead because each request must be passed through multiple handlers.
- Chain Length: Long chains can impact performance. Consider the average length of the chain and the processing time of each handler.
- Handler Complexity: Complex handlers will naturally increase the processing time for each request.
Benefits:
- Decoupling: Reduces coupling between the sender of a request and the handlers.
- Flexibility: Allows you to add, remove, or reorder handlers dynamically.
- Single Responsibility Principle: Each handler has a single responsibility: to handle a specific type of request.
- Open/Closed Principle: You can extend the chain with new handlers without modifying existing code.
Syntax and Usage
The Chain of Responsibility pattern typically involves the following components:
- Handler Interface (Abstract Handler): Defines the interface for handling requests. It usually includes a method for setting the next handler in the chain and a method for handling the request.
- Concrete Handlers: Implement the handler interface. Each concrete handler is responsible for handling a specific type of request. If it canāt handle the request, it passes it to the next handler in the chain.
- Client: Creates the chain of handlers and sends requests to the first handler in the chain.
#include <iostream>
// Abstract Handler
class Handler {
public:
virtual Handler* setNext(Handler* handler) = 0;
virtual std::string handle(std::string request) = 0;
};
// Base Handler
class AbstractHandler : public Handler {
private:
Handler* next_handler_;
public:
AbstractHandler() : next_handler_(nullptr) {}
Handler* setNext(Handler* handler) override {
this->next_handler_ = handler;
return handler;
}
std::string handle(std::string request) override {
if (this->next_handler_) {
return this->next_handler_->handle(request);
}
return "End of chain reached. Request unhandled.";
}
protected:
Handler* getNext() {
return next_handler_;
}
};
// Concrete Handlers
class ConcreteHandlerA : public AbstractHandler {
public:
std::string handle(std::string request) override {
if (request == "TypeA") {
return "ConcreteHandlerA handled the request.";
} else {
return AbstractHandler::handle(request);
}
}
};
class ConcreteHandlerB : public AbstractHandler {
public:
std::string handle(std::string request) override {
if (request == "TypeB") {
return "ConcreteHandlerB handled the request.";
} else {
return AbstractHandler::handle(request);
}
}
};
// Client Code
int main() {
ConcreteHandlerA* handlerA = new ConcreteHandlerA();
ConcreteHandlerB* handlerB = new ConcreteHandlerB();
handlerA->setNext(handlerB);
std::cout << "Client: Who wants to handle TypeA?\n";
std::cout << handlerA->handle("TypeA") << std::endl;
std::cout << "\nClient: Who wants to handle TypeB?\n";
std::cout << handlerA->handle("TypeB") << std::endl;
std::cout << "\nClient: Who wants to handle TypeC?\n";
std::cout << handlerA->handle("TypeC") << std::endl;
delete handlerA;
delete handlerB;
return 0;
}Basic Example
Imagine a system for processing support tickets. Different types of tickets (e.g., billing, technical, sales) require different levels of expertise. The Chain of Responsibility can be used to route tickets to the appropriate support agent or team.
#include <iostream>
#include <string>
class SupportTicket {
public:
enum TicketType {
BILLING,
TECHNICAL,
SALES,
GENERAL
};
SupportTicket(TicketType type, const std::string& description) : type_(type), description_(description) {}
TicketType getType() const { return type_; }
std::string getDescription() const { return description_; }
private:
TicketType type_;
std::string description_;
};
class SupportHandler {
public:
virtual SupportHandler* setNext(SupportHandler* handler) = 0;
virtual void handleTicket(SupportTicket& ticket) = 0;
virtual ~SupportHandler() = default;
};
class AbstractSupportHandler : public SupportHandler {
private:
SupportHandler* nextHandler = nullptr;
public:
SupportHandler* setNext(SupportHandler* handler) override {
nextHandler = handler;
return handler;
}
void handleTicket(SupportTicket& ticket) override {
if (nextHandler != nullptr) {
nextHandler->handleTicket(ticket);
} else {
std::cout << "Ticket of type " << ticket.getType() << " unhandled. Description: " << ticket.getDescription() << std::endl;
}
}
protected:
SupportHandler* getNext() const { return nextHandler; }
};
class BillingSupportHandler : public AbstractSupportHandler {
public:
void handleTicket(SupportTicket& ticket) override {
if (ticket.getType() == SupportTicket::BILLING) {
std::cout << "BillingSupportHandler is handling ticket: " << ticket.getDescription() << std::endl;
} else {
AbstractSupportHandler::handleTicket(ticket);
}
}
};
class TechnicalSupportHandler : public AbstractSupportHandler {
public:
void handleTicket(SupportTicket& ticket) override {
if (ticket.getType() == SupportTicket::TECHNICAL) {
std::cout << "TechnicalSupportHandler is handling ticket: " << ticket.getDescription() << std::endl;
} else {
AbstractSupportHandler::handleTicket(ticket);
}
}
};
class SalesSupportHandler : public AbstractSupportHandler {
public:
void handleTicket(SupportTicket& ticket) override {
if (ticket.getType() == SupportTicket::SALES) {
std::cout << "SalesSupportHandler is handling ticket: " << ticket.getDescription() << std::endl;
} else {
AbstractSupportHandler::handleTicket(ticket);
}
}
};
int main() {
BillingSupportHandler billingHandler;
TechnicalSupportHandler technicalHandler;
SalesSupportHandler salesHandler;
billingHandler.setNext(&technicalHandler)->setNext(&salesHandler); // Build the chain
SupportTicket billingTicket(SupportTicket::BILLING, "Issue with invoice.");
SupportTicket technicalTicket(SupportTicket::TECHNICAL, "Cannot connect to the server.");
SupportTicket salesTicket(SupportTicket::SALES, "Request for product demo.");
SupportTicket generalTicket(SupportTicket::GENERAL, "General inquiry.");
billingHandler.handleTicket(billingTicket);
billingHandler.handleTicket(technicalTicket);
billingHandler.handleTicket(salesTicket);
billingHandler.handleTicket(generalTicket);
return 0;
}Explanation:
SupportTicketrepresents a support ticket with a type and description.SupportHandleris the abstract handler interface.AbstractSupportHandlerprovides a default implementation for setting the next handler and forwarding the request.BillingSupportHandler,TechnicalSupportHandler, andSalesSupportHandlerare concrete handlers that handle specific types of tickets.- The
mainfunction creates the chain of handlers and sends tickets to the first handler in the chain. - The
setNextmethod allows for chaining handlers together. Note the use of chainingsetNext(&technicalHandler)->setNext(&salesHandler);. This is a common and readable way to construct the chain.
Advanced Example
Consider a system that validates user input. The validation process might involve multiple steps, such as checking for null values, validating data types, and ensuring that the input conforms to specific rules. The Chain of Responsibility pattern can be used to implement this validation pipeline. This example uses smart pointers and lambdas for more modern C++.
#include <iostream>
#include <string>
#include <memory>
#include <functional>
class ValidationRequest {
public:
ValidationRequest(std::string data) : data_(std::move(data)) {}
std::string getData() const { return data_; }
void setData(const std::string& data) { data_ = data; }
bool isValid() const { return isValid_; }
void setValid(bool valid) { isValid_ = valid; }
private:
std::string data_;
bool isValid_ = true; // Initially assume valid
};
class Validator {
public:
using ValidatorFunction = std::function<bool(ValidationRequest&)>;
Validator(ValidatorFunction validationFunction) : validationFunction_(std::move(validationFunction)), nextValidator_(nullptr) {}
void setNext(std::unique_ptr<Validator> next) {
nextValidator_ = std::move(next);
}
void validate(ValidationRequest& request) {
if (request.isValid() && validationFunction_ != nullptr) {
request.setValid(validationFunction_(request));
}
if (request.isValid() && nextValidator_ != nullptr) {
nextValidator_->validate(request);
}
}
private:
ValidatorFunction validationFunction_;
std::unique_ptr<Validator> nextValidator_;
};
int main() {
// Create validators using lambdas
auto nullCheckValidator = std::make_unique<Validator>([](ValidationRequest& request) {
return !request.getData().empty();
});
auto lengthValidator = std::make_unique<Validator>([](ValidationRequest& request) {
return request.getData().length() <= 50;
});
auto alphanumericValidator = std::make_unique<Validator>([](ValidationRequest& request) {
for (char c : request.getData()) {
if (!isalnum(c)) {
return false;
}
}
return true;
});
// Build the chain
nullCheckValidator->setNext(std::move(lengthValidator));
nullCheckValidator->setNext(std::move(alphanumericValidator)); // Last validator in chain
// Create a validation request
ValidationRequest request1("ValidInput123");
ValidationRequest request2("");
ValidationRequest request3("ThisIsAVeryLongInputThatExceedsTheMaximumAllowedLength");
ValidationRequest request4("Invalid Input with Spaces");
// Validate the requests
nullCheckValidator->validate(request1);
nullCheckValidator->validate(request2);
nullCheckValidator->validate(request3);
nullCheckValidator->validate(request4);
// Print the results
std::cout << "Request 1 is valid: " << request1.isValid() << std::endl;
std::cout << "Request 2 is valid: " << request2.isValid() << std::endl;
std::cout << "Request 3 is valid: " << request3.isValid() << std::endl;
std::cout << "Request 4 is valid: " << request4.isValid() << std::endl;
return 0;
}Common Use Cases
- Event Handling: Processing events in a GUI framework.
- Authentication/Authorization: Checking user credentials and permissions.
- Logging: Filtering and routing log messages based on severity or source.
Best Practices
- Keep Handlers Simple: Each handler should have a single, well-defined responsibility.
- Handle Unhandled Requests: Always provide a mechanism for handling requests that are not processed by any handler.
- Document the Chain: Clearly document the order and purpose of each handler in the chain.
Common Pitfalls
- Long Chains: Avoid creating excessively long chains, as this can impact performance.
- Complex Handlers: Keep handlers simple and focused to avoid introducing bugs and performance issues.
- Tight Coupling: Ensure that the handlers are loosely coupled to each other and to the client.
Key Takeaways
- The Chain of Responsibility pattern decouples senders and receivers of requests.
- It allows you to create a flexible and extensible chain of handlers.
- Careful design is needed to avoid performance issues and ensure that all requests are handled appropriately.