I've spent a bit of time outside of my day-to-day work playing around with functional languages, F# being my favourite so far. In particular the book Domain Driven Design Made Functional by Scott Wlaschin is well worth a read, whether you are into functional programming or not. Scott is also the author of the excellent F# for fun and profit website.
I decided I wanted to use some of the benefits of functional programming in a C# project. C# 9 has seen some great additions to the language, including pattern matching and record types. These along with some patterns borrowed from functional programming play really nicely when developing Domain Models. Many of the ideas I'm going to talk about here have been inspired by Scott, but also Greg Young.
The full source code can be found here.
Rock 🪨, Paper 📃, Scissors ✂️
I wanted a simple domain to test out these concepts, so I went with the game Rock Paper Scissors. In this game, we only have two commands:
- Create Game: A new game instance is started with a UUID
- Make Move: The players take it in turns to make a move, each player has one move
Once the second move has been made, an end game state is reached with either one of the players winning or the game is tied.
The first departure from "regular" Domain Driven Design is in how we raise and process Domain Events. In most examples I've found online, Domain events are raised by either storing them inside the aggregate root in some List structure, or outside in the Application layer, usually by injecting something into the method. Here we use a different technique, each command returns a list of events. Many people will immediately jump to the fact that this is against CQRS and the "Tell Don't Ask" principal. In this case however, we're not returning part of the object state to manipulate somewhere else (which is what these principals were design to prevent). Instead, we're returning a list of events that describe how the state should be changed. Importantly, we've already carried out the business logic to check if these events are viable, now they are just facts that must be applied to our aggregate or entity. The example below is the Handle method for the Make Move event.
As you can see, I'm using a command handler pattern, where state isn't mutated inside the aggregate at all, but the business logic for whether or not these events can be raised still resides here.
Typically, we would then apply these events to the aggregate so that the state can be mutated. In this approach though, instead of mutating the state, a new instance of the Game
aggregate is returned based on the event in question. Here, there aren't that many events to worry about, so I have a single static method, with a pattern matching switch statement to deal with each event type. It would also be possible to create separate methods to deal with specific events.
How do we deal with the domain events when they're returned? These events are handled at the application layer, where they should be, since the details of what happens to them shouldn't be of concern to the Domain Model. In fact, we can prescribe a very clear set of steps that should be the same for every task:
- Retrieve a list of domain events from the store relating to the aggregate in question.
- Apply a left fold over the events to get the current state of the aggregate.
- Issue the command to the aggregate in order to get a new set of events.
- Append those new events to the store.
In code, this looks something like this:
As it should be clear now, a left fold is where we apply an accumulator function recursively to a sequence. In C# we can achieve this result using the built-in Aggregate method.
Overall, these techniques should help make a codebase more declarative and easier to reason about. I wont go into all of the other advantages here - although there are plenty. One of those benefits is in how we can start to do application level integration tests. The following snippet shows the test for a game that results in a tie:
Hopefully this shows how, just because you don't explicitly keep track of the state of your application, you can still test for the required behaviour and in a way that in my opinion make testing simple.
There are a few more things I'd like to do to with this code, including converting the domain model to F# and looking at a multi paradigm approach to the application. I'm still fairly new to the world of functional programming (and ES/DDD!), so would be very happy to receive feedback in the comments for how I can improve things.
Top comments (0)