Implementing Chain of Responsibility and Command Patterns for Flexible Request Handling
Behavioral design patterns focus on the interactions and responsibility distribution between objects, offering optimal ways to define their behaviors and duties. This discussion covers the Chain of Responsibility and Command patterns.
Chain of Responsibility Pattern
Definition
The Chain of Responsibility pattern decouples request senders from receivers by allowing multiple objects the opportunity to process a request. The request is passed along a chain of handler objects until one handles it, creating a flexible linkage where senders don't need to know the specific receiver.
Problem Statement
Software systems often encounter scenarios where a request might be processed by any of several objects, with the specific handler determined only at runtime. Directly coupling senders to receivers makes systems rigid and difficult to extend.
Implementation Approach
The pattern addresses this through three key components:
- Abstract Handler: Defines the interface for handling requests and maintains a reference to the next handler in the chain.
- Concrete Handlers: Implement the handling interface, processing requests they're responsible for or passing them along the chain.
- Chain Assembly: Handlers are dynamically linked at runtime to form processing chains.
#include <iostream>
#include <string>
// Abstract handler interface
class RequestProcessor {
protected:
RequestProcessor* successor;
public:
void SetSuccessor(RequestProcessor* next) {
successor = next;
}
virtual void ProcessRequest(int priority, const std::string& content) = 0;
};
// Concrete handler for low-priority requests
class LowPriorityHandler : public RequestProcessor {
public:
void ProcessRequest(int priority, const std::string& content) override {
if (priority >= 1 && priority <= 3) {
std::cout << "LowPriority handled: " << content << std::endl;
}
if (successor != nullptr) {
successor->ProcessRequest(priority, content);
}
}
};
// Concrete handler for medium-priority requests
class MediumPriorityHandler : public RequestProcessor {
public:
void ProcessRequest(int priority, const std::string& content) override {
if (priority >= 4 && priority <= 7) {
std::cout << "MediumPriority handled: " << content << std::endl;
}
if (successor != nullptr) {
successor->ProcessRequest(priority, content);
}
}
};
// Client implementation
int main() {
RequestProcessor* lowHandler = new LowPriorityHandler();
RequestProcessor* mediumHandler = new MediumPriorityHandler();
lowHandler->SetSuccessor(mediumHandler);
// Process various requests
lowHandler->ProcessRequest(2, "Routine system check");
lowHandler->ProcessRequest(5, "User authentication request");
lowHandler->ProcessRequest(8, "Critical system alert");
delete lowHandler;
delete mediumHandler;
return 0;
}
Use Cases
- Multiple potential handlers for a request with runtime determination
- Requests submitted to one of several objects without explicit receiver specification
- Systems requiring dynamic handler addition or removal
Advantages and Disadvantages
Advantages:
- Reduces coupling between request senders and receivers
- Simplifies object connections by eliminating explicit receiver specification
- Enables dynamic runtime modification of handler chains
Disadvantages:
- Requests may traverse the entire chain without being processed
- Performance overhead from sequential handler evaluations
- Debugging complexity increases with chain length
Command Pattern
Definition
The Command pattern encapsulates requests as objects, enabling parameterization of objects with different requests, queues, or logging. It supports undo operations and completely decouples request senders from receivers through command objects.
Problem Statement
Systems often need to decouple request senders from receivers to allow new receivers without modifying sender code, or new commands without changing receiver implementations. Support for undo/redo operations adds further complexity.
Implementation Approach
The pattern structures solutions through four components:
- Command Interface: Declares execution methods
- Concrete Commands: Implement the interface and associate with receiver objects
- Invoker: Triggers command eexcution
- Receiver: Performs the actual operations
#include <iostream>
#include <memory>
#include <vector>
// Receiver interface
class Device {
public:
virtual void PerformOperation() = 0;
virtual ~Device() {}
};
// Concrete receiver: Light
class Light : public Device {
public:
void PerformOperation() override {
std::cout << "Light turned on" << std::endl;
}
};
// Command interface
class DeviceCommand {
protected:
std::shared_ptr<Device> targetDevice;
public:
DeviceCommand(std::shared_ptr<Device> device) : targetDevice(device) {}
virtual void Execute() = 0;
virtual ~DeviceCommand() {}
};
// Concrete command: Activate light
class LightOnCommand : public DeviceCommand {
public:
LightOnCommand(std::shared_ptr<Device> device) : DeviceCommand(device) {}
void Execute() override {
targetDevice->PerformOperation();
}
};
// Invoker: Control panel
class ControlPanel {
private:
std::vector<std::shared_ptr<DeviceCommand>> commandSlots;
public:
void AssignCommand(int position, std::shared_ptr<DeviceCommand> cmd) {
if (position >= 0 && position < commandSlots.size()) {
commandSlots[position] = cmd;
}
}
void TriggerCommand(int position) {
if (position >= 0 && position < commandSlots.size() &&
commandSlots[position] != nullptr) {
commandSlots[position]->Execute();
}
}
void AddCommand(std::shared_ptr<DeviceCommand> cmd) {
commandSlots.push_back(cmd);
}
};
// Client implementation
int main() {
auto roomLight = std::make_shared<Light>();
auto lightCommand = std::make_shared<LightOnCommand>(roomLight);
ControlPanel panel;
panel.AddCommand(lightCommand);
panel.TriggerCommand(0); // Output: Light turned on
return 0;
}
Use Cases
- Abstracting executable actions with specific receivers
- Submitting requests without explicit receiver specification
- Implementing undo/redo functionality
- Maintaining sender-receiver decoupling
Advantages and Disadvantages
Advantages:
- Decouples request senders from receivers
- Facilitates easy addition of new commands
- Naturally supports undo/redo operations
Disadvantages:
- Can lead to class proliferation with many command-receiver pairs
- Increases system complexity through additional abstraction layers
- Introduces performance overhead from multiple object instantiations