Ga door naar de hoofdinhoud

Whist with a decider

Back in June, I attended DDD Europe 2023. It was the first time I was at this conference, but I liked it a lot, and I was intrigued by Jérémie Chassaing's talk, in which he used a decider to keep track of the internal state of aggregates.

I don't know a lot (yet?) about functional programming, but I was charmed by this elegant way to work with commands and events. So I wanted to experiment with deciders as well. As it turns out, I have an ideal project for doing this kind of experiments: dikdikdik, my web based score sheet for the wiezen (whist) card game. It already had commands and events, so using a decider should not be too hard.

someone deciding what to bid with their cards

Dikdikdik has two aggregates: a table (as in a piece of furniture, the table is where the players sit down, and play their games) and a score sheet. Initially those aggregates had methods that emitted events, and other methods that applied those events to their internal states. The latter ones were also used to rebuild the aggregates based on their event streams, since dikdikdik is also an event sourced application.

In a first step, I replaced the table entity by a TableDecider and a TableInternalState class. The idea is that when you pass the internal state of a table and a command to the decide-method of the TableDecider, it produces events, describing what happens. When passing those events with the internal state to the evolve-method of the decider, it applies the events, creating an updated internal state.

I could use this decider pattern to implement event sourcing, and it also allowed me to create a kind of testing framework, that made it easy to create given-when-then-unit tests in a quite elegant way, see e.g. this test that tests joining players.

I created the decider in a more-or-less test driven way, by converting my old tests for the Table aggregate to new tests for the table decider, and then make them pass by converting the old logic to the decider based logic.

This conversion was interesting, because it made me look back into the existing code for the write side of the table. And whenever I see code I wrote a couple of months or years ago, I am reminded about the things I learned since then. That's a good thing, I presume. Other pieces of the code have become less relevant, since the project has changed as well during the last couples of years. I created a couple of issues on gitlab for the oddities I enountered, e.g. #304, #308, #302.

When all unit tests passed, it didn't take a lot of work to make the integration tests and e2e tests to be all green as well. Which made me happy, because I think this was an indication that the degree of decoupling in my project is low. If you look at the merge request (which I reviewed en merged myself, since I am the only developer on the project 😉), you will notice that almost all changes are in the Domain\WriteModel\Table namespace, and almost no other things needed to change. The other aggregate, the ScoreSheet, still uses my old way of working, and guess what: that doesn't matter. The score sheet doesn't need to care about the internal workings of the table, and vice versa, the events are the only things that pass the boundries between them.

All with all, there were two other things I needed to change. One thing, was the validation of the games that can be logged. Now I can use a service for this, that I can nicely inject into the decider, which is way more elegant than what I did before to let the aggregate class validate the logged games. (The validation service still has some issues in its current form, but it is already injected; that's something.)

Another thing is that the decider is aware of the initial state of the aggregate. Previously, I prepared the initial state by handling the event TableCleared. Now this event is not really needed anymore, but in my integration tests I still (ab)use it to reset the read models.

Another interesting change that happened, when introducing the decider, is that I don't have a dedicated handler for each command anymore. Which feels a little strange to me, I always learnt that each command should have its own handler. Now the TableDecider takes every TableCommand, and calls the handler function that corresponds to the command. But I guess these functions are the handlers now, which may be fine.

Anyway, I am happy about the result I've got so far. I think I will already release this to score.rijkvanafdronk.be at the end of this month, so that we can try it out during the next meetup of our wiezen club. (Of course everything should just work, since all tests are green, but I want to use the application quickly after the release, so that we would notice any bugs that are not covered by the tests.)

So what's next:

Now that the write side for the table is handled by a decider, I will also replace the one for the score sheet. Since the ScoreSheet class is much smaller than the Table class was, this should take less time than converting the table, but I also have little free time to develop these days, so we will see how this turns out.

And then there's the process manager, that passes commands to the score sheet when games are played at the table. I think Jérémy did something like this in his talk, by combining deciders, but I will probably have to watch a recording of his talk again, because I'm not sure anymore about how this works.

Another thing I want to do, is add the generic decider classes I created to the krakboem libray I created for my own event sourced projecs. That library needs some updates as well, because it was created when php 7.4 was a thing.

For a short period of time, I had timelines for the new features of dikdikdik. But not anymore, because life happens, and I will see when I have time to code. Anyway, I want to thank Jérémie for his inspiring talk. I'm glad I got this far already, and we'll see what the future brings.

Until then: have a nice time playing whist.

Commentaar

Comments powered by Disqus