Refactoring to Pure Functions Part 1
Let's look at how we can refactor from a traditional OO pattern to a more streamlined slightly more functional pattern using pure functions and look at the benefits and clarity this approach might bring.
In this example we are going to look at an online restaurant checkout flow. There are lots of actions that need to be taken during checkout such as: validating the store, the store hours, the shopping cart and pricing, calculating order total and taxes, processing payment, sending the order to the point-of-sale and/or delivery provider, sending email confirmation, etc. In general, our OO implementation will look something like this:
We have one central OrderService that orchestrates several other services. We have a DeliveryCoordinator service that orchestrates which delivery provider we will use (could be based on price, round robin, availability, etc). In general, it shows a rough approximation of a lot of OO styled applications.
One thing that all these services require in a traditional OO setup is Dependency Injection:
//program.cs //DI builder.Services.AddDbContext<TylersPizzaDbContext>( options => options.UseInMemoryDatabase("tylerPizzaChain")); builder.Services.AddTransient<IOrderService, OrderService>(); builder.Services.AddTransient<IShoppingCartService, ShoppingCartService>(); builder.Services.AddTransient<IStoreService, StoreService>(); builder.Services.AddTransient<IDeliveryCoordinatorService, DeliveryCoordinatorService>(); builder.Services.AddTransient<IStoreHoursValidationService, StoreHoursValidationService>(); builder.Services.AddTransient<ICustomerService, CustomerService>(); builder.Services.AddTransient<IPaymentProcessorClient, PaymentProcessorClient>(); builder.Services.AddTransient<IPointOfSaleClient, PointOfSaleClient>(); builder.Services.AddTransient<IDoorDashClient, DoorDashClient>(); builder.Services.AddTransient<IGrubHubClient, GrubHubClient>(); builder.Services.AddTransient<IUberEatsClient, UberEatsClient>(); builder.Services.AddHttpClient<IDoorDashClient, DoorDashClient>(_ => _.BaseAddress = new Uri("http://localhost:8888")); builder.Services.AddHttpClient<IGrubHubClient, GrubHubClient>(_ => _.BaseAddress = new Uri("http://localhost:8887")); builder.Services.AddHttpClient<IUberEatsClient, UberEatsClient>(_ => _.BaseAddress = new Uri("http://localhost:8886")); builder.Services.AddHttpClient<IPointOfSaleClient, PointOfSaleClient>(_ => _.BaseAddress = new Uri("http://localhost:8885")); builder.Services.AddHttpClient<IPaymentProcessorClient, PaymentProcessorClient>(_ => _.BaseAddress = new Uri("http://localhost:8884"));
And then our various services have to inject the needed dependencies. So for instance, our OrderService that orchestrates the checkout process must inject everything it needs:
public class OrderService : IOrderService { private readonly IDeliveryCoordinatorService _deliveryCoordinatorService; private readonly IShoppingCartService _shoppingCartService; private readonly IStoreService _storeService; private readonly IPointOfSaleClient _pointOfSaleClient; private readonly IStoreHoursValidationService _storeHoursValidationService; private readonly IPaymentProcessorClient _paymentProcessorClient; private readonly ICustomerService _customerService; public OrderService( IDeliveryCoordinatorService deliveryCoordinatorService, IShoppingCartService shoppingCartService, IStoreService storeService, IPointOfSaleClient pointOfSaleClient, IStoreHoursValidationService storeHoursValidationService, IPaymentProcessorClient paymentProcessorClient, ICustomerService customerService) { _deliveryCoordinatorService = deliveryCoordinatorService; _shoppingCartService = shoppingCartService; _storeService = storeService; _pointOfSaleClient = pointOfSaleClient; _storeHoursValidationService = storeHoursValidationService; _paymentProcessorClient = paymentProcessorClient; _customerService = customerService; }
Dependency Injection can be one of those things that infects your application like the plague and you end up using it even when you don't need to. I'm not necessarily against Dependency Injection, but it is far from the only solution to the Dependency Inversion principle in the SOLID principles. Do we really need an IoC container? Not necessarily. Think about. Once upon a time IoC containers like we have today didn't exist. In fact, some languages does not even contain the same language-level concepts that C# does such as interfaces, but the inversion of control can still be accomplished.
Let's think back to what a pure function is. A pure function is one that does not have side effects and does not concern itself with things outside.
Let's stop and think about the boundaries of our application for a moment. Almost without fail the only things that must be impure in our application are things that talk to external resources, be it HTTP, Database, message buses, etc. How many external resources does your application talk to? Most other logic can all be written in a pure fashion. If we really wanted to press things, even a lot of our external resource calls could be written in a pure fashion, but let's take things slow. All of our normal business logic could be written in a pure fashion.
In this concept, we will separate our data from our business logic. One one side we have data and on the other side we have functions. The two don't need to intermingle. This way our functions can take in some data and output some data and keep everything pure.
If we take this approach, the majority of our code can be written as static
in C#. They can be extension methods, or just organize our static functions based on what they do or what they act upon. I like to think of this as we are creating a bunch of building blocks or Lego pieces. Lego let's you fit pieces together any which way you want in order to build something larger. We can treat our code the same way if we focus on creating pure functions that don't impact or effect one another. They can be pieced together based upon what parameter(s) they take in and what output they return.
With this in mind, in .NET our only impure dependencies are things that talk to external resources. Looking at our DI above in the program.cs, we primarily have the DbContext and the HttpClients. Given how the .NET HttpClient handles connection re-use for us under-the-hood, we want to keep our HttpClients. However, we don't necessarily have to constructor inject them into a bunch of classes. We have three main impure dependencies, so in this example, I find it easier to create a single dependency that handles those and I'll call it ImpureDependencies
. Note, if we had a lot more dependencies, this would not be the ideal solution. Our program.cs can be re-written as:
//program.cs //DI builder.Services.AddDbContext<TylersPizzaDbContext>( options => options.UseInMemoryDatabase("tylerPizzaChain")); builder.Services.AddTransient<ImpureDependencies>(); builder.Services.AddHttpClient("DoorDashClient", _ => _.BaseAddress = new Uri("http://localhost:8888")); builder.Services.AddHttpClient("GrubHubClient", _ => _.BaseAddress = new Uri("http://localhost:8887")); builder.Services.AddHttpClient("UberEatsClient", _ => _.BaseAddress = new Uri("http://localhost:8886")); builder.Services.AddHttpClient("PointOfSaleClient", _ => _.BaseAddress = new Uri("http://localhost:8885")); builder.Services.AddHttpClient("PaymentProcessorClient", _ => _.BaseAddress = new Uri("http://localhost:8884"));
And our ImpureDependencies class:
public class ImpureDependencies { public readonly TylersPizzaDbContext _dbContext; public readonly IConfiguration _configuration; public readonly IHttpClientFactory _httpClientFactory; public ImpureDependencies(TylersPizzaDbContext dbContext, IConfiguration configuration, IHttpClientFactory httpClientFactory) { _dbContext = dbContext; _configuration = configuration; _httpClientFactory = httpClientFactory; } }
Using named Http Clients and injecting an IHttpClientFactory let's us access any HttpClient we need by name instead of requiring to independently inject each client into each class that holds the functions to reach those services.
Next I like to use a pattern first introduced to me by Greg Young in his 8 Lines of Code presentation. He mentions a practice where you provide a single place where developers can see all the workflows/pipelines that your application performs. With that in mind, I organize my code similar to:
├── Controllers
├── Database
├── Models
├── Pipelines
│ ├── BuildingBlocks
│ ├── ImpureDependences.cs
│ ├── ProcessOrderPipeline.cs
Obviously the Controllers is where all the api controllers are stored. Since this is ASP.NET Core, the Database directory contains the Entity Framework setup. The /Pipelines/BuildingBlocks directory contains all the sub-directories to organize my pure functions according to what data they act upon. The ImpureDependencies.cs is shown above. And the ProcessOrderPipeline is the pipeline used for customer checkout. Terminology can vary, but the general premise is there that all pipelines or workflows called by the controllers exist under the Pipelines directory.
Because everything is pure, it can also be static. So our ProcessOrderPipline.cs contains a function that executes the workflow and that function takes in all dependencies required:
using TylersPizzaChain.Exceptions; using TylersPizzaChain.Models; using TylersPizzaChain.Pipelines.BuildingBlocks; using TylersPizzaChain.Pipelines.BuildingBlocks.Clients; using TylersPizzaChain.Pipelines.BuildingBlocks.DatabaseQueries; namespace TylersPizzaChain.Pipelines { public static class ProcessOrderPipeline { public static async Task<OrderConfirmation> Execute(ImpureDependencies impure, OrderDetails input) { //throw new NotImplementedException(); //look up store var store = await StoreQueries.GetStoreById(impure._dbContext, input.StoreId); if (store == null) { throw new OrderProcessingException("Store not found"); } var storeHours = await StoreQueries.GetStoreHours(impure._dbContext, input.StoreId); if (storeHours == null) { throw new OrderProcessingException("Store Hours not found"); } //validate store hours with order time var isOrderTimeValid = StoreHoursValidation.ValidateOrderTime(impure._configuration, input, storeHours, store.TimeZone); if (!isOrderTimeValid) { throw new OrderProcessingException("Invalid store time"); } //look up shopping cart var shoppingCart = await ShoppingCartQueries.GetShoppingCartById(impure._dbContext, input.ShoppingCartId); if (shoppingCart == null) { throw new OrderProcessingException("Shopping Cart not found"); } //validate shopping cart var isSHoppingCardValid = await ShoppingCartQueries.ValidateShoppingCartToStore(impure._dbContext, input.ShoppingCartId, store); if (!isSHoppingCardValid) { throw new OrderProcessingException("Shopping Cart is not valid"); }//TODO: return invalid items //calculate order total var totalTax = Math.Round(shoppingCart.MenuItems?.Aggregate(0M, (acc, val) => acc + (val.Price * val.TaxRate)) ?? 0, 2); var subtotal = shoppingCart.MenuItems?.Aggregate(0M, (acc, val) => acc + val.Price) ?? 0; var orderTotal = subtotal + totalTax; //TODO: add delivery fee if applicable //lookup saved payment var savedPayment = await CustomerQueries.GetCustomerSavedPayment(impure._dbContext, input.CustomerId, input.PaymentId); if (savedPayment == null) { throw new OrderProcessingException("Saved Payment not found"); } //process payment var paymentResponse = await PaymentProcessorClient.ProcessPayment(impure._httpClientFactory, savedPayment.VendorPaymentId, orderTotal); if (!paymentResponse.IsSuccess) { throw new OrderProcessingException("Payment failed"); }//TODO: parse why payment failed to notify customer //send to point of sale //send to point of sale var posResponse = await PointOfSaleClient.SendToPointOfSale(impure._httpClientFactory, input, shoppingCart); if (!posResponse.IsSuccess) { throw new OrderProcessingException("Error sending to point of sale"); }//perform cleanup on order //if delivery, send to delivery service if (input.OrderType == OrderType.Delivery) { //send to a delivery vendor var deliveryResponse = await DeliveryCoordinator.SendOrderToDeliveryProvider(impure._httpClientFactory, input, shoppingCart, store, orderTotal); if (!deliveryResponse.IsSuccess) { throw new OrderProcessingException("Could not notify deliver provider"); } } //send back confirmation return new OrderConfirmation() { ConfirmationNumber = "1234", Message = "Order Processed.", Processed = true, ShoppingCardId = input.ShoppingCartId }; } } }
I can hear the question already, without interfaces how do I swap out implementations? I would ask in return, what are you realistically swapping out? All of our impure dependencies are injected, so those can be swapped out for testing or replacement. Everything else is pure. We can easily write functions that intelligently choose what the next function to be called should be.
Our DeliveryCoordinator, which would choose a provider based on some criteria (availability, round robin, etc) can still coordinate (think strategy pattern) those actions without needing constructor injection:
public static class DeliveryCoordinator { public static async Task<DeliveryResponse> SendOrderToDeliveryProvider(IHttpClientFactory httpClientFactory, OrderDetails orderDetails, ShoppingCart shoppingCart, Store store, Decimal orderTotal) { var r = new Random().Next(1, 3); var response = r switch { 1 => await DoorDashClient.SendOrder(httpClientFactory, orderDetails, shoppingCart, store, orderTotal), 2 => await GrubHubClient.SendOrder(httpClientFactory, orderDetails, shoppingCart, store, orderTotal), 3 => await UberEatsClient.SendOrder(httpClientFactory, orderDetails, shoppingCart, store, orderTotal), _ => throw new NotImplementedException() }; return response; } }
Even though in this example all the delivery providers return the same DeliveryResponse model, we could just as easily create a common interface such as IDeliveryResponse and let each provider return a customized implementatin of that response. In fact, I think in this style of coding, interfaces are more powerful on our Models than on our classes. Functions can be polymorphic if they take in data that meets an interface instead of a specific concrete class.
While we have removed all the "services" that each had their own interface and dependency injection, the overall architecture diagram at the top hasn't really changed all that much except that now everything is pure and we have very few true dependencies. We have achieved the same functionality but with far fewer lines of code (fewer DI setups in Program.cs, little to zero constructor injection). We are left with the code which really matters to us and not boilerplate to handle DI. We still have the ability to pass in our true dependencies from the top. For many applications this pattern can get you a long way. We just have to learn to think simpler. We don't need class upon class with constructor injection. We don't need to put everything into IOC containers.
In the next post we will look at refactoring pure functions to create better error handling.
If you want to explore the examples in this article see the following branches: