Focus on the happy path - stop using exceptions and if statements to control the flow in your code! With this approach your code can be much shorter, making it easier to understand and maintain.
Tricks that allows to achieve this are:
- Stop using exceptions to represent errors - return
Result<T>from all methods. - Implement flow as a chain of methods.
- Stop using
nullto represent the lack of a value.
- Type that represents the result of an operation -
Result<T>. The variable of that type can store either data of typeTor anError. Error- the base class for errors returned from methods. It support nesting/aggregate errors.- Set of
Thenmethods, which allows to invoke next operation onResult<T>. Result.None- represents the lack of a value returned from a method.- Set of
Act<T>methods, which allows to invoke an action if the result of the previous operation isT. - Set of
Swap<T>methods, which allows to swap the result type if the result of the previous operation isT. asyncsupport - allThen,Act, andSwapmethods haveasyncoverrides.
- Methods like:
a.
return AbstractResultFactoryBeanImpl.CreateResultOf<string>(...)b.return Result.Of<string>(...)c.return new Result<string>(...) - No
result.IsError- the goal of this library is to prevent people from usingifstatements, so such a property would make no sense. - No
result.IsValidorresult.IsSuccess- these are even worse thanIsError, because errors needs special handling, not valid results! - No parameterless constructors available for users to prevent from creating uninitialized results.
Let's take a method:
public static string ReverseString(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
throw new ArgumentException(nameof(input));
}
return new string(input.Reverse().ToArray());
}With HappyPath we only need to replace throwing exceptions with returning an Error:
public static Result<string> ReverseString(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
return new Error(ErrorMessage);
}
return new string(input.Reverse().ToArray());
}No need to change valid cases or wrapping anything in Result<T>.
We can convert this:
public string ProcessAnimal(Guid id)
{
Animal animal = null!;
try
{
animal = GetRandomPetFromOracle();
}
catch (ThisParrotIsDeadException pe)
{
errorMessage = pe.Message;
animal = new Cat("Kitty");
}
catch (Exception e)
{
errorMessage = e.Message;
}
if (animal is Dog d)
{
isPies = true;
animal = new Cat(d.Name);
}
var result = TestService.ToUpperCase($"{animal.GetType().Name} is {animal.Name}");
return result;
}to more concise and "fluent" version:
public Result<string> ProcessAnimal(Guid id)
{
return GetRandomPetFromOracle()
.Act<Error>(e => errorMessage = e.Message)
.Act<Dog>(d => isDog = true)
.Swap<ThisParrotIsDeadError>(e => new Cat("Kitty"))
.Swap<Dog>(d => new Cat(d.Name))
.Then(c => TestService.ToUpperCase($"{c.GetType().Name} is {c.Name}"));
}Of course, the GetPetFromOracle method must return Result<Animal> and use errors instead of exceptions, but this is an easy migration as shown before.