Неизменяемый управляемый событиями шаблон DDD без сохранения состояния в Kotlin

Я был заинтересован в создании полностью повторяемых, воспроизводимых состояний игры. Это привело меня в мир DDD и, в частности, в концепцию агрегатов + источник событий (хотя это не обязательно концепция DDD). В этом контексте состояние агрегата восстанавливается из последовательности событий домена, каждое событие представляет собой некоторое изменение агрегата. Это дало мне одну часть воспроизводимости, вернувшись к точному состоянию на основе последовательности событий.

new state = old state + event

Еще одна мудрость, которую я прочитал из книг по DDD (Domain-Driven Design и Domain-Driven Design), заключается в том, что объекты-значения, вероятно, следует использовать чаще, чем предполагалось изначально; оставляя Entities более сфокусированными на контейнерах Value Object. Кроме того, объекты-значения можно поддерживать как неизменяемые. Однако это означает, что когда необходимо изменить один из этих объектов-значений, этот объект заменяется в контейнере, а не на месте. Итак, мне нужен был способ получения или создания этих событий. В книге IDDD в разделе «Источники событий на функциональных языках» упоминается, что агрегированные методы могут быть преобразованы в функции без сохранения состояния, которые принимают команду, любые доменные службы и возвращают список событий.

events = state + command

Такая запись в обоих местах привела к перегрузке оператора плюса.

С помощью обеих этих частей я могу моделировать изменение любого объекта-значения или сущности более функциональным способом без сохранения состояния, используя только команды или события.

В следующем коде у нас есть три категории объектов. У нас есть объекты Brazier, которые реагируют на команды Brazier, отвечая событиями Brazier. Жаровни можно воссоздать из событий жаровни, чтобы привести жаровню в заданное состояние. События Brazier — это результат заданного состояния и команды.

Что я ищу

  • Меня беспокоит, что, возможно, я неправильно использую или нарушаю соглашение об операторе «плюс». Когда я думаю о других примерах, которые я видел, они были закрыты по типу, в котором реализован оператор. Например, Int + Int = Int, Double + Double = Double, Money + Money = Money. Хотя это Brazier + Command = Events. Я не думаю, что все, что я прочитал, говорит о том, что оператор должен или должен быть закрыт для одного типа, так что, возможно, это просто то, о чем мне не нужно беспокоиться.
  • В моей предыдущей итерации (не при проверке кода) класс Brazier имел isLit: Boolean имущество. Поскольку было только два варианта: истина или ложь, я сделал их объектами в запечатанной иерархии классов. В других контекстах они все еще могут быть частью такой запечатанной иерархии, может быть объектами, может быть классами, в зависимости от того, требуется ли какое-то другое состояние. Поскольку эти конкретные классы, такие как Brazier, будут реагировать или отвечать только на определенные команды и события, естественно существует конечное число возможных комбинаций, которые, похоже, могут хорошо вписаться в такую ​​запечатанную иерархию, но я ‘ m также задается вопросом, не является ли это хорошим использованием запечатанных иерархий. Тем не менее, похоже, что это здорово упрощает методы обработки, учитывая, что все случаи упоминаются в поле when.
  • Похоже, что этот шаблон хорошо подойдет для агрегата. Но есть неловкое чувство, связанное с его использованием с объектами-значениями, которое я не могу объяснить. Конечно, результат является неизменяемым, и способы изменения объекта-значения четко определены, но это больше похоже на то, что операции объекта-значения должны быть закрыты над типом, но, опять же, возможно, это просто неприятное ощущение, которое не должно сильно весить. много.
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()

Некоторые тесты, иллюстрирующие использование:

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)
    }
}

0

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *