Revisiting the Chain of Responsibility Design Pattern

Variations on a Classic Design Pattern for Modern Applications

In software design, the Chain of Responsibility pattern is a behavioral design pattern that allows a client object to pass a request along a chain of potential handlers until one of them handles the request. This pattern promotes the decoupling of sender and receiver, offering flexibility in assigning responsibilities to different objects. Originally described by the Gang of Four, the Chain of Responsibility pattern can be particularly useful in scenarios where multiple objects can handle a request, but the specific handler isn't known in advance.

Key Use Cases

  • Organization: Like many design patterns, the Chain of Responsibility can be a clear way to organize some algorithms, such as authentication chains.
  • Extensibility: Provides a clear method for extending existing application features, for example in plugin architectures.

Original Pattern, or the Competitive Variation

As originally described by the Gang of Four, the Chain of Responsibility pattern involves the following components:

  • Handler Interface: This interface declares a method for handling requests and a method to set the next handler in the chain.
  • Concrete Handlers: These classes implement the handler interface and handle the request or pass it along the chain.
  • Client: This code initiates the request, which gets passed along the chain of handlers until one of them handles it.
UML Diagram of Original/Competitive Variation

So a Client invokes the operation on a handler, which probably has static type Handler Interface (Operation) by in reality is a Concrete Handler (OperationA, OperationB). The handler then processes the operation itself, or delegates to the next handler, and so on until either the operation is processed successfully, or the end of the chain is reached.

A classic example of this pattern is a user authentication system, where authentication requests are passed through a chain of handlers until either one of the handlers successfully authenticates the user and returns a session, or the end of the chain is reached and the user is not authenticated.

One defining characteristic of the Chain of Responsibility pattern in its original incarnation is that the caller receives feedback about the processing of the operation in the form of a return value.

The Collaborative Variation

The Collaborative variation involves the same components and relationships as the Competitive variation, but the handlers are called according to a different call protocol. Where the Competitive variation returns the result from the first handler in the chain that produces one, the Collaborative version instead returns the combined result from all handlers in the chain. So whereas Concrete Handlers in the Competitive variation return the result immediately if they are able to process the operation successfully, or otherwise delegate to the next Handler, Handlers in the Collaborative variation always delegate to the next handler, and then modify the result from the delegate before returning the modified response.

A classic example of this pattern is text formatting systems, where each handler performs specific text formatting tasks on a tree representation of a document.

The Consumer Variation

The Consumer variation involves the same components and relationships as the Collaborative variation and even uses the same call protocol, but it doesn't return a value. The caller initiates the request not for the result, since the handlers produce none, but rather for the side effects of the invocation and for the mutations the handlers apply to the inputs.

UML Diagram of the Consumer Variation. Note that operation has void return type.

A classic example of this pattern is HTTP request filtering, where each handler can inspect the request and make changes as desired.

While similar, this variation of the Chain of Responsibility pattern differs from the Observer pattern in that in the Chain of Responsibility pattern, the Client gets feedback from the invocation, whereas in the Observer pattern, the Client does not.

The Master Object Variation

The Master Object variation introduces a new component, the Master Object, which is responsible for managing the chain of handlers and the call protocol:

  • Handler Interface: This interface declares a method for handling requests only.
  • Master Object: This class manages the sequence of handlers, allowing for dynamic addition, removal, or reordering of handlers as needed. It also typically implements the Handler Interface itself. This object is responsible for invoking individual handlers according to the call protocol.
  • Concrete Handlers: Each handler checks if it can process the request and returns a response if it can, or an empty result otherwise.
  • Client: Initiates the request through the Master Object.

This variation decouples the Handler Interface from the manner of its invocation. Because the Master Object implements the Handler Interface, the pattern still promotes the decoupling of the sender and the receiver. In most cases, Concrete Handlers don't have to know they're part of a chain, and the Client doesn't have to know it's using a chain.

This variation can be used to simplify any of the above variations.

Conclusion

The Chain of Responsibility pattern as originally described by the Gang of Four still provides a powerful way to decouple senders and receivers of requests. However, modern real-world applications often demand variations to fit specific requirements. By introducing a master object to control the call order or changing the call protocol, developers can create flexible and adaptable systems that leverage the strengths of both competitive and collaborative handling approaches while reducing coupling and minimizing incremental complexity.

More blog posts

see all