The Operation Result Pattern
The idea of a binary decision can be found in many places in our field : **true **or false, 1 or 0, tabs or spaces - it's either one or the other, though in the last example there is only real choice...
We can model a simple binary decision in our code by using a boolean type, but what do we do when our operation has more than two possible outcomes? In this post, I will outline a common pattern for accomplishing this, and introduce the Operation Result pattern.
In the interest of making this a simple example to follow, we will begin by considering the following piece of code:
Looking at the
Withdraw method, we can see that it first checks that there are
sufficient funds to withdraw the desired amount, and if so it will subtract the
amount from the balance and return
true. If there are insufficient funds in
the account it will simply return
false. Think about what is wrong with this.
A returned result of
false in this situation could mean several things.
Perhaps the account simply has insufficient funds, but what if the account does
have funds but has been frozen for suspicious activity? Or perhaps this
transaction would surpass the daily withdrawal limit? Lets implement these
situations to illustrate the issue:
A very common pattern used here would be to throw exceptions for the failure paths, creating custom exception types to map to real world business cases:
As far as handling the failure paths is concerned, this is a massive improvement and the code is incredibly simple to follow thanks to our custom exception types. If you've ever read anything about Domain Driven Design, you'll know that you should write your code using the ubiquitous language of the domain, and each potential outcome from the method should represent a unique business scenario.
However, me being the pedant that I am, I now see that a boolean isn't really a
sufficient return type anymore: the method either returns
true or throws an
exception. One change I could make here is to change the return value to a
decimal and return the new balance, but that doesn't feel very intuitive either.
This lone decimal value could represent any value, and offers no context to what
it represents short of looking into the method.
Introducing the Operation Result pattern
The Operation Result pattern, Result Object pattern, or even just Result pattern
is a design pattern where you return the outcome of an operation as an object
containing the result and any accompanying values. If you've ever written code
in F#, you'll know that this is built into the language and is also used by a
lot of packages that you have probably used such as
By returning even our failure results as result objects, we are able to avoid
nesting our code in
try..catch..finally statements and just work with the
outcome of a method. In it's simplest form, we can just create a record type and
refactor our code to use it like this:
This is an improvement on the initial design of the method, and if we need more information on why the operation failed, we have a reason built right into the object. For fairly simple use cases this is perfectly sufficient, but in our scenario I see two issues with this code in it's current state:
- Lets say that we wanted to perform an action specifically if the withdrawal
failed due to the account being frozen: we would need to check the
Reasonvalue against a hard-coded string, which is a terrible idea.
Reasonproperty on a successful result object is completely unused. Any type with properties only used in a particular use case is not a well designed one, and in a way a violation of the Interface Segregation principle, even though we aren't using an interface here.
By creating a separate record type for success and failure results, we are able to keep the failure specific properties away from successful results:
You are also able to utilise C# pattern matching capabilities on these objects, which can be extremely powerful:
This fixes one of the issues I saw in the code, but this approach would still require programming against string values if we wanted to program against specific failure outcomes.
Just as I mentioned earlier in the post when we were talking about custom exceptions, we should treat every failure scenario as a unique business outcome, meaning we should create a result object for every possible outcome scenario. This allows us to use pattern matching even further and also add additional properties for each scenario. Let's say we need the following information in each of the following scenarios:
- If the withdrawal is successful, return the new account balance
- If the withdrawal fails due to insufficient funds, return the current account balance
- If the withdrawal fails due to the withdrawal limit being exceeded, return the maximum amount that the account holder can withdraw for the rest of the day
- If the account is frozen, return a more detailed reason as to why the account was frozen.
Modelling that in our code, we will come up with the following types:
And then we can use them in our method like this:
Now we have a more type-safe way of programming against each of our result scenarios:
What I have shown here are 3 levels to the Operation Result pattern, depending the complexity of your use case. Uncle Bob once said that clean code reads like well-written prose, and by mapping each of our outcomes to Plain English descriptions of real world scenarios, we are able to follow along our code without having to translate what a boolean means in each scenario.
I have been using this pattern recently in a massive refactor job I have undertaken and it has made the code infinitely more comprehensible. Record Types in C# have been a total game changer for this approach, but you are perfectly fine just using regular classes if you aren't able to use record types in your environment or language of choice.
This pattern isn't really documented on any large sites and I hope that this article can shine a light on this lesser known design pattern. Remember that the best solution is always the simplest one that you can get away with, and readability is more important than cleverness.