Decoupling with Chain of Responsibility Pattern in C#
The Chain of Responsibility Pattern allows us to easily separate dependent parts to make code more extensible and testable.
The article cooperates with the sample project. A walkthrough with the downloaded project is not required, but I recommend it for better understanding.
The sample project consists of two different approaches to validating the user’s data within a registration. Two processors simulate the user’s registration process.
BasicUserRegistrationProcessor.cs
follows the simple path of if statements.
public class BasicUserRegistrationProcessor
{
public void Register(User user)
{
if (string.IsNullOrWhiteSpace(user.Username))
{
throw new Exception("Username is required.");
}
if (string.IsNullOrWhiteSpace(user.Password))
{
throw new Exception("Password is required.");
}
if (user.BirthDate.Year > DateTime.Now.Year - 18)
{
throw new Exception("Age under 18 is not allowed.");
}
}
}
ChainPatternRegistrationProcessor.cs
is taking advantage of the Chain of Responsibility Pattern. This implementation applies the same set of validations.
public class ChainPatternRegistrationProcessor
{
public void Register(User user)
{
var handler = new UsernameRequiredValidationHandler();
handler.SetNext(new PasswordRequiredValidationHandler())
.SetNext(new OnlyAdultValidationHandler());
handler.Handle(user);
}
}
Chain of Responsibility Pattern
Chain of Responsibility Pattern(ChoRP) is a pattern firstly presented in the great book Design Patterns: Elements of Reusable Object-Oriented Software.
We can achieve loose coupling in software design by practicing ChoRP. The pattern is very powerful yet pretty simple to implement in our applications. It allows us to easily decouple parts of code to make it more readable, testable, extensible, and maintainable.
Components of ChoRP
ChoRP consists of three components.
- Request
- Abstraction of Handler
- Concrete Handlers
One or more Concrete Handlers will take care of Requests in the chained process. Mostly, the Request is some kind of contract or Data Transfer Object of the handling event. In the sample project, it is a User class
.
public class User
{
public string Username { get; set; }
public string Password { get; set; }
public DateTime BirthDate { get; set; }
}
I am usually using the interface
and the abstract class
together as the abstraction for the Concrete Handlers. It contains two methods; Handle
and SetNext
.
public abstract class Handler<T> : IHandler<T> where T : class
{
private IHandler<T> Next { get; set; }
public virtual void Handle(T request)
{
Next?.Handle(request);
}
public IHandler<T> SetNext(IHandler<T> next)
{
Next = next;
return Next;
}
}
public interface IHandler<T> where T : class
{
IHandler<T> SetNext(IHandler<T> next);
void Handle(T request);
}
T
is a generic type of class
which represents the Request. Method SetNext
sets a private property Next
, which is subsequently invoked in the virtual method Handle(T request)
.
Concrete Handler inherits from the abstract class Handler
and implements an interface IHandler
.
public class OnlyAdultValidationHandler : Handler<User>, IHandler<User>
{
public override void Handle(User user)
{
if (user.BirthDate.Year > DateTime.Now.Year - 18)
{
throw new Exception("Age under 18 is not allowed.");
}
base.Handle(user);
}
}
OnlyAdultValidationHandler
class overrides the Handle
method of its parent but also invokes the parent's Handle
method at the end of the method’s body. This is a crucial factor for chaining Concrete Handlers. Also, by inheriting from the Handler class
, the OnlyAdultValidationHandler
gains theSetNext
method through which we will set another chain segment.
##Chaining Handlers
First, you need to set up the chain via the SetNext
method. The method returns the IHandler
type so you can set Handlers in a row.
SetNext(new Handler()).SetNext(new Handler())
Once we finish the chain definition, we can continue by invoking Handler.Handle(request)
which will call base.Handle(user)
therefore Next?.Handle(request)
ensures that the chain reaction is triggered.
Drawbacks of Coupled Implementations
Why so much workaround? The basic implementation looks much more straightforward and readable than the ChoRP implementation, right?
Well, it is not clear at first look, but those validations are tightly coupled. Password Validation is dependent on the result of Username Validation. Only Adult Age Validation is dependent on the result of previous validations.
It is even more evident if you write unit tests that are going to test different scenarios of user registration. You are not able to test Password Validation without introducing value into the user’s Username property
.
public class BasicUserRegistrationTests
{
[Fact]
public void When_Username_Is_Empty_Then_Exception
_Should_Be_Thrown()
{
//Arrange
var user = new User();
//Act
Action act = () => new BasicUserRegistrationProcessor()
.Register(user);
//Assert
act.Should().Throw<Exception>();
}
[Fact]
public void When_Password_Is_Empty_Then_Exception
_Should_Be_Thrown()
{
//Arrange
var user = new User
{
Username = "Daniel Rusnok"
};
//Act
Action act = () => new BasicUserRegistrationProcessor()
.Register(user);
//Assert
act.Should().Throw<Exception>();
}
}
But once you apply a ChoRP, then you are not only able to test Password Validation result, but also you are able to test both possible results of Password Validation which are Throw<Exception>
when the password property is empty and NotThrow<Exception>
when the password property is filled.
public class ChainPatternRegistrationTests
{
[Fact]
public void When_Password_Is_Empty_Then_Exception
_Should_Be_Thrown()
{
//Arrange
var user = new User{Password = string.Empty};
//Act
Action act = () => new PasswordRequiredValidationHandler()
.Handle(user);
//Assert
act.Should().Throw<Exception>();
}
[Fact]
public void When_Password_Is_Filled_Then_Exception
_Should_NOT_Be_Thrown()
{
//Arrange
var user = new User{Password = Guid.NewGuid().ToString()};
//Act
Action act = () => new PasswordRequiredValidationHandler()
.Handle(user);
//Assert
act.Should().NotThrow<Exception>();
}
}
With BasicUserRegistrationProcessor
we are only able to test if Password Validation NotThrow<Exception>
when the whole user object is valid. And this is the biggest demonstration of how basic registration implementation is coupled.
[Fact]
public void When_All_Properties_Are_valid_Then_Exception_Should_NOT_Be_Thrown()
{
//Arrange
var user = new User
{
Username = "Daniel Rusnok",
Password = Guid.NewGuid().ToString(),
BirthDate = DateTime.Now.AddYears(-20)
};
//Act
Action act = () => new BasicUserRegistrationProcessor()
.Register(user);
//Assert
act.Should().NotThrow<Exception>();
}
Summary
The power of decoupling is not only in its independent testability but also in its reusability. Such a Handler does not always have to accept a concrete type as a parameter. It can take an abstraction of the specific type like an interface.
Multiple types of Requests can implement such an interface. In such situations, the Handler becomes useable for various processes, and you are not repeating yourself. So ChoRP also supports the DRY