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.

Decoupling with Chain of Responsibility Pattern in C#

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.

  1. Request
  2. Abstraction of Handler
  3. 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.

image.png

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

itixo-logo-blue.png