Я был заинтересован в создании полностью повторяемых, воспроизводимых состояний игры. Это привело меня в мир 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)
}
}