Chain of Responsibility
30 Jun 2021 - Alfie J.
Alternative Names
- CoR
- Chain of Command
Intent
Allows for the passing of requests along a chain of handlers. Upon receiving a request, it's at the discretion of the handler to decide whether to process the request or to pass it onto the next handler in the chain.
Main Characteristics
- Sender - Invokes the Handler
- Handler - Runs through the chain of receivers
- Receiver - Handles the given command
Usage/Example
If we have a method or function which has a large if-else-else-if conditional idiom, the CoR pattern allows the refactoring of this code smell into an object-oriented approach.
For example, if we needed to validate different criteria for a person registering for a credit card:
public class User
{
public string Name { get; set; }
public int Age { get; set; }
public string Location { get; set; }
}
public static bool CheckEligible(User user)
{
var creditCheckPassed = CreditScoreValidator.Validate(user.Name);
if (user.Name.Length <= 1)
return false;
else if (user.Location != "United Kingdom")
return false;
else if (user.Age < 18)
return false;
else if (!creditCheckPassed)
return false;
return true;
}
This could be refactored to have a particular "handler" for each individual criteria.
I.e. a unique handler for validating a persons Name, Location, Age, and Credit Rating.
The individual handler will be in charge of validating the specific condition and then calling the next handler in the chain. If one handler fails, it can either return a value or throw an exception.
To refactor this into the CoR pattern, we begin by firstly creating a contract (or interface) in which all of our handlers will implement:
public interface IHandler<T> where T : class
{
IHandler<T> SetNext(IHandler<T> next);
void Handle(T request);
}
SetNext() will allow us to append a handler to the chain of handlers.
Handle() will act on the request and then invoke the next handler in the chain.
The handler chain can be implemented as a Linked List, Collection, or simply an Array. I've opted to use a linked list where each handler points to the next handler in the chain.
Next, implementing the first (base) abstract version of a handler:
public abstract class Handler<T> : IHandler<T> where T : class
{
private IHandler<T> Next { get; set; }
public IHandler<T> SetNext(IHandler<T> next)
{
Next = next;
return Next;
}
public virtual void Handle(T request)
{
Next?.Handle(request);
}
}
Each concrete implementation of the Handler<T> will override the default Handle() method. (N.B. a concrete handler in this case is synonymous with a receiver)
This could also be implemented using a non-abstract base handler whose responsibility is to execute and visit all of the different handlers that get registered. With this approach using an abstract class, each handler can terminate the chain by disregarding the call of base.Handle().
The concrete handlers or receivers could look something like:
public class NameValidationHandler : Handler<User>
{
public override void Handle(User request)
{
if (request.Name.Length <= 1)
throw new ValidationException($"User has invalid name");
base.Handle(request);
}
}
public class AgeHandler : Handler<User>
{
public override void Handle(User request)
{
if (request.Age < 18)
throw new ValidationException($"User must be over 18");
base.Handle(request);
}
}
public class LocationValidationHandler : Handler<User>
{
public override void Handle(User request)
{
if (!string.Equals(request.Location, "United Kingdom"))
throw new ValidationException($"User must be from the UK");
base.Handle(request);
}
}
public class CreditRatingValidationHandler : Handler<User>
{
public override void Handle(User request)
{
var creditScoreCheck = CreditScoreValidator.Validate(request.Name);
if (!creditScoreCheck)
throw new ValidationException($"User must have valid credit rating");
base.Handle(request);
}
}
Note that the CreditRatingValidator is implemented naively to simulate talking to another component or service:
public class CreditScoreValidator
{
public static bool Validate(string name)
{
// Not actually how it's done - simulating using a service
return string.Equals(name, "Alfie", StringComparison.InvariantCultureIgnoreCase);
}
}
With the following chain-of-responsibility set up, we could create a simple processor or "Sender" to invoke our newly created chain:
public static bool CheckEligible(User user)
{
try
{
// Setup Chain of Handlers
var handler = new NameValidationHandler()
.SetNext(new LocationValidationHandler()
.SetNext(new AgeHandler()
.SetNext(new CreditRatingValidationHandler())));
// Begin Handling Validation
handler.Handle(user);
}
catch (ValidationException)
{
// Validation Failed
return false;
}
// Validation Passed
return true;
}
This will traverse the set of handlers and throw a custom exception should any of the steps fail, resulting in false. If all steps pass then true is returned to the caller.
The custom exception is simply:
public class ValidationException : Exception
{
public ValidationException(string message)
: base(message)
{
}
}
An example project written in .NET 5 is attached at the bottom to step-through or debug the code.
An example main method could look like:
private static void Main()
{
// Setup some test users
var users = new List<User>
{
new() { Name = "Alfie", Age = 27, Location = "United Kingdom" },
new() { Name = "Simon", Age = 35, Location = "United Kingdom" },
new() { Name = "John", Age = 23, Location = "United States" }
};
// Check eligibility for each user
foreach (var user in users)
{
var registered = EligibilityProcessor.CheckEligible(user);
Console.WriteLine($"{user.Name} registered: {registered}");
}
}
This example would return:
- Alfie registered: True
- Simon registered: False
- John registered: False
Real World Examples
.NET Core / ASP.NET Core Logging
Logging in .NET Core / ASP.NET Core makes use of the Chain of Responsibility pattern to add logger types:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
logging.AddDebug();
logging.AddEventLog();
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
The implementation of the logging handler uses an array for the receivers instead of the linked list implementation above. Rather than setting a SetNext() as above, it simply iterates over the collection and runs one (or more) of the different handlers in succession.
Benefits
- Using CoR over a set of if-else statements gives a more extensive, object-oriented, and dynamic implementation
- It's trivial to re-arrange and have control over the order in which the handlers operate
- Cleaner approach with a focus on single-responsibility (SRP), maintainability, and testability in mind.
- Easily extend a chain and add additional handlers without breaking existing code (Open/Closed principle).
- Code is decoupled. Classes that invoke operations (sender) can be decoupled from operations which perform operations (receiver). This also helps to achieve a cleaner, more extensible codebase.
Drawbacks
- Requires writing more code to achieve the same result as with the if-else idiom.
- Some requests may end up unhandled (though usually by design )
Notes
- Refactoring into an object-oriented approach like this means we can move other validation classes or objects into the respective handler classes which reduces dependencies and clutter in the processor class.
- One or many handlers can act on a given request
- Essentially an object-oriented way to express a chain of if, else-if, and else statements.
- We could re-order the validation checks so that expensive ones can go last, for e.g. if checking credit rating costs money - it would make sense to check that after validating all of the other requirements
Resources
The material is .7z, which is a 7zip compressed archive which can be unpacked with the 7zip utility.