Handling events using the chain of responsibility pattern
The chain of responsibility pattern helps avoid tying the handler logic to the sender that fired the event. This pattern was identified by the GoF’s book.
Motivation
The program receives an initial triggered event. Each chained handler decides whether to process the request or pass it on to the next handler without responding. A pattern can consist of command objects that are processed by a series of handler objects. Some handlers can act as dispatchers, capable of sending commands in different directions to form a responsibility tree.
The chain of responsibility pattern allows you to build a chain of implementations in which a certain action is performed before or after calling the next handler in the chain.
Finding it in the JDK
The java.logging
module includes the java.util.logging
package, which contains a Logger
class, intended for recording application component messages. Loggers can be chained and the logged message is only processed by the desired Logger
instances.
Another example provided in the JDK is the DirectoryStream
class, which comes with the java.base
module and is located in the java.nio
package. This class is responsible for iterating over entire directories and contains a nested filter interface. The interface provides an accept
method. The actual representation of the chained filter differentiates depending on whether the directory is to be processed or excluded.
Sample code
Let us examine an example of how the chain of responsibility design pattern can be used to respond to a trigger event from the driver system (Example 5.3):
System.out.println("Pattern Chain of Responsibility, vehicle system initialisation"); var engineSystem = new EngineSystem(); var driverSystem = new DriverSystem(); var transmissionSystem = new TransmissionSystem(); driverSystem.setNext(transmissionSystem); transmissionSystem.setNext(engineSystem); driverSystem.powerOn(); }
Here’s the output:
Pattern Chain of Responsibility, vehicle system initialisation DriverSystem: activated TransmissionSystem: activated EngineSystem, activated
Example 5.3 – The DriverSystem instance initiates the powerOn event that is propagated through the chained instances
The behavior of the system chain created is transparent, and the logic is properly encapsulated by each system. The provided generic abstract VehicleSystem
instance defines the functionality, what function each element must fulfill, and how the following elements should be chained (Example 5.4):
sealed abstract class VehicleSystem permits DriverSystem, EngineSystem, TransmissionSystem { ... protected VehicleSystem nextSystem; protected boolean active; ... void setNext(VehicleSystem system){ this.nextSystem = system; } void powerOn(){ if(!this.active){ activate(); } if(nextSystem != null){ nextSystem.powerOn(); } } }
Example 5.4 – The sealed classes usage provides additional stability and control
The client receives a framework for how and which classes can be considered when building a chain (Figure 5.2):
Figure 5.2 – The UML class diagram showing which elements participate in the powerOn event
Conclusion
The chain of responsibility pattern showed that an incoming event that affects the runtime behavior of a program can result in the creation of multiple objects. The manipulators are encapsulated and the logic is properly isolated according to SOLID principles. Using this pattern, the client gets the opportunity to dynamically decide which handlers should be involved in the event process. Therefore, it is a hot candidate for security frameworks or similar.
Chained handlers can issue multiple commands to the client at runtime. Let’s explore command responses in more detail.