Overview
The Chain of Responsibility Design Pattern is a behavioral pattern that allows you to chain together a series of objects to handle a request in a flexible and extensible way. This pattern is useful when you have a series of objects that can handle a request but don’t know which one is capable of handling it. By using the Chain of Responsibility Design Pattern, you can pass the request through the chain of objects until one of them is able to handle it. This pattern is a good fit when the problem that is being solved requires flexibility of each element in the chain having autonomy in deciding when a request should stop processing and when this condition is different for each element of the chain.
The pattern is useful when a problem being decomposed can be effectively represented as a series of chained objects, in which a request is passed through those objects until one of them is capable of handling it.
For example, let’s say that we want to create an authentication system. It should be possible to authenticate using:
- Username and password
- Token
- Certificate
The system should authenticate using the first available method. Such a problem can be presented using the Chain of Responsibility Design Pattern.
Use Cases
Here are some use cases for the Chain of Responsibility Design Pattern:
- Authentication: You can use the Chain of Responsibility Design Pattern to create an authentication system that supports different authentication methods, like username and password-based authentication, token-based authentication, certificate-based authentication, and biometric data based authentication. Each handler in the chain checks if the user can be authenticated with a specific method and passes request to the next handler if authentication method is not supported.
- Authorization: You can use the Chain of Responsibility Design Pattern to create a chain of authorization handlers that check if a user has the necessary permissions to access a resource. Each handler in the chain can check a specific permission and pass the request to the next handler if the permission is not granted.
- Exception handling: You can use the Chain of Responsibility Design Pattern to create a chain of exception handlers that handle exceptions based on their type. Each handler in the chain can decide whether to handle the exception or pass it to the next handler in the chain.
- Validation: You can use the Chain of Responsibility Design Pattern to create a chain of validators that validate a user input based on different criteria. Each validator in the chain can check a specific criterion and pass the input to the next validator if it doesn’t meet the criterion.
Structure
The Chain of Responsibility Design Pattern consists of three main components:
- Handler: An abstract class or an interface that defines the interface for handling requests and has a reference to the next handler in the chain.
- ConcreteHandler: A concrete implementation of the Handler that will handle the request if it is capable of doing so or will pass the request to the next handler in the chain.
- Client: The object that creates the chain of handlers and sends requests to the first handler in the chain.
Here is a UML diagram that illustrates the structure of the Chain of Responsibility Design Pattern:
Example Code
In the following example, we will create an authentication system using the Chain of Responsibility Design Pattern. We will have three handlers:
- Username and Password based Authentication Handler
- Token-based Authentication Handler
- Certificate-based AuthenticationHandler
The authentication system should authenticate a user based on the first available option. If none of the authentication options successfully authenticate the user, authentication should be unsuccessful.
Let’s start by creating the BaseAuthenticationHandler:
public abstract class BaseAuthenticationHandler {
private BaseAuthenticationHandler nextHandler;
public abstract boolean handleAuthentication(AuthenticationData authenticationData);
public void setNextHandler(BaseAuthenticationHandler handler) {
this.nextHandler = handler;
}
protected boolean handleNext(AuthenticationData authenticationData) {
if (nextHandler != null) {
return nextHandler.handleAuthentication(authenticationData);
}
return false;
}
}
Then we will implement authentication handlers:
public class UsernamePasswordAuthenticationHandler extends BaseAuthenticationHandler {
@Override
public boolean handleAuthentication(AuthenticationData authenticationData) {
System.out.println("Executing username and password based authentication....");
if (authenticationData.doesSupport(UsernamePasswordAuthenticationData.class)) {
UsernamePasswordAuthenticationData usernamePasswordAuthenticationData = authenticationData.toType();
return usernamePasswordAuthenticationData.getUsername().equals("admin")
&& usernamePasswordAuthenticationData.getPassword().equals("123");
}
return handleNext(authenticationData);
}
}
public class TokenAuthenticationHandler extends BaseAuthenticationHandler {
@Override
public boolean handleAuthentication(AuthenticationData authenticationData) {
System.out.println("Executing Token based authentication....");
if (authenticationData.doesSupport(TokenAuthenticationData.class)) {
TokenAuthenticationData tokenAuthenticationData = authenticationData.toType();
return tokenAuthenticationData.getToken().equals("token200");
}
return handleNext(authenticationData);
}
}
public class CertificateAuthenticationHandler extends BaseAuthenticationHandler {
@Override
public boolean handleAuthentication(AuthenticationData authenticationData) {
System.out.println("Executing Certificate based authentication....");
if (authenticationData.doesSupport(CertificateAuthenticationData.class)) {
CertificateAuthenticationData certificateAuthenticationData = authenticationData.toType();
return certificateAuthenticationData.getCertificate().equals("certificate150");
}
return handleNext(authenticationData);
}
}
We will also create a class that will prepare chained authentication handlers:
public class AuthenticationHandler {
private final BaseAuthenticationHandler rootAuthenticationHandler;
public AuthenticationHandler() {
var usernamePasswordAuthenticationHandler = new UsernamePasswordAuthenticationHandler();
var tokenAuthenticationHandler = new TokenAuthenticationHandler();
var certificateAuthenticationHandler = new CertificateAuthenticationHandler();
usernamePasswordAuthenticationHandler.setNextHandler(tokenAuthenticationHandler);
tokenAuthenticationHandler.setNextHandler(certificateAuthenticationHandler);
rootAuthenticationHandler = usernamePasswordAuthenticationHandler;
}
public boolean handleAuthentication(AuthenticationData authenticationData) {
System.out.println("Trying to authenticate with " + authenticationData.getClass().getSimpleName() + "...");
boolean authenticationResult = rootAuthenticationHandler.handleAuthentication(authenticationData);
System.out.println("Authentication was " + (authenticationResult ? "successful" : "unsuccessful"));
System.out.println();
return authenticationResult;
}
}
We will then try to authenticate using various methods:
var authenticationHandler = new AuthenticationHandler();
authenticationHandler.handleAuthentication(new UsernamePasswordAuthenticationData("admin", "123"));
authenticationHandler.handleAuthentication(new TokenAuthenticationData("token200"));
authenticationHandler.handleAuthentication(new CertificateAuthenticationData("certificate150"));
authenticationHandler.handleAuthentication(new BiometricAuthenticationData());
Which will produce:
Trying to authenticate with UsernamePasswordAuthenticationData...
Executing username and password based authentication....
Authentication was successful
Trying to authenticate with TokenAuthenticationData...
Executing username and password based authentication....
Executing Token based authentication....
Authentication was successful
Trying to authenticate with CertificateAuthenticationData...
Executing username and password based authentication....
Executing Token based authentication....
Executing Certificate based authentication....
Authentication was successful
Trying to authenticate with BiometricAuthenticationData...
Executing username and password based authentication....
Executing Token based authentication....
Executing Certificate based authentication....
Authentication was unsuccessful
You can find the source code for this example on GitHub.
Summary
The Chain of Responsibility Design Pattern is a behavioral pattern that allows you to chain together a series of objects to handle a request in a flexible and extensible way. This pattern is useful when you have a series of objects that can handle a request but don’t know which one is capable of handling it. By using the Chain of Responsibility Design Pattern, you can pass the request through the chain of objects until one of them is able to handle it. The structure of the pattern consists of three main components: Handler, ConcreteHandler, and Client. The pattern can be used in various use cases, such as authorization, exception handling and validation.
References
- Erich G, Richard H, Ralph J, John V. Chain of Responsibility. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley Professional. October 31, 1994.
- Chain of Responsibility Design Pattern. Refactoring Guru. Accessed May 2, 2023. https://refactoring.guru/design-patterns/chain-of-responsibility
- Chain of Responsibility Design Pattern. Sourcemaking. Accessed May 2, 2023. https://sourcemaking.com/design_patterns/chain_of_responsibility