I've been interested in making fully repeatable, reproducible game states. This led me into the world of DDD, and specifically, the concept of Aggregates + Event Sourcing (although that's not necessarily a DDD concept). In this context, the state of the Aggregate is reconstituted from a sequence of Domain Events, each Event representing some change to the Aggregate. This gave me one portion of reproducibility, returning to an exact state based on a sequence of Events.
new state = old state + event
Another piece of wisdom I read from the DDD books (Domain-Driven Design, and Implementing Domain-Driven Design), is that Value Objects should probably be used more often than initially realized; leaving Entities to be more focused as Value Object containers. Additionally, Value Objects can be maintained as Immutable. This means though that when a change needs to happen to one of these Value Objects, the object is replaced in the container, rather than being changed in place. So I needed a way of getting, or generating these Events. The IDDD book mentions in the Event Sourcing in Functional Languages section that Aggregate methods can be transformed into stateless functions that take the Command, any Domain Services, and returns a list of Events.
events = state + command
This kind of notation in both places led me to overloading the plus operator.
With both of these pieces, I'm able to model changing any Value Object or Entity in a more functional, stateless way, using only Commands or Events.
In the following code, we have three categories of objects. We have the Brazier objects, which react to Brazier Commands, by responding with Brazier Events. Braziers can be reconstituted from Brazier Events, to bring a Brazier to a given state. Brazier Events are the result of a given state, plus a Command.
What I'm looking for
- I'm worried that perhaps I'm misusing, or abusing the plus operator convention. When I think about other examples I've seen, they have been closed over the type that implemented the operator. For instance, Int + Int = Int, Double + Double = Double, Money + Money = Money. This though is Brazier + Command = Events. I don't think anything I've read says that the operator should, or must be closed over a single type, so maybe it's just something that I don't need to worry about.
- In my previous iteration (not on code review), the Brazier class had an
isLit: Boolean
property. Since there were only two options, true or false, I made them objects in a sealed class hierarchy. In other contexts, they could still be part of a sealed hierarchy like this, maybe being objects, maybe being classes, depending on if some other state is required. Since these specific classes, like Brazier, will only react to, or respond with, specific Commands and Events, there's kind of naturally a finite number of possible combinations, which seems like it might fit well with a sealed hierarchy like this, but I'm also wondering if this isn't a great use of the sealed hierarchies. It does seem to nicely simplify the handling methods though, given that all cases are mentioned in the when. - This pattern seems like it would work well for an Aggregate. But there's this awkward feeling with regards to using it with Value Objects that I can't quite explain. Sure, the result is Immutable, and the ways the Value Object can change are clearly defined, but it feels more like Value Object operations should be closed over the type, but again, maybe this is just an uncomfortable feeling that shouldn't weigh very much.
sealed class BrazierCommand
object LightBrazier : BrazierCommand()
object ExtinguishBrazier : BrazierCommand()
sealed class BrazierEvent
object BrazierLit : BrazierEvent()
object BrazierExtinguished : BrazierEvent()
sealed class Brazier {
companion object {
fun fromEvents(
events: Iterable<BrazierEvent>,
initial: Brazier = UnlitBrazier,
): Brazier =
events.fold(initial, Brazier::plus)
}
operator fun plus(command: BrazierCommand): Iterable<BrazierEvent> =
when (command) {
is LightBrazier -> listOf(BrazierLit)
is ExtinguishBrazier -> listOf(BrazierExtinguished)
}
operator fun plus(event: BrazierEvent): Brazier =
when (event) {
is BrazierLit -> LitBrazier
is BrazierExtinguished -> UnlitBrazier
}
}
object LitBrazier : Brazier()
object UnlitBrazier : Brazier()
Some tests that illustrate usage:
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class BrazierTests {
@Test
fun `UnlitBrazier plus LightBrazier equals BrazierLit`() {
val brazierEvents = UnlitBrazier + LightBrazier
assertEquals(BrazierLit, brazierEvents.single())
}
@Test
fun `LitBrazier plus ExtinguishBrazier equals BrazierExtinguished`() {
val brazierEvents = LitBrazier + ExtinguishBrazier
assertEquals(BrazierExtinguished, brazierEvents.single())
}
@Test
fun `UnlitBrazier plus BrazierLit equals LitBrazier`() {
val brazier = UnlitBrazier + BrazierLit
assertEquals(LitBrazier, brazier)
}
@Test
fun `LitBrazier plus BrazierExtinguished equals UnlitBrazier`() {
val brazier = LitBrazier + BrazierExtinguished
assertEquals(UnlitBrazier, brazier)
}
@Test
fun `reconstitute LitBrazier from events`() {
val events = listOf(
BrazierLit,
)
val brazier = Brazier.fromEvents(events)
assertEquals(LitBrazier, brazier)
}
}