Skip to main content

Chain of Responsibility

The Chain of Responsibility pattern is a behavioral design pattern used in software engineering, which is especially useful for handling requests.

Concept

  • Purpose: It allows you to pass requests along a chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain.
  • Avoids Coupling: The sender of a request is not coupled to a particular receiver. This gives more flexibility in distributing responsibilities among objects.
  • Example Use-Case: Consider a system where multiple checks (like validation, authentication, authorization) need to be performed. Each of these checks can be a handler in the chain.

Components

chain of responsibility
  1. Handler Interface: Defines a method for handling requests and an optional method for setting the next handler in the chain.
  2. Concrete Handlers: Implement the handler interface. They perform specific actions, check if the request should be handled, and pass the request to the next handler if necessary.
  3. Client: Initiates the request to a chain of handler objects.

How It Works

  • A request is sent by the client and is passed along a chain of handler objects.
  • Each handler either handles the request or forwards it to the next handler in the chain.
  • The request travels along the chain until a handler handles it or the chain is exhausted.

Advantages

  • Single Responsibility Principle: Each handler in the chain handles a specific part of the request, keeping the classes small and focused.
  • Open/Closed Principle: New handlers can be added without changing the existing code.
  • Flexibility: The chain's structure can be dynamically altered or extended.

Disadvantages

  • Performance: The request can take a long time to traverse the entire chain if the chain is long.
  • Debugging Difficulty: It can be hard to observe the path of the request through the chain, making debugging challenging.

TypeScript Example

interface Handler {
setNext(handler: Handler): Handler;
handle(request: string): void;
}

abstract class AbstractHandler implements Handler {
private nextHandler: Handler;

public setNext(handler: Handler): Handler {
this.nextHandler = handler;
return handler;
}

public handle(request: string): void {
if (this.nextHandler) {
this.nextHandler.handle(request);
}
}
}

class ConcreteHandler1 extends AbstractHandler {
public handle(request: string): void {
if (request === "handle1") {
console.log(`ConcreteHandler1 handled request: ${request}`);
} else {
super.handle(request);
}
}
}

class ConcreteHandler2 extends AbstractHandler {
public handle(request: string): void {
if (request === "handle2") {
console.log(`ConcreteHandler2 handled request: ${request}`);
} else {
super.handle(request);
}
}
}

// Usage
const handler1 = new ConcreteHandler1();
const handler2 = new ConcreteHandler2();

handler1.setNext(handler2);

handler1.handle("handle2"); // This will be handled by ConcreteHandler2

This pattern is particularly useful in scenarios where a request may be handled in multiple ways, or when the processing might involve several steps that can be decoupled from each other.

Tips for Fullstack Developers

  • When implementing this pattern in a React application, consider how you can use composition to create a chain of components, each handling specific aspects of functionality.
  • The Chain of Responsibility can be useful in middleware patterns, common in Node.js backends, where you have a series of functions that process incoming HTTP requests.
  • Think about error handling in the context of this pattern. Ensure that errors are either handled or passed along the chain appropriately.
  • Keep an eye on the performance implications, especially in a full-stack environment where both client-side and server-side processing might be involved.

Advanced Implementation Concept

  • Implicit Next: Handlers do not explicitly call the next handler. Instead, the framework or underlying system takes care of progressing to the next handler.
  • Automatic Continuation: The continuation to the next handler can be automatic unless the current handler decides to terminate the chain, often done by not calling next() or by sending a response.

TypeScript Example with Implicit Next

Consider an example similar to middleware in Express.js:

type Request = { /*...*/ };
type Response = { /*...*/ };
type NextFunction = () => void;

interface Middleware {
(req: Request, res: Response, next: NextFunction): void;
}

class MiddlewareChain {
private middlewares: Middleware[] = [];

public use(middleware: Middleware) {
this.middlewares.push(middleware);
}

public handleRequest(req: Request, res: Response) {
const execute = (index: number) => {
if (index < this.middlewares.length) {
this.middlewares[index](req, res, () => execute(index + 1));
}
};

execute(0);
}
}

// Usage
const chain = new MiddlewareChain();

chain.use((req, res, next) => {
console.log("Middleware 1");
next(); // Proceed to next middleware
});

chain.use((req, res, next) => {
console.log("Middleware 2");
// This middleware doesn't call next(), so the chain ends here
});

const request: Request = { /*...*/ };
const response: Response = { /*...*/ };

chain.handleRequest(request, response);

In this example:

  • Each middleware function takes req, res, and next as arguments.
  • Calling next() within a middleware function proceeds to the next middleware in the chain.
  • If a middleware function doesn't call next(), the chain stops.

Fullstack Developer Tips

  • When designing middleware-like systems in a React or Node.js environment, consider how you can abstract the flow control to make the individual components or middleware functions as simple and focused as possible.
  • Testability is key. Ensure each handler or middleware function is easily testable in isolation.
  • Always be mindful of the error handling. In a chain, an unhandled error in one link can break the entire chain.
  • When using this pattern on the frontend, especially with React, consider how the context API or higher-order components can be utilized to implement a similar flow in a component hierarchy.

Functional Chain of Responsibility

type HandlerFunction<T> = (request: T, next: (request: T) => void) => void;

function createChain<T>(...handlers: HandlerFunction<T>[]): (request: T) => void {
return (request: T) => {
const execute = (index: number, req: T) => {
if (index < handlers.length) {
handlers[index](req, () => execute(index + 1, req));
}
};

execute(0, request);
};
}

// Example Usage
// Define some handlers
const checkNumber = (request: number, next: (request: number) => void) => {
if (request > 10) {
console.log("Number is greater than 10");
next(request);
} else {
console.log("Number is 10 or less");
}
};

const doubleNumber = (request: number, next: (request: number) => void) => {
console.log(`Doubling number: ${request}`);
next(request * 2);
};

// Create the chain
const processNumber = createChain(checkNumber, doubleNumber);

// Use the chain
processNumber(5); // Output will be "Number is 10 or less"
processNumber(15); // Outputs "Number is greater than 10" followed by "Doubling number: 15"

In this implementation:

  • HandlerFunction<T> is a type for a handler function that takes a request and a next function.
  • createChain function takes a series of handler functions and returns a new function representing the chain.
  • Each handler decides whether to process the request and whether to call next to pass the request to the next handler.

Fullstack Developer Perspective

  • This functional approach aligns well with the principles of functional programming, emphasizing immutability and pure functions.
  • In a Node.js backend, this could be used for creating middleware-like chains where each function processes the request in some way and decides whether to pass it on.
  • In a React application, a similar approach could be taken to compose a sequence of functions for processing data or handling events.
  • Ensuring each function has a single responsibility and is pure (no side effects) makes the code easier to test and reason about.
  • This pattern is also useful for creating pipelines in data processing, where data flows through a series of transformations.