Published on

A Tale of Two Responsibilities

Authors

TLDR

Typically, once we receive arguments from an external system, we validate them. Oftentimes, after the validation is done, developers continue using the same type of argument received from the outside. However, it is very likely that some of the properties better represent the domain if we apply transformations to them. That is, the way we gather certain data from external sources, might be slightly different from the data that we need to use to compute something, let's say, an interest calculation. For example, we might be forced to accept string properties that represent numbers, Nullable properties that represent attributes that are actually required, etc.

Proposed approach is: once validation is done, map any external argument to some other type that represents a Valid argument. In addition to handling transformations, a Valid argument is now our key to be able to call the components that use the data safely. That is, if there is a Valid argument instance, we know for sure that validation already happened.

In other words, the proposed approach uses the type system to prevent invalid operations such as calling a “worker” object with arguments that have not been validated yet.

The Problem

Time ago, my team was working on a calculation engine library. All components in the engine had two stages:

  • Validation stage: to verify the calculation was feasible given certain Arguments
  • Calculation stage: it would grab the Arguments and use them to compute a number.

Also, the library could be consumed from other applications. We couldn’t make any assumptions at all about how the API was going to be called. For example, pretty much all arguments accepted by our components contained Nullable values. Also, we always wanted to give the most accurate error message. For example, if a null value was provided for a field called Rate, we wanted our validation message to be “Rate is null” instead of a more generic “Rate is null or zero”.

Let’s understand some of the issues we faced using an example component that calculates the Interest of a loan.

A developer familiar with the Single Responsibility Principle would likely split the mentioned stages into 2 different classes. Let’s say: Interest and InterestValidator. Before analyzing those, let’s see how we gather data from the outside by inspecting the InterestArgs type.

public record InterestArgs
{
    public decimal? Principal { get; init; }
    public decimal? Rate { get; init; }
    public int? PeriodInYears { get; init; }
}

As you can see, all fields are Nullable types. As mentioned, we can use that fact to tell apart between library calls sending zero vs null for each of those fields, thus, returning more accurate error messages.

Not a solution

One initial approach to our Interest class is shown below:

public class Interest
{
    private InterestValidator Validator { get; init; }

    public Interest(InterestValidator validator)
    {
        this.Validator = validator;
    }

    public decimal Calculate(InterestArgs args)
    {
        var validationResult = this.Validator.Validate(args);
        if (validationResult.Errors.Count > 0)
        {
            throw new ValidationException(validationResult);
        }
        
        return (args.Principal ?? 0) * (args.Rate ?? 0) * (args.PeriodInYears ?? 0) / 100;
    }
}

...and its InterestValidator companion class:

public class InterestValidator
{
    public InterestValidatorResult Validate(InterestArgs rawArgs) 
    {
        var errors = GetErrors(rawArgs);
        if (errors.Any())
        {
            return new InterestValidatorResult(errors);
        }                   
        
        return new InterestValidatorResult(Enumerable.Empty<string>());
    }

    private static IEnumerable<string> GetErrors(InterestArgs rawArgs)
    {
        if (rawArgs.Principal == null)
        {
            yield return "Principal cannot be null";
        }
        else if (rawArgs.Principal <= 0)
        {
            yield return "Principal must be greater than zero.";
        }

        if (rawArgs.Rate == null)
        {
            yield return "Rate cannot be null";
        }
        else if (rawArgs.Rate <= 0)
        {
            yield return "Rate must be greater than zero.";
        }

        if (rawArgs.PeriodInYears == null)
        {
            yield return "PeriodInYears cannot be null";
        }
        else if (rawArgs.PeriodInYears <= 0)
        {
            yield return "PeriodInYears must be greater than zero.";
        }
    }
}

This works fine but it suffers from -at least- a couple of issues:

  • It makes too many assumptions about the input data: All nullable fields that are numeric are being defaulted to 0 which means that another developer inspecting the code may mistakenly think that the “defaulting to zero” is a business rule when it is actually a consequence of the mismatch between “how we collect the argument data” and “the data we need to compute stuff safely”.

    • Of course, she could have taken a look at the Validator class and “learn” that for example, Principal cannot be null. However, that adds unnecessary navigation just to understand something that could have been clear from the scratch. In more complex scenarios, it could be virtually impossible.
  • It is not friendly for API clients that want to know whether a set of arguments is correct before making the call to Calculate. They would need to either add a try-catch for the ValidationException making their code less elegant or make a call to Validator.Validate() themselves. The latter approach might seem cleaner but now the validation logic would be executed twice, one within their code and one within our Interest class.

How can we change our design to get rid of the mismatch and still have a dev-friendly API ?

Types to the rescue

At the time we were developing this calculation engine, I was reading an excellent F# book by Isaac Abraham. There is a chapter that talks about “Accepting data from external systems” and how we can protect our business logic from unsafe data that comes from external sources.

The answer is fairly simple: just use 2 different types for your args:

  • one for the unsafe/yet-to-be-validated data
  • one for the data once has been validated.

That is, if your application accepts an object called InterestArgs, pass it through a Validator method/class and have that validation logic return a new object of type ValidInterestArgs in case all data is valid. Otherwise, return a collection of errors.

Now all the calculation methods can safely assume their working data is valid because they now accept a ValidInterestArgs instead of InterestArgs.

alt text

We were using C# at that time (not F#) but we thought that idea should work regardless of the language (as long as it is strongly typed).

The solution

Let's introduce a new type ValidInterestArgs as shown below. I'll copy InterestArgs right above it for comparison:

    public record InterestArgs
    {
        public decimal? Principal { get; init; }
        public decimal? Rate { get; init; }
        public int? PeriodInYears { get; init; }
    }
    public record ValidInterestArgs
    {      
        public decimal Principal { get; init; }
        public decimal Rate { get; init; }
        public int PeriodInYears { get; init; }
    }

Not surprisingly, ValidInterestArgs is very similar to InterestArgs except for the fact that all its properties are non-nullable. That is, we have also applied transformations to simplify the usage.

Other types of transformations could be, for example, dropping properties that are required for the Validation stage but not for the Calculation stage.

Our new InterestValidator looks like this:

public class InterestValidator
{
    public InterestValidatorResult Validate(InterestArgs rawArgs) 
    {
        var errors = GetErrors(rawArgs);
        if (errors.Any())
        {
            return new InterestValidatorResult(errors);
        }          

        var validArgs = new ValidInterestArgs()
        {
            Principal = rawArgs.Principal!.Value,
            Rate = rawArgs.Rate!.Value,
            PeriodInYears = rawArgs.PeriodInYears!.Value,
        };
        
        return new InterestValidatorResult(validArgs);
    }

    private static IEnumerable<string> GetErrors(InterestArgs rawArgs)
    {
        if (rawArgs.Principal == null)
        {
            yield return "Principal cannot be null";
        }
        else if (rawArgs.Principal <= 0)
        {
            yield return "Principal must be greater than zero.";
        }

        if (rawArgs.Rate == null)
        {
            yield return "Rate cannot be null";
        }
        else if (rawArgs.Rate <= 0)
        {
            yield return "Rate must be greater than zero.";
        }

        if (rawArgs.PeriodInYears == null)
        {
            yield return "PeriodInYears cannot be null";
        }
        else if (rawArgs.PeriodInYears <= 0)
        {
            yield return "PeriodInYears must be greater than zero.";
        }
    }
}

We are using the Nullable reference types feature available since C# 8. In our case, we need to add ! to indicate that it is safe to access the value of our Nullable fields. We know it is safe because they have been validated in GetErrors method but the compiler does not.

 var validArgs = new ValidInterestArgs()
{
    Principal = rawArgs.Principal!.Value,
    Rate = rawArgs.Rate!.Value,
    PeriodInYears = rawArgs.PeriodInYears!.Value,
};

The return object from InterestValidator.Validate() is a InterestValidatorResult which is just a simple type to return either a ValidInterestArgs or a list of error messages.

public record InterestValidatorResult
{
    public ValidInterestArgs? ValidArgs { get; }
    public List<string> Errors { get; }

    public InterestValidatorResult(ValidInterestArgs validArgs)
    {
        this.ValidArgs = validArgs;
        this.Errors = Enumerable.Empty<string>().ToList();
    }

    public InterestValidatorResult(IEnumerable<string> errors)
    {
        this.ValidArgs = null;
        this.Errors = errors.ToList();
    }
} 

Unfortunately, C# does not include any built-in Either or Validation<FAIL,SUCCESS> functional-programming-like classes. We couldn't afford adding a third party library such as language-ext at that time. So we moved forward with above custom type that "simulates" an Either by using different constructors for each case.

And our Interest class is now very straightforward:

public class Interest
{
    public decimal Calculate(ValidInterestArgs args)
    {
        return args.Principal * args.Rate * args.PeriodInYears / 100;
    }
}

Since Interest accepts a ValidInterestArgs, it is impossible to skip validations by mistake. The type system takes care of controlling only valid workflow. Developers are forced to call InterestValidator which is the only class with the responsibility of creating instances of type ValidInterestArgs.

Conclusion

Using the right types instead of trying to "reuse" existing types when they have been created with a different goal in mind can simplify our code and prevent subtle issues.

You can get take a look at the complete example here: https://github.com/fabricior/validators-example.