Refactoring with Composition
This is the Part 2 of Refactoring To Pure Functions.
Prior we did a little experiment of reducing our Dependency Injection and relying more on static functions. This allowed us to organize our code and creating discrete "pipelines" of functionality. We then had one place to look and see all the supported pipelines our application supported. Each pipeline built using our collection of "building blocks" of pure or static functions.
Let's take a look at how we can clean up some of our clutter in our pipeline. What if we had a function that could take a value and pass it to another function? There are a few variations of this function, but let's take a look at one:
public static TResult Then<T, TResult>(this T value, Func<T, TResult> function) => function(value);
This is an extension method that will operate on any value and pass it to the next function.
"tyler jennings" .Then(_ => _.ToUpper()) .Then(_ => _.ToArray()) .Then(_ => String.Join(" ", _)); //"T Y L E R J E N N I N G S"
If we take a look at the start of our ProcessOrderPipeline
from Part 1:
//look up store var store = await StoreQueries.GetStoreById(impure._dbContext, input.StoreId); if (store == null) { throw new OrderProcessingException("Store not found"); }
We make a call to the database to get the store and then we check if it's null and throw an exception. If we are going to compose functions together, it also helps to have a standard type that can be passed from function to function. With that in mind, we need to get the store, check if null, and if not null, merge the value into our shared type. It could also be helpful to have an extension that can handle our null check for us.
public static T GuardAgainstNull<T>(this T value, Func<Exception> newException) { if (value == null) throw newException(); else return value; }
To merge this all together, our code to get the store and check for null can now become:
(await StoreQueries.GetStoreById(impure._dbContext, input.StoreId)) .GuardAgainstNull(() => new OrderProcessingException("Store not found")) .Then(store => new OrderForProcessing().Merge(store))
Now some of our previous pipeline contained several async/await method calls. How does that play with our Then
function? It doesn't play too well. We need an async version:
public static TResult ThenAsync<T, TResult>(this T value, Func<T, Task<TResult>> function) { var taskResult = function(value); taskResult.Wait(); return taskResult.Result; }
This is an extension method on a value that takes an argument of a Func that takes our value and returns a Task<TResult>
. However, to continue our function chaining, we want to wait and unwrap the value from the Task and return the value instead.
To combine all of this together, our pipeline can now turn into a readable chain of functions:
public static async Task<OrderConfirmation> Execute(ImpureDependencies impure, OrderDetails input) => (await StoreQueries.GetStoreById(impure._dbContext, input.StoreId)) .GuardAgainstNull(() => new OrderProcessingException("Store not found")) .Then(store => new OrderForProcessing().Merge(store)) .ThenAsync(async ofp => await GetStoreHours(ofp, impure, input)) .Then(ofp => ValidateOrderTime(ofp, impure, input)) .ThenAsync(async ofp => await RetrieveShoppingCart(ofp, impure, input)) .ThenAsync(async ofp => await ValidateShoppingCartToStore(ofp, impure, input)) .ThenAsync(async ofp => await GetCustomerSavedPayment(ofp, impure, input)) .ThenAsync(async ofp => await ProcessPayment(ofp, impure)) .ThenAsync(async ofp => await SendToPOS(ofp, impure, input)) .ThenAsync(async ofp => await IfDeliverySendToProvider(ofp, impure, input));
The more we start to treat our code as building blocks, the more we can see how things can fit together. This has been a fun experiment in writing the same application code several different ways. It's also fun to see how extension methods can help create our own fluent language for how we want things to work. Can this be improved? Oh yes! But that is also, in my opinion, the joy of programming. In a future post we will look at extending this concept with Result types.
A more complete example can be seen here.