C++ Design Patterns: Chain of Responsibility Pattern

The Chain of Responsibility Pattern is a behavioral design pattern that allows requests to be passed along a chain of handlers until one of them handles the request. This pattern creates a chain of receiver objects for the request, allowing multiple objects to have the opportunity to process the request, thereby avoiding coupling between the sender and receiver of the request.

Core Roles of the Chain of Responsibility Pattern

  1. Abstract Handler (Handler): Defines the interface for handling requests and contains a reference to the next handler in the chain.
  2. Concrete Handler (Concrete Handler): Implements the request handling interface. If it can handle the request, it does so; otherwise, it passes the request to the next handler.
  3. Client: Creates the chain of handlers and submits requests to the first handler in the chain.

Example Implementation of the Chain of Responsibility Pattern

Below is an example of the implementation of the Chain of Responsibility Pattern using a “Leave Approval Process” where different lengths of leave require approval from different levels of management:

#include <iostream>
#include <string>

// Abstract Handler: Approver
class Approver {
protected:
    Approver* nextApprover;  // Next approver
    std::string name;       // Approver's name

public:
    Approver(const std::string& approverName, Approver* next = nullptr)
        : name(approverName), nextApprover(next) {}

    virtual ~Approver() {
        // Recursively delete the chain of responsibility
        delete nextApprover;
        nextApprover = nullptr;
    }

    // Interface for processing requests
    virtual void processRequest(int days) = 0;
};

// Concrete Handler: Team Leader (approves 1-3 days of leave)
class TeamLeader : public Approver {
public:
    TeamLeader(const std::string& name, Approver* next = nullptr)
        : Approver(name, next) {}

    void processRequest(int days) override {
        if (days <= 3) {
            std::cout << "Team Leader " << name << " approved " << days << " days of leave" << std::endl;
        } else if (nextApprover) {
            std::cout << "Team Leader " << name << " cannot approve " << days << " days of leave, passing to the next level" << std::endl;
            nextApprover->processRequest(days);
        } else {
            std::cout << "No one can approve " << days << " days of leave" << std::endl;
        }
    }
};

// Concrete Handler: Department Manager (approves 4-7 days of leave)
class DepartmentManager : public Approver {
public:
    DepartmentManager(const std::string& name, Approver* next = nullptr)
        : Approver(name, next) {}

    void processRequest(int days) override {
        if (days <= 7) {
            std::cout << "Department Manager " << name << " approved " << days << " days of leave" << std::endl;
        } else if (nextApprover) {
            std::cout << "Department Manager " << name << " cannot approve " << days << " days of leave, passing to the next level" << std::endl;
            nextApprover->processRequest(days);
        } else {
            std::cout << "No one can approve " << days << " days of leave" << std::endl;
        }
    }
};

// Concrete Handler: General Manager (approves 8-30 days of leave)
class GeneralManager : public Approver {
public:
    GeneralManager(const std::string& name, Approver* next = nullptr)
        : Approver(name, next) {}

    void processRequest(int days) override {
        if (days <= 30) {
            std::cout << "General Manager " << name << " approved " << days << " days of leave" << std::endl;
        } else if (nextApprover) {
            std::cout << "General Manager " << name << " cannot approve " << days << " days of leave, passing to the next level" << std::endl;
            nextApprover->processRequest(days);
        } else {
            std::cout << days << " days of leave exceeds approval limits, not approved" << std::endl;
        }
    }
};

// Client usage
int main() {
    // Create the chain of approvers: Team Leader -> Department Manager -> General Manager
    Approver* generalManager = new GeneralManager("Mr. Wang");
    Approver* deptManager = new DepartmentManager("Manager Li", generalManager);
    Approver* teamLeader = new TeamLeader("Team Leader Zhang", deptManager);

    // Submit leave requests for different days
    std::cout << "=== Requesting 2 days of leave ===" << std::endl;
    teamLeader->processRequest(2);

    std::cout << "\n=== Requesting 5 days of leave ===" << std::endl;
    teamLeader->processRequest(5);

    std::cout << "\n=== Requesting 15 days of leave ===" << std::endl;
    teamLeader->processRequest(15);

    std::cout << "\n=== Requesting 40 days of leave ===" << std::endl;
    teamLeader->processRequest(40);

    // Clean up resources (recursively delete the entire chain)
    delete teamLeader;

    return 0;
}

How the Chain of Responsibility Works

  1. Each handler contains a reference to the next handler, forming a chain.
  2. When a request occurs, it is submitted to the first handler in the chain.
  3. Each handler determines whether it can handle the request:
  • If it can handle it, it processes the request and terminates the chain.
  • If it cannot handle it, it passes the request to the next handler.
  1. If the request reaches the end of the chain without being handled, it may be ignored or handled by the system.

Two Organizational Structures of the Chain of Responsibility

  • Linear Chain: Each handler only knows the next handler (as shown in the example).
  • Tree Structure: Each handler can have multiple subsequent handlers, forming a more complex processing network.

Differences Between the Chain of Responsibility Pattern and Other Patterns

  • Chain of Responsibility Pattern: Requests are passed along the chain and handled by the first object that can process it.
  • Command Pattern: Requests are encapsulated as objects that can be stored, queued, and undone.
  • Mediator Pattern: Coordinates interactions between multiple objects through a central object.

Application Scenarios of the Chain of Responsibility Pattern

  1. When multiple objects can handle the same request, and which object handles it is determined at runtime.
  2. When a request needs to be submitted to one of multiple objects without explicitly specifying the receiver.
  3. When there is a need to dynamically specify the set of objects that handle the request.

Advantages and Disadvantages of the Chain of Responsibility Pattern

Advantages:

  • Reduces coupling between the request sender and receiver.
  • Allows dynamic adjustment of handlers in the chain, adding or removing handlers.
  • Each handler only needs to focus on the requests it can handle, maintaining a single responsibility.
  • Follows the Open-Closed Principle, allowing new handlers to be added without modifying existing code.

Disadvantages:

  • Requests may not be handled by any handler, necessitating an appropriate fault tolerance mechanism.
  • For long chains, there may be performance issues in processing requests.
  • Debugging can be complex, requiring tracking of the entire chain’s execution process.

The Chain of Responsibility Pattern is widely used in practical development, for example:

  • Logging systems (different levels of logs handled by different processors).
  • Event bubbling mechanisms (event passing in GUIs).
  • Filter chains (request filtering in web frameworks).
  • Permission control systems (different roles have different approval rights).

This pattern decouples the sender of the request from multiple possible receivers, enhancing the flexibility and scalability of the system.

Leave a Comment