I sometimes end up with services encapsulating the responsibility of doing some sort of business process for which there are several possible outputs. Typically one of those output is success and the others represent the possible failures of the process itself.
To fix the idea consider the following interfaces and classes:
interface IOperationResult
{
}
class Success : IOperationResult
{
public int Result { get; }
public Success(int result) => Result = result;
}
class ApiFailure : IOperationResult
{
public HttpStatusCode StatusCode { get; }
public ApiFailure(HttpStatusCode statusCode) => StatusCode = statusCode;
}
class ValidationFailure : IOperationResult
{
public ReadOnlyCollection<string> Errors { get; }
public ValidationFailure(IEnumerable<string> errors)
{
if (errors == null)
throw new ArgumentNullException(nameof(errors));
this.Errors = new List<string>(errors).AsReadOnly();
}
}
interface IService
{
IOperationResult DoWork(string someFancyParam);
}
The classes consuming the IService
abstraction are required to process the returned IOperationResult
instance. The straightforward way to do so is writing a plain old switch
statement and decide what to do in each case:
switch (result)
{
case Success success:
Console.WriteLine($"Success with result {success.Result}");
break;
case ApiFailure apiFailure:
Console.WriteLine($"Api failure with status code {apiFailure.StatusCode}");
break;
case ValidationFailure validationFailure:
Console.WriteLine(
$"Validation failure with the following errors: {string.Join(", ", validationFailure.Errors)}"
);
break;
default:
throw new NotSupportedException($"Unknown type of operation result {result.GetType().Name}");
}
Writing this type of code in different points of the codebase quickly generates a mess, because this basically violates the open closed principle.
Each time the implementation of IService
gets modified by introducing a new implementation of IOperationResult
there are several switch statements that must be modified too. The developer implementing the new feature must be aware of their existence, unless there are well written tests which can automatically detect the missing modifications in the points where the code switches over IOperationResult
instances.
Maybe the switch statement can be avoided at all.
This is easy to do when IService
is used for one specific purpose. As an example, when I write ASP.NET core MVC controllers in order to keep action methods simple and lean I inject a service in the controller and delegate to it all the processing logic. This way the action method only cares about handling the HTTP request, validating the parameters and returning an HTTP response to the caller. In this scenario the switch
statement can be avoided from the beginning by using polymorphism. The trick is modifying IOperationResult
this way:
interface IOperationResult
{
IActionResult ToActionResult();
}
The action method simply calls ToActionResult
on the IOperationResult
instance and returns the result.
In some cases the IService
abstraction must be used by different callers and we need to let them the freedom to decide what to do with the operation result.
One possible solution is defining one higher order function, lets call it processor for simplicity, having the responsibility of processing a given instance of IOperationResult
. It's something like this:
static class Processors
{
static T Process<T>(
IOperationResult operationResult,
Func<Success, T> successProcessor,
Func<ApiFailure, T> apiFailureProcessor,
Func<ValidationFailure, T> validationFailureProcessor) =>
operationResult switch
{
Success success => successProcessor(success),
ApiFailure apiFailure => apiFailureProcessor(apiFailure),
ValidationFailure validationFailure => validationFailureProcessor(validationFailure),
_ => throw new ArgumentException($"Unknown type of operation result: {operationResult.GetType().Name}")
};
}
The advantages here are the following:
- there is only one point where the
switch
statement is done - each time a new implementation of
IOperationResult
is defined there is only one point that needs to be modified. Doing so the signature of theProcess
function gets modified too. - the modification done at the previous point produces several compile time errors where the
Process
function gets called. This errors must be fixed, but we can trust the compiler being able to find all the points to be modified
A more object oriented alternative is modifying the definition of IOperationResult
by adding one method per each intended usage of the operation result, so that the switch
statement can be avoided once more and the only thing to do is actually writing a new implementation of the interface.
This is an example in the hypothesis that there are two different consumers of IService
:
interface IOperationResult
{
string ToEmailMessage(); // used by the email sender service
ICommand ToCommand(); // used by the command sender service
}
Any thoughts ? Are there other or better alternatives ?
MediatR
library instead - lostechies.com/jimmybogard/2014/09/09/… and jimmybogard.com/…