A Calm Coder

Coding, thinking, and more.

Advertisements

A Brief Overview of the Chain of Responsibility Pattern

1. What is Chain of Responsibility (CoR)

The Chain of Responsibility is a behavioral design pattern used to delegate a request from its source object to a series of handler objects. To achieve decoupling between the source and handlers while segmenting their responsibilities, the handlers are linked together, forming a chain of responsibility.

As described in the GoF book, the Chain of Responsibility (CoR) pattern helps reduce coupling and enhances flexibility in assigning responsibility, especially when the appropriate handler is unknown beforehand or when dealing with uncertain changes.

If you need to handle a request while segmenting the processing into distinct handling sessions for better maintainability and flexibility, this approach is worth considering.

2. Implementation in OOP Language.

A typical implementation of the CoR pattern involves an abstract handler class and a series of concrete handlers that extend it. Generally, an interface is defined to establish the contract for handler classes.

A simplest example in TypeScript:

interface Handler<Request=string, Result=string> {
    setSuccessor(handler: Handler<Request, Result>) : Handler<Request, Result>;
    
    handle(request: Request) : Result;
}

abstract class AbstractHandler<Request=string, Result=string> implements Handler<Request, Result>
{
    private successor: Handler<Request, Result>;

    public setSuccessor(handler: Handler<Request, Result>): Handler<Request, Result> {
        this.successor = handler;
        return handler;
    }

    public handle(request: Request): Result {
        if (this.successor) {
            return this.successor.handle(request);
        }

        return "" as unknown as Result;
    }
}

class ConcreteHandler1 extends AbstractHandler<string, string> {
    public handle(request: string): string {
        if (request === 'forConcreteHandler1') {
            return `this is handled by Concrete Handler 1.`;
        }

        return super.handle(request);
    }
}

class ConcreteHandler2 extends AbstractHandler<string, string> {
    public handle(request: string): string {
        if (request === 'forConcreteHandler2') {
            return `this is handled by Concrete Handler 2.`;
        }

        return super.handle(request);
    }
}


function clientCode(handler: Handler<string, string>) {
    const requests = ['forConcreteHandler1','forConcreteHandler2', 'forConcreteHandler3'];

    for (const r of requests) {
        console.log(`Launching Request ${r}`);

        const result = handler.handle(r);
        if (result !== '') {
            console.log(result);
        } else {
            console.log(`Request ${r} is left unhandled.`);
        }
    }
}

const h1 = new ConcreteHandler1();
const h2 = new ConcreteHandler2();

h1.setSuccessor(h2);


/*
Call client code.
*/
console.log('handler 1 -> handler 2.\n');
clientCode(h1);

3. How It Works

If you find it challenging to grasp the mechanism between these classes, here’s a quick takeaway:

  1. OOP Implementation: In object-oriented programming (OOP), interfaces and abstract classes help define concrete implementations and enforce consistency, particularly in compiled languages. The super class is responsible for tracing the successor handler, following the Open/Closed Principle. This means that while the concrete handling logic can be extended in concrete handler classes, the successor attribute and setter method for this successor attribute remain encapsulated within the abstract class.

  2. Handler Chaining: The actual chaining of handlers occurs in the execution code. Since handlers can be subject to modification, CoR offers significant flexibility in structuring the chain.

In a future programming post, I will discuss some challenges that may arise in real-world development and explore possible workarounds.


Advertisements

Leave a comment