3.4 - Chain of Responsibility Pattern: Behavioral Design Patterns

The Chain of Responsibility Pattern is a behavioral design pattern that allows multiple objects to handle a request in a chain structure. The request gets passed along the chain until it is handled by one of the objects. This pattern promotes loose coupling and allows multiple handlers to process the request without the sender needing to know who will handle it.

Components of the Chain of Responsibility Pattern:

  1. Handler Interface: Declares the method to handle requests and allows setting the next handler in the chain.

  2. Concrete Handlers: Implement the handler interface and define the request handling logic. Each concrete handler is responsible for handling certain types of requests and passing along unhandled requests to the next handler in the chain.

  3. Request: Encapsulates the request details, such as the type or other relevant information.

  4. Client: Creates and configures the chain of handlers, then submits requests for processing.

Code Example

Below is an implementation of the Chain of Responsibility Pattern with renamed class and function names to enhance clarity.

1. Handler Interface

The Processor interface defines the methods for setting the next processor in the chain and for handling requests.

// Handler Interface
interface Processor {
    void setNextProcessor(Processor processor);
    void processRequest(TaskRequest request);
}

2. Concrete Handlers

The TaskHandlerA and TaskHandlerB classes implement the Processor interface. Each class processes a specific type of request and forwards unhandled requests to the next processor in the chain.

// Concrete Handler A
class TaskHandlerA implements Processor {
    private Processor nextProcessor;

    @Override
    public void setNextProcessor(Processor processor) {
        this.nextProcessor = processor;
    }

    @Override
    public void processRequest(TaskRequest request) {
        if (request.getRequestType().equals(TaskType.TYPE_A)) {
            System.out.println("TaskHandlerA is processing request of Type A");
            // Handle request logic here
        } else if (nextProcessor != null) {
            nextProcessor.processRequest(request);
        }
    }
}

// Concrete Handler B
class TaskHandlerB implements Processor {
    private Processor nextProcessor;

    @Override
    public void setNextProcessor(Processor processor) {
        this.nextProcessor = processor;
    }

    @Override
    public void processRequest(TaskRequest request) {
        if (request.getRequestType().equals(TaskType.TYPE_B)) {
            System.out.println("TaskHandlerB is processing request of Type B");
            // Handle request logic here
        } else if (nextProcessor != null) {
            nextProcessor.processRequest(request);
        }
    }
}

3. Request

The TaskRequest class represents the request being sent along the chain. It contains details about the request type.

// Request class
class TaskRequest {
    private final TaskType requestType;

    public TaskRequest(TaskType requestType) {
        this.requestType = requestType;
    }

    public TaskType getRequestType() {
        return requestType;
    }
}

4. Enum for Request Type

The TaskType enum defines the types of requests that can be processed.

// Enum for Request Types
enum TaskType {
    TYPE_A, TYPE_B
}

5. Client

The ChainDemo class sets up the chain of responsibility by creating handlers and linking them. It then submits requests to be processed.

// Client class
public class ChainDemo {
    public static void main(String[] args) {
        // Create handlers
        Processor handlerA = new TaskHandlerA();
        Processor handlerB = new TaskHandlerB();

        // Set up the chain of processors
        handlerA.setNextProcessor(handlerB);

        // Create requests
        TaskRequest request1 = new TaskRequest(TaskType.TYPE_A);
        TaskRequest request2 = new TaskRequest(TaskType.TYPE_B);
        TaskRequest request3 = new TaskRequest(TaskType.TYPE_A);

        // Process requests through the chain
        handlerA.processRequest(request1);
        handlerA.processRequest(request2);
        handlerA.processRequest(request3);
    }
}

Explanation:

  1. Processor Interface: The Processor interface declares methods for setting the next processor and processing requests. It ensures that all handlers in the chain can pass requests along the chain if they can't handle them.

  2. TaskHandlerA and TaskHandlerB: These are concrete handlers that implement the Processor interface. Each handler checks if the request type matches what it can handle. If it can, it processes the request; otherwise, it forwards the request to the next processor.

  3. TaskRequest: The TaskRequest class represents the request and holds information about the request type (either TYPE_A or TYPE_B).

  4. TaskType: An enum that defines the possible types of requests (TYPE_A and TYPE_B).

  5. ChainDemo: The client that creates and links the processors, submits requests, and starts the chain.

Benefits of the Chain of Responsibility Pattern:

  1. Loose Coupling: The sender of a request does not need to know which object will handle the request or how many objects are in the chain. It only needs to know the starting point of the chain.

  2. Flexibility: You can dynamically modify the chain by adding, removing, or changing the order of handlers without affecting other parts of the code.

  3. Responsibility Division: This pattern allows you to divide the processing logic across multiple objects, each responsible for handling a specific type of request. This helps with the Single Responsibility Principle.

  4. Scalability: The chain can be easily extended by adding new handlers that can process different types of requests, making it more scalable for complex systems.

Real-World Use Cases:

  • Technical Support Systems: Requests for technical support are often processed by multiple levels of support, from frontline support agents to specialized experts.

  • Event Handling: In UI frameworks, events (like mouse clicks or keyboard input) are handled by a chain of components. If one component doesn’t handle the event, it gets passed to the next.

  • Authentication Middleware: In web applications, authentication and authorization are often processed through a chain of middleware functions, each checking specific security aspects of the request.

Conclusion:

The Chain of Responsibility Pattern is a powerful tool for structuring code where multiple objects may need to handle a request. It simplifies complex decision-making processes, promotes loose coupling, and allows for greater flexibility in request handling. By using this pattern, you can create a robust and maintainable system that can easily evolve to accommodate new types of requests or changes in handling logic.