DEV Community

Cover image for Decoupling with Chain of Responsibility Pattern in C#
Digital Craft Workshop
Digital Craft Workshop

Posted on

Decoupling with Chain of Responsibility Pattern in C#

The article cooperates with the sample project. 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.");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

Chain of Responsibility Pattern

Chain of Responsibility Pattern(ChoRP) is a pattern firstly presented in great book Design Patterns: Elements of Reusable Object-Oriented Software.

Illustration

We can achieve loose coupling in software design by practicing ChoRP. The pattern is a 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.

ChoRP comes from a family of the behavioral pattern, also like State Pattern.

How to manage states with State Design Pattern in C#?

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 Request 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; }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

OnlyAdultValidationHandler class overrides Handle method of its parent but also invokes the parent's Handle the 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 SetNext method threw which we will set another chain segment.

Chaining Handlers

First, you need to set up the chain via the SetNext method. The method returns 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.

Illustration

Drawbacks of Coupled Implementations

Why so much workaround? The basic implementation looks much more straightforward and readable than 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>();
    }
Enter fullscreen mode Exit fullscreen mode

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>();
    }
Enter fullscreen mode Exit fullscreen mode

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>();
}
Enter fullscreen mode Exit fullscreen mode

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.

Related Reading


Subscribe to Digital Craft Workshop on Substack

Top comments (0)