The Chain of Responsibility pattern is a behavioural design pattern that lets a group of items handle a request in a certain order. In this pattern, we create a chain of objects, and each object can either handle the request itself or send it to the next object in the chain.
The idea behind the Chain of Responsibility pattern is to separate the object that sends a request from the object that gets it. We can achieve that by letting more than one object handle the request. The client doesn't have to be aware of the object that will handle the request, thus giving us the ability to extend or modify the chain without interrupting the flow of control.
You can find the example code of this post on GitHub
Conceptualizing the Problem
Assume we're developing an online betting system. We wish to limit system access so that only authenticated users can place bets. Moreover, users with administrative privileges must have complete access to all bets placed in our system.
After some planning, it becomes clear that these checks should be done one after the other. When a request comes in with the user's credentials, the system should try to authenticate the user to the server. But if the authentication fails because the credentials are wrong, there is no reason to check anything else.
As we go on our merry lives coding away, we eventually came across a couple of more cases that should be implemented as sequential checks. Someone was worried at first about the security of sending raw data straight to the betting system, so the requested data had to go through an extra step to clean it. After that, we found out that the system was open to brute-force password cracking. To stop illegal access, we quickly added a check that filters repeated failed requests from the same IP address.
Finally, the government passed a law that limited how much a player could bet in a single day and how long they could spend on our platform. Because of this, we had to add more checks to make sure that these rules are followed and that the order is carried out.
As we added more and more features to the system, the sequential checks code became more and more spaghettified. The original code, which was not the best specimen of a well-polished and clean module, got bigger and bigger as new features were added. And then it reached critical spaghetti mass. Changing one check sent so many ripples through the system we got a lot of unexpected effects on modules that shouldn't have any connection with the login system. Even worse, when some bright-eyed colleague tried to reuse a check to protect another part of the system, they fell into our monstrosity's trap and had to copy the whole thing for it to work.
As you might imagine, at this point, the situation was far from perfect, and keeping the system running took a lot of time, money, blood, sweat and tears. Every day was a fight with the code, and even a simple change in the process left trails of carnage in its wake. After fighting with the code for a while, it was decided to change the way the whole system works.
The Chain of Responsibility pattern, like many other behavioural design patterns, is based on the idea of turning individual actions into separate objects called handlers. In our case, each check should be in its own class, and the check should be done by a single method. The request and its details are then passed to this function as a parameter.
For this system to work, handlers are linked together in a chain. Each handler in the chain has a field where a reference to the next handler is saved. As handlers work on requests, they pass them along the chain until all handlers have had a chance to work on them.
One of the main benefits of this design is that a handler can choose not to send the request any further down the chain, which stops any further processing. This lets more flexible and modular systems be made that can handle different kinds of requests in different ways.
In our betting system example, a handler would do the necessary processing before choosing whether to send the request further down the chain. All handlers can do their main job, like checking for authentication or caching, as long as the request has correct data.
There are different ways to use the Chain of Responsibility pattern, and each one has a slightly different approach. One way is for the handler to decide as soon as it gets a request whether or not to process it. If it's possible, the handler takes care of the request and doesn't pass it on to the next entity in the chain. Most of the time, this way is used to handle events in stacks of elements in graphical user interfaces.
When a user hits a button, for example, the event moves through a chain of GUI elements, starting with the button, going through its containers like forms or panels, and ending with the main program window. The event is taken care of by the first thing in the chain that can take care of it.
This method is very helpful when there is a clear line of duty and a clear order of things. The request is sent down the chain until it is treated by the handler at the top of the chain. This can make it easier to create complex behaviour that involves many parts or subsystems.
All handler classes must implement the same interface. Each concrete handler should only be concerned with the one after it that has the execute method. This allows us to create chains at runtime by combining different handlers without attaching your code to their concrete classes.
Structuring the Chain of Responsibility Pattern
In its basic implementation, the Chain of Responsibility Pattern has 4 main participants:
- Handler: The Handler declares the interface, which is shared by all concrete handlers. It normally only has one method for handling requests, but it may also have a method for setting the next handler in the chain.
- Basic Handler: The Base Handler class is optional, and it contains boilerplate code that is shared by all handler classes. This class often includes a field for holding a reference to the next handler. Clients can create a chain by sending a handler to the previous handler's constructor or setter. The class may alternatively implement the default handling behaviour: after verifying its existence, it can pass execution to the next handler.
- Concrete Handlers: Concrete Handlers contain the actual code that is used to process requests. When a request arrives, each handler must decide whether to process it and, if so, whether to pass it along the chain. Handlers are typically self-contained and immutable, accepting all required data just once through the constructor.
- Client: Depending on the logic of the application, the Client may compose chains only once or dynamically. It is important to note that a request can be delivered to any handler in the chain and does not have to be the first one.
To demonstrate the Chain of Responsibility pattern we are going to create a system similar to the one we described above. In this example, we are going to handle a request containing user data. These handlers will perform various tasks, such as authentication, authorization, validation and sanitization.
First, we are going to create our IHandler participant:
public abstract class BaseMiddleware
{
private BaseMiddleware? _nextHandler = null;
public static BaseMiddleware Link(BaseMiddleware head, params BaseMiddleware[] chain)
{
var internalHead = head;
foreach(var nextLink in chain)
{
internalHead._nextHandler = nextLink;
internalHead = nextLink;
}
return head;
}
public abstract bool Check(string email, string password);
/// <summary>
/// Runs the check of the next object in the chain or ends traversal if this is the last object.
/// </summary>
/// <param name="email">The email to check</param>
/// <param name="password">The password to check</param>
/// <returns>The result of the next check in the chain, or true if this is the last link.</returns>
protected bool CheckNext(string email, string password)
{
return _nextHandler == null || _nextHandler.Check(email, password);
}
}
The BaseMiddleware
class contains all the boilerplate code for creating a chain of middleware components. The class declares the Link
method that allows us to create the chain of responsibility by connecting a handler with a list of other handlers. The class also declares the CheckNext
method that allows traversing the chain and triggering their Check
method.
It finally declares an abstract Check
method, that needs to be implemented by all subclasses. This method will contain the business logic for each individual handler.
The next step is to implement our ConcreteHandlers. First, we are going to implement our LoginThrottlingMiddleware
:
public class LoginThrottlingMiddleware : BaseMiddleware
{
private readonly int _requestsPerMinute;
private int _requestCount;
private long _currentTime;
public LoginThrottlingMiddleware(int requestsPerMinute)
{
_requestsPerMinute = requestsPerMinute;
_currentTime = DateTimeOffset.Now.ToUnixTimeMilliseconds();
}
public override bool Check(string email, string password)
{
if (DateTimeOffset.Now.ToUnixTimeMilliseconds() > _currentTime + 60000)
{
_requestCount = 0;
_currentTime = DateTimeOffset.Now.ToUnixTimeMilliseconds();
}
_requestCount++;
if (_requestCount <= _requestsPerMinute)
return CheckNext(email, password);
Console.WriteLine("Request limit exceeded. Please try again later.");
return false;
}
}
This method limits the amount of login requests a user can send per minute. The Check
method first checks if the time limit has passed and it resets the _requestCount
. Then a _requestCount
is increased by one. If the _requestCount
does not exceed the limit, it triggers the next handler in the chain. If it does indeed exceeds the limit, it stops the process.
Next up, the AuthenticationMiddleware
class:
public class AuthenticationMiddleware : BaseMiddleware
{
private readonly Server _server;
public AuthenticationMiddleware(Server server)
{
_server = server;
}
public override bool Check(string email, string password)
{
if (!_server.EmailExists(email) || _server.PassWordIsValid(email, password))
{
Console.WriteLine("Invalid username or password");
return false;
}
return CheckNext(email, password);
}
}
The AuthenticationMiddleware
implements its own flavour of the Check
method. The method communicates with the server and if the email does not exist or the password doesn't match the one registered under the email, it stops the chain. If all goes well, it calls the next handler in the chain.
Finally, we got our AuthorizationMiddleware
:
public class AuthorizationMiddleware : BaseMiddleware
{
public override bool Check(string email, string password)
{
if (email.Contains("admin"))
{
Console.WriteLine("Hello, admin!");
return CheckNext(email, password);
}
Console.WriteLine("Hello, user!");
return CheckNext(email, password);
}
}
This method is the easiest and probably the most secure method I have ever written. If the email contains the word admin
then, of course, the user is an admin! In any case, it then calls the next handler in the chain to continue the process.
Now, let's move on to our Server
class:
public class Server
{
private Dictionary<string, string> _users = new Dictionary<string, string>();
private BaseMiddleware _middleware;
public void SetMiddleware(BaseMiddleware middleware)
{
_middleware = middleware;
}
public bool Login(string email, string password)
{
if (_middleware.Check(email, password))
{
Console.WriteLine("Authorization successful");
return true;
}
return false;
}
public void Register(string email, string password)
{
_users.Add(email, CalculateHash(password));
}
public bool EmailExists(string email)
{
return _users.ContainsKey(email);
}
public bool PassWordIsValid(string email, string password)
{
var provided = CalculateHash(password);
return _users[email].Equals(provided);
}
private string CalculateHash(string value)
{
byte[] inputBytes = Encoding.UTF8.GetBytes(value);
// Create an instance of the SHA-512 algorithm
SHA512 sha512 = SHA512.Create();
// Compute the has value of the input bytes
byte[] hashBytes = sha512.ComputeHash(inputBytes);
// Convert the hash bytes to a hex string
StringBuilder sb = new StringBuilder();
for (var i = 0; i < hashBytes.Length; i++)
sb.Append(hashBytes[i].ToString("x2"));
return sb.ToString();
}
}
The Server
class represents the back-end system. It provides methods for registering a user and checking the user's credentials. I went on and created a method that calculates the SHA-512
hash of the password and uses that as the password value, to try and imitate how a real-life system should handle passwords.
Finally, let's create our Client participant, the Program
class:
class Program
{
private static Server server;
private static void Init()
{
server = new Server();
server.Register("admin@gmail.com", "this_is_super_secure_admin_password");
server.Register("user@gmail.com", "password123");
BaseMiddleware middleware = BaseMiddleware.Link(
new LoginThrottlingMiddleware(3),
new AuthenticationMiddleware(server),
new AuthorizationMiddleware());
server.SetMiddleware(middleware);
}
public static void Main(string[] args)
{
Init();
bool success;
do
{
Console.Write("Enter email: ");
string email = Console.ReadLine();
Console.Write("Input password: ");
string password = Console.ReadLine();
success = server.Login(email, password);
} while (!success);
}
Now let's try to run our program. In the run below, someone tried to log in as an admin. After three unsuccessful tries the user is throttled and no more requests are processed for a specific time frame:
Pros and Cons of the Chain of Responsibility Pattern
✔ We can control the order of request handling. | ❌Some requests may end up unhandled. |
✔We can decouple classes that invoke operations from classes that perform operations, thus respecting the Single Responsibility Principle. | |
✔We can introduce new handlers into the app without breaking the existing client code, thus respecting the Open/Closed Principle |
Relations with Other Patterns
-
Chain of Responsibility, Chain of Responsibility, Mediator, and Observer all provide different ways to connect clients who send requests to endpoints who receive them:
- Chain of Responsibility sends a request down a list of possible users in order until one of them takes care of it.
- Chain of Responsibility makes connections between senders and receivers that only go in one way.
- Mediator cuts off direct connections between senders and receivers, causing them to talk through a mediator object instead.
- Observer lets receivers sign up for and drop out of getting requests on the fly.
- Chain of Responsibility and Composite are two words that are often used together. In this case, when a leaf component gets a request, it may send it up the chain of all the parent components until it reaches the bottom of the object tree.
- Handlers can be used as Chain of Responsibilitys in Chain of Responsibility. In this case, you can use the same context object, which is shown by a request, to do a lot of different things. But there is another way, in which the request is itself a Command object. In this case, you can do the same thing in several different situations that are all linked together.
- Chain of Responsibility and Decorator both have class designs that are a lot like each other. Both patterns use recursive composition to pass the execution through several objects. But there are a few important changes. The CoR handlers can do different things without affecting each other. They can also stop sending the request to the next person at any time. On the other hand, different Decorators can add to an object's behaviour while keeping it consistent with the base interface. Also, decorators can't stop the request from going through its normal steps.
Final Thoughts
In this article, we have discussed what is the Chain of Responsibility pattern, when to use it and what are the pros and cons of using this design pattern. We then examined some use cases for this pattern and how the Chain of Responsibility relates to other classic design patterns.
It's worth noting that the Chain of Responsibility pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.
Top comments (0)