2
\$\begingroup\$

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)
    }
}
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Very interesting question! Makes me want to read more about the DDD topic, which I should :-)

For now at least to answer one of your questions:

Not sure for plus, but times/div operator is definitely okay to use for different types. It's typical for multiplication or division with scalar. Imagine for example 2D Vector multiplied by scalar value. Then signature would be something like:

operator fun Vector.times(ratio: Float): Vector {
    return Vector(x = x * ratio, y = y * ratio)
}

I am not sure though, if plus as an operator is the correct way to go. Can't think of real-world example, where you are adding 2 completely different types returning 3rd. Is there a better name for the operation? Is that really "adding"? Maybe so, maybe not.

Definitely consider infix notation as alternative too: https://kotlinlang.org/docs/functions.html#infix-notation

Edit:

I don't like implementations of your plus and minus function - this branching using when. The whole point of classing is that you can get result without having to decide based on instance. Maybe BrazierEvent class should have a field, that is supposed to be returned to basically do mapping you have there, ex. BrazierLit -> LitBrazier and BrazierExtinguished -> UnlitBrazier. Same goes for minus and that functionality. It should be inside of BrazierCommand class.

\$\endgroup\$
3
  • \$\begingroup\$ Yeah, previously I had a different naming pattern. applyEvent, and then separate command methods, light or extinguish, which I changed to an applyCommand method. At this point, I started using the infix notation, like newState = oldState applyEvent event. Then, while I was writing some notes about the structure, I started using the plus notation mentioned in the OP. That's what lead me to think about it in terms of the plus operator. But yeah, it's not quite adding. Thanks again for the feedback! \$\endgroup\$
    – Zymus
    Apr 7, 2021 at 17:25
  • \$\begingroup\$ No worries, looking back at it again, I added edit to comment on one more code smell. \$\endgroup\$
    – K.H.
    Apr 12, 2021 at 21:55
  • \$\begingroup\$ Thanks for the edit. My usage of the Sealed Class + When (is) was partially inspired by this section from the Kotlin documentation (kotlinlang.org/docs/…) where it says "The key benefit of using sealed classes comes into play when you use them in a when expression." \$\endgroup\$
    – Zymus
    Apr 18, 2021 at 2:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.