CLOSE
Updated on 02 Sep, 202546 mins read 129 views

The Chain of Responsibility Pattern is a behavioral design pattern that allows a request to be passed along a chain of handlers until one of them handles it. This pattern promotes loose coupling by letting the sender of a request and the receivers (handlers) interact indirectly.

The Chain of Responsibility is a behavioral design pattern that allows a request to be passed sequentially through a chain of handlers. Each handler determines whether to process the request or forward it to the next handler in the sequence.

Problem

Imagine you're building an online ordering system that needs robust access control. To ensure security and proper functionality, the system must enforce several sequential checks:

  1. Authentication: Only authenticated users can create orders.
  2. Authorization: Users with administrative permissions have full access to manage all orders.

At first, these checks seemed manageable. The application would attempt to authenticate users based on credentials in the incoming request. If authentication failed, no further checks were performed.

Growing Complexity Over Time:

As the system evolved, new requirements arose:

  • Data Validation: A colleague pointed out the risk of processing raw request data, so you added a step to sanitize inputs.
  • Brute Force Protection: To defend against password cracking attempts, you implemented a check to block repeated failed requests from the same IP address.
  • Caching: To improve performance, you added a mechanism to return cached results for repeated requests with identical data, bypassing other checks when possible.

Each new feature added more complexity. The code for these checks, initially simple, turned into a tangled mess. Adding or modifying a check often broke unrelated parts of the system. Reusing these checks for other components became a nightmare since those components required only specific checks, forcing you to duplicate code.

Solution:

The Chain of Responsibility pattern offers an elegant solution to problems involving sequential checks or processes. It works by encapsulating specific behaviors into separate, self-contained objects called handlers. Each handler is responsible for performing one type of check or operation.

In the context of an online ordering system, each security or performance check—such as authentication, authorization, data validation, brute-force protection, or caching—would be implemented as a standalone class with a single method to handle requests. The request, along with its associated data, is passed to this method for processing.

The pattern then connects these handlers into a chain, where:

  1. Each handler contains a reference to the next handler in the chain.
  2. When a handler finishes its check, it decides whether to pass the request to the next handler.
  3. The request flows through the chain until either all handlers have processed it, or one handler stops the chain.

First-Responder Style

A slightly different and more canonical approach focuses on single responsibility:

  • When a handler receives a request, it determines if it can process it.
  • If it can, it handles the request and stops further propagation.
  • If it cannot, the request is passed to the next handler.

This style ensures that only one handler processes the request, which is particularly useful for event-driven systems.

Real-World Analogy

GUI Event Handling:

Consider a graphical user interface (GUI) where user actions (like clicking a button) generate events:

  1. The event flows through a chain of GUI elements, starting with the button and propagating through its containers (like panels or windows).
  2. The first element capable of handling the event processes it, and the propagation stops.
  3. This chain is often derived from the hierarchical structure of GUI components, showing that a chain can be extracted from an object tree.

Customer Support System:

Think of a customer support system in a company.

  1. A customer reports an issue.
  2. The first level support check if it can resolve the issue.
  3. If not, the report moves to the next level of support.
  4. This continues until the issue is resolved or reaches the final handler.

This is exactly how the Chain of Responsibility works in software.

Key Components of the Pattern

This pattern consists of the following components:

1 Handler (Abstract Class / Interface)

An abstract class or interface that defines the method for handling the requests and a reference to the next handler in the chain.

2 Concrete Handler

A class that implements the handler and process the request if it can. Otherwise, it forwards the request to the next handler.

3 Client

The object that sends the request, typically unaware of the specific handler that will process it.

Problem Without Chain of Responsibility

Suppose we have a support ticket system. The tickets can be basic, intermediate, or advanced. Without CoR, the client has to know exactly which handler to call for each request:

#include <iostream>
#include <string>
using namespace std;

class SupportSystem {
public:
    void handleRequest(const string& issue) {
        if (issue == "basic") {
            cout << "Level One Support resolved: " << issue << endl;
        } else if (issue == "intermediate") {
            cout << "Level Two Support resolved: " << issue << endl;
        } else if (issue == "advanced") {
            cout << "Level Three Support resolved: " << issue << endl;
        } else {
            cout << "No handler could resolve the issue: " << issue << endl;
        }
    }
};

int main() {
    SupportSystem support;
    support.handleRequest("basic");
    support.handleRequest("intermediate");
    support.handleRequest("advanced");
    support.handleRequest("expert");
}

Problem in this Approach:

  1. Tightly coupled: Client code must know all levels of support.
  2. Hard to extend: Adding a new level requires modifying the handleRequest method.
  3. Violation of Single Responsibility Principle: The SupportSystem class handles all types of requests itself.

Solution With Chain of Responsibility

By applying the CoR pattern, each support level becomes an independent handler. The client just sends the request to the first handler, and the request flows through the chain:

#include <iostream>
#include <string>
using namespace std;

// Abstract Handler
class SupportHandler {
protected:
    SupportHandler* nextHandler;
public:
    SupportHandler() : nextHandler(nullptr) {}
    void setNextHandler(SupportHandler* handler) { nextHandler = handler; }
    virtual void handleRequest(const string& issue) = 0;
};

// Concrete Handlers
class LevelOneSupport : public SupportHandler {
public:
    void handleRequest(const string& issue) override {
        if (issue == "basic") {
            cout << "Level One Support resolved: " << issue << endl;
        } else if (nextHandler) nextHandler->handleRequest(issue);
    }
};

class LevelTwoSupport : public SupportHandler {
public:
    void handleRequest(const string& issue) override {
        if (issue == "intermediate") {
            cout << "Level Two Support resolved: " << issue << endl;
        } else if (nextHandler) nextHandler->handleRequest(issue);
    }
};

class LevelThreeSupport : public SupportHandler {
public:
    void handleRequest(const string& issue) override {
        if (issue == "advanced") {
            cout << "Level Three Support resolved: " << issue << endl;
        } else if (nextHandler) 
            cout << "No handler could resolve the issue: " << issue << endl;
    }
};

int main() {
    // Create handlers
    LevelOneSupport level1;
    LevelTwoSupport level2;
    LevelThreeSupport level3;

    // Setup chain: Level1 -> Level2 -> Level3
    level1.setNextHandler(&level2);
    level2.setNextHandler(&level3);

    // Client sends requests
    level1.handleRequest("basic");        // Handled by Level 1
    level1.handleRequest("intermediate"); // Handled by Level 2
    level1.handleRequest("advanced");     // Handled by Level 3
    level1.handleRequest("expert");       // No handler

    return 0;
}

Benefits of CoR in this Solution:

  1. Loose coupling: Client does not need to know which handler processes the request.
  2. Easy to extend: Adding a new support level just means adding a new handler class and linking it.
  3. Single Responsibility: Each handler handles its own level of request.

Examples

Example 1: ATM Cash Dispense

#include <iostream>
using namespace std;

// Abstract Handler (Base Class)
class MoneyHandler {
protected:
    MoneyHandler *nextHandler;

public:
    MoneyHandler() {
        this->nextHandler = nullptr;
    }

    void setNextHandler(MoneyHandler *next) { 
        nextHandler = next; 
    }

    virtual void dispense(int amount) = 0;
};

class ThousandHandler : public MoneyHandler {
private:
    int numNotes;

public:
    ThousandHandler(int numNotes) {
        this->numNotes = numNotes;
    }

    void dispense(int amount) override {
        int notesNeeded = amount / 1000;

        if(notesNeeded > numNotes) {
            notesNeeded = numNotes;
            numNotes = 0;
        }
        else {
            numNotes -= notesNeeded;
        }

        if(notesNeeded > 0)
            cout << "Dispensing " << notesNeeded << " x ₹1000 notes.\n";

        int remainingAmount = amount - (notesNeeded * 1000);
        if(remainingAmount > 0) {
            if(nextHandler != nullptr) nextHandler->dispense(remainingAmount);
            else {
                cout << "Remaining amount of " << remainingAmount << " cannot be fulfilled (Insufficinet fund in ATM)\n";
            }
        }
    }
};

// Concrete Handler for 500 Rs Notes
class FiveHundredHandler : public MoneyHandler {
private:
    int numNotes;

public:
    FiveHundredHandler(int numNotes) {
        this->numNotes = numNotes;    
    }

    void dispense(int amount) override {
        int notesNeeded = amount / 500;

        if(notesNeeded > numNotes) {
            notesNeeded = numNotes;
            numNotes = 0;
        }
        else {
            numNotes -= notesNeeded;
        }

        if(notesNeeded > 0)
            cout << "Dispensing " << notesNeeded << " x ₹500 notes.\n";

        int remainingAmount = amount - (notesNeeded * 500);
        if(remainingAmount > 0) {
            if(nextHandler != nullptr) nextHandler->dispense(remainingAmount);
            else {
                cout << "Remaining amount of " << remainingAmount << " cannot be fulfilled (Insufficinet fund in ATM)\n";
            }
        }
    }
};

// Concrete Handler for 200 Rs Notes
class TwoHundredHandler : public MoneyHandler {
private:
    int numNotes;

public:
    TwoHundredHandler(int numNotes) {
        this->numNotes = numNotes;
    }

    void dispense(int amount) override {
        int notesNeeded = amount / 200;

        if(notesNeeded > numNotes) {
            notesNeeded = numNotes;
            numNotes = 0;
        }
        else {
            numNotes -= notesNeeded;
        }

        if(notesNeeded > 0)
            cout << "Dispensing " << notesNeeded << " x ₹200 notes.\n";

        int remainingAmount = amount - (notesNeeded * 200);
        if(remainingAmount > 0) {
            if(nextHandler != nullptr) nextHandler->dispense(remainingAmount);
            else {
                cout << "Remaining amount of " << remainingAmount << " cannot be fulfilled (Insufficinet fund in ATM)\n";
            }
        }
    }
};

// Concrete Handler for 100 Rs Notes
class HundredHandler : public MoneyHandler {
private:
    int numNotes;

public:
    HundredHandler(int numNotes) {
        this->numNotes = numNotes;
    }

    void dispense(int amount) override {
        int notesNeeded = amount / 100;

        if(notesNeeded > numNotes) {
            notesNeeded = numNotes;
            numNotes = 0;
        }
        else {
            numNotes -= notesNeeded;
        }

        if(notesNeeded > 0)
            cout << "Dispensing " << notesNeeded << " x ₹100 notes.\n";

        int remainingAmount = amount - (notesNeeded * 100);
        if(remainingAmount > 0) {
            if(nextHandler != nullptr) nextHandler->dispense(remainingAmount);
            else {
                cout << "Remaining amount of " << remainingAmount << " cannot be fulfilled (Insufficinet fund in ATM)\n";
            }
        }
    }
};

// Client Code
int main() {
    // Creating handlers for each note type
    MoneyHandler* thousandHandler = new ThousandHandler(3);
    MoneyHandler* fiveHundredHandler = new FiveHundredHandler(5);
    MoneyHandler* twoHundredHandler= new TwoHundredHandler(10);
    MoneyHandler* hundredHandler= new HundredHandler(20);

    // Setting up the chain of responsibility
    thousandHandler->setNextHandler(fiveHundredHandler);
    fiveHundredHandler->setNextHandler(twoHundredHandler);
    twoHundredHandler->setNextHandler(hundredHandler);

    int amountToWithdraw = 4000;

    // Initiating the chain
    cout << "\nDispensing amount: ₹" << amountToWithdraw << endl;
    thousandHandler->dispense(amountToWithdraw);

    return 0;
}

References

Leave a comment

Your email address will not be published. Required fields are marked *