In this iteration, we keep all the properties but instantiate the OperationResult objects using static factories. Moreover, we hide certain properties in the sub-classes, so each result type only contains the data it needs. The OperationResult class itself only exposes the Succeeded property in this scenario.A static factory method is nothing more than a static method that creates objects. It is handy and easy to use but less flexible.
I cannot stress this enough: be careful when designing something static, or it could haunt you later; static members are not extensible and can make their consumers harder to test.
The OperationResultMessage class and the OperationResultSeverity enum remain unchanged. In the following code block, we do not consider the severity when computing the operation’s success or failure state. Instead, we create an abstract OperationResult class with two sub-classes:
- The SuccessfulOperationResult class represents successful operations.
- The FailedOperationResult class represents failed operations.
Then the next step is to force the use of the specifically designed classes by creating two static factory methods:
- The static Success method returns a SuccessfulOperationResult object.
- The static Failure returns a FailedOperationResult object.
This technique moves the responsibility of deciding whether the operation is a success from the OperationResult class to the Operation method that explicitly creates the expected result.The following code block shows the new OperationResult implementation (the static factories are highlighted):
namespace OperationResult.StaticFactoryMethod;
public abstract record class OperationResult
{
private OperationResult() { }
public abstract bool Succeeded { get; }
public static OperationResult Success(int?
value = null)
{
return new SuccessfulOperationResult { Value = value };
}
public static OperationResult Failure(params OperationResultMessage[] errors)
{
return new FailedOperationResult(errors);
}
private record class SuccessfulOperationResult : OperationResult
{
public override bool Succeeded { get; } = true;
public virtual int?
Value { get; init; }
}
private record class FailedOperationResult : OperationResult
{
public FailedOperationResult(params OperationResultMessage[] errors)
{
Messages = errors.ToImmutableList();
}
public override bool Succeeded { get; } = false;
public ImmutableList<OperationResultMessage> Messages { get; }
}
}
After analyzing the code, there are a few closely related particularities:
- The OperationResult class has a private constructor.
- Both the SuccessfulOperationResult and FailedOperationResult classes are nested inside the OperationResult class, inherit from it, and are private.
Nested classes are the only way to inherit from the OperationResult class because, like other members of the class, nested classes have access to their private members, including the constructor. Otherwise, it is impossible to inherit from OperationResult. Moreover, as private classes, they can only be accessed internally from the OperationResult class for the same reason and become inaccessible from the outside.
Since the beginning of the book, I have repeated flexibility many times; but you don’t always want flexibility. Even if most of the book is about improving flexibility, sometimes you want control over what you expose and what you allow consumers to do, whether to protect internal mechanisms (encapsulation) or for maintainability reasons.
For example, allowing consumers to change the internal state of an object can lead to unexpected behaviors. Another example would be when managing a library; the larger the public API, the more chances of introducing a breaking change. Nonetheless, over-hiding elements can be a detrimental experience for the consumers; if you need something somewhere, the chances are that someone else will too (eventually).