Chain of Responsibility Design Pattern in Python
What is it?
The Chain of Responsibility Design Pattern caters to the requirement that different requests need different processing. Based on the request, different types of processing are needed. For example, you have a list of numbers, say 6, 12, 24, 18 and you need to process these numbers according to the range in which they lie. That is, you wish to process numbers 1-10 in one way, 11-20 in another way, and numbers not in the range 1-20 in another way. The Chain of Responsibility Pattern provides a sophisticated method to handle this design.
It is classified under Behavioural Design Patterns as it offers one of the best ways to handle communication between objects.
Why the need for it: Problem Statement
The Chain of Responsibility caters to the requirement that different requests need different processing.
Terminology
- Abstract Handler: The Abstract handler is inherited by all Concrete Handlers. It provides the following 3 methods:
- init(): stores a successor handler who will handle the request if it is not handled by the current handler.
- handle(): the handle() method is inherited by the concrete handlers as-is. It is invoked by delegate() method of Client. Further, it invokes the processRequest() method of the current handler.
- processRequest(): the processRequest() method is overridden by the Concrete Handlers, where each concrete handler provides its own logic to handle the requests.
- Concrete Handlers: Each Concrete Handler inherits from the Abstract Handler, has the handle() method by default, due to inheritance of the Abstract Handler and overrides the processRequest() method of the Abstract Handler in which it provides its own logic to handle requests.
- init(): inherited as-is from the Abstract Handler. This method is not present in the class definition of the Concrete Handler. It stores a successor handler who will handle the request if it is not handled by the current Concrete Handler.
- handle(): inherited as-is from the Abstract Handler. This method is also not present in the class definition of the Concrete Handler. It is invoked by the delegate() method of the Client, and it further invokes the processRequest() of this Concrete Handler.
- processRequest(): overridden method of the Abstract Handler. Contains the logic of each Concrete Handler to handle requests. Each Concrete Handler will have their own logic. The method returns True if request handled successfully, if not, control goes back to the handle() method which passes the request to the successor handler.
- Default Handler: The Default Handler receives the request after each of the Concrete Handlers have tried to process it and failed. The Default Handler also inherits from Abstract Handler, has the handle() method by default, due to inheritance of the Abstract Handler and overrides the processRequest() method of the Abstract Handler to inform the user that there is no concrete handler in place to handle this request.
- Client: The Client defines a sequence in which the responsibility will be handed over of handling the requests. The Client takes the requests, and delegates them one by one to the Concrete Handlers, using the delegate() method.
- init(): defines the sequence in which the chain of responsibility will flow.
- delegate(): takes requests as input, iterates over them, and sends them, one by one, to handlers as per sequence of handlers defined in the init() method.
Pseudo Code
class AbstractHandler: '''Abstract Handler: inherited by all concrete handlers; throws a NotImplementedError if the concrete handler does not define its own copy of processRequest() method.''' def __init__(successor): ''''sets the next handler to local variable "_successor"''' def handle(request): '''invokes the processRequest() of the current handler; if request is handled, then processing of next request begins; if request cannot be handled by the current handler, it is passed on to the handle() method of the successor handler.''' def processRequest(request): '''throws a NotImplementedError if the concrete handler does not define its own copy of processRequest() method.''' class ConcreteHandlerOne(AbstractHandler): '''Concrete Handler # 1: Inherits from the abstract handler; overrides the processRequest() method of the AbstractHandler; has the handle() method by default, due to inheritance of the AbstractHandler''' def processRequest(request): '''Attempt to handle the request; return True if handled''' class ConcreteHandlerTwo(AbstractHandler): '''Concrete Handler # 2: Inherits from the abstract handler; overrides the processRequest() method of the AbstractHandler; has the handle() method by default, due to inheritance of the AbstractHandler''' def processRequest(request): '''Attempt to handle the request; return True if handled''' class DefaultHandler(AbstractHandler): '''Default Handler: inherits from the abstract handler; overrides the processRequest() method of the AbstractHandler; has the handle() method by default, due to inheritance of the AbstractHandler''' def processRequest(request): '''Provide an elegant message saying that this request has no handler. returns True to imply that even this request has been handled.''' class Client: '''Client: Uses handlers''' def __init__(): '''Create the sequence of handlers that you want the requests to follow, and assign the sequence to local variable "handle".''' def delegate(requests): '''Iterates over requests and sends them, one by one, to handlers as per sequence of handlers defined above.''' ## USING THE ABOVE SETUP ## # Create a client object # Create requests to be processed. # By calling the delegate() method of the client object, send the requests, one by one, to handlers as per sequence of handlers defined in the Client class.
How to implement it
Consider the scenario where you have a list of numbers, say 6, 12, 24, 18 and you need to process these numbers according to the range in which they lie. That is, you wish to process numbers 1-10 in one way, 11-20 in another way and numbers not in the range 1-20 in another way. Here's one way you can achieve the purpose using the Chain of Responsibility Design Pattern.
class AbstractHandler: '''Abstract Handler: inherited by all concrete handlers; throws a NotImplementedError if the concrete handler does not define its own copy of processRequest() method.''' def __init__(self, successor): ''''sets the next handler to local variable "_successor"''' self._successor = successor def handle(self, request): '''invokes the processRequest() of the current handler; if request is handled, then processing of next request begins; if request cannot be handled by the current handler, it is passed on to the handle() method of the successor handler.''' handled = self.processRequest(request) if not handled: self._successor.handle(request) def processRequest(self, request): '''throws a NotImplementedError if the concrete handler does not define its own copy of processRequest() method.''' raise NotImplementedError('Must provide implementation in subclass!') class ConcreteHandlerOne(AbstractHandler): '''Concrete Handler # 1: Inherits from the abstract handler; overrides the processRequest() method of the AbstractHandler; has the handle() method by default, due to inheritance of the AbstractHandler''' def processRequest(self, request): '''Attempt to handle the request; return True if handled''' if 0 < request <= 10: print("This is {} handling request '{}'".format(self.__class__.__name__, request)) return True class ConcreteHandlerTwo(AbstractHandler): '''Concrete Handler # 2: Inherits from the abstract handler; overrides the processRequest() method of the AbstractHandler; has the handle() method by default, due to inheritance of the AbstractHandler''' def processRequest(self, request): '''Attempt to handle the request; return True if handled''' if 10 < request <= 20: print("This is {} handling request '{}'".format(self.__class__.__name__, request)) return True class DefaultHandler(AbstractHandler): '''Default Handler: inherits from the abstract handler; overrides the processRequest() method of the AbstractHandler; has the handle() method by default, due to inheritance of the AbstractHandler''' def processRequest(self, request): '''Provide an elegant message saying that this request has no handler. returns True to imply that even this request has been handled.''' print("This is {} telling you that request '{}' has no handler right now.".format(self.__class__.__name__, request)) return True class Client: '''Client: Uses handlers''' def __init__(self): '''Create the sequence of handlers that you want the requests to follow, and assign the sequence to local variable "handle". Notice that the DefaultHandler, like all Handlers, needs to be provided a successor, but since it is the last handler in line, we give it a successor "None".''' self.handler = ConcreteHandlerOne(ConcreteHandlerTwo(DefaultHandler(None))) def delegate(self, requests): '''Iterates over requests and sends them, one by one, to handlers as per sequence of handlers defined above.''' for request in requests: self.handler.handle(request) # Create a client object clientOne = Client() # Create requests to be processed. requests = [6, 12, 24, 18] # Send the requests one by one, to handlers as per sequence of handlers defined in the Client class. clientOne.delegate(requests) ### OUTPUT ### This is ConcreteHandlerOne handling request '6' This is ConcreteHandlerTwo handling request '12' This is DefaultHandler telling you that request '24' has no handler right now. This is ConcreteHandlerTwo handling request '18'
Walkthrough of implementation
- We first create a client object, after having defined the sequence of handlers in the definition of the Client class.
- Then, we create a few requests that we need to process.
- Then, using the delegate() method of the client object, we send the requests one by one, to handlers as per the sequence of handlers defined in the Client class.
The way this takes effect is that, the first request, 6 in this case, is sent to handle() method of the first handler in the sequence, ConcreteHandlerOne in this case. Before the handle() method, its __init__() is executed, which defines its successor as per the sequence in the definition of Client class, ConcreteHandlerTwo, in this case. Then the handle() method of the ConcreteHandlerOne tries to process the request by calling its processRequest() method, and if it succeeds, the second request, 12, follows the same path. If the processRequest() of ConcreteHandlerOne fails to handle the request, the __init__(), handle() and processRequest() of the successor i.e. ConcreteHandlerTwo are executed one by one, and the process carries on. If no concrete handler is able to handle the request, the request is passed to the DefaultHandler, and its processRequest() method notifies the user that there is no concrete handler in place for this request.
Related to: Composite
- Creational Patterns
- Factory
- Abstract Factory
- Prototype
- Singleton
- Builder
- Architectural Pattern
- Model View Controller (MVC)
See also: