Замена проверки типа на посетителя в неизменяемом агрегате на основе событий

В моем предыдущем вопросе в одном из ответов упоминалось, что я не должен изменять поведение на основе класса Event.

Весь смысл классификации в том, что вы можете получить результат, не принимая решения на основе экземпляра.

Я не возражаю с этим мнением и полагаю, что это более приемлемо из-за Документация Kotlin по шаблону использования с запечатанными классами.

На самом деле я ищу способ координировать взаимодействие между двумя разными типами классов.

Раньше я не использовал шаблон Visitor, но из того, что я читал, май быть хорошим вариантом использования.


Brazier — это агрегат DDD с событиями. Его состояние восстанавливается из событий. Погашенные жаровни можно зажечь, зажженные жаровни можно потушить. Brazier инкапсулирует переход конечного состояния между включенным и погашенным. Никаких изменений не произойдет, если мы попытаемся зажечь зажженный мангал, точно так же, как не произойдет никаких изменений, если мы попытаемся потушить потушенный мангал.

Когда команда выполняется, возвращается список событий, а не изменяется состояние жаровни. Затем события в конечном итоге сохраняются в хранилище событий, и во время восстановления инициализируется новое состояние.

Brazier поддерживает две команды: light а также extinguish. Эти команды вызывают два события: Lit а также Extinguished.

Я собираюсь представить два раздела кода.

Первый демонстрирует код с помощью проверки типа с when чтобы решить, каким должно быть новое состояние на основе обрабатываемого события.

Второй демонстрирует код, использующий (возможно, наивный) шаблон посетителя, чтобы решить, каким должно быть новое состояние на основе обрабатываемого события. Здесь Событие — это Посетитель, вызывающий правильный метод для Состояния, в зависимости от конкретного типа Состояния.

Обе реализации имеют почти идентичные тесты, с той лишь разницей, что используется тип Brazier.

Основное различие между ними — интерфейс иерархии событий и afterApplying в классах State.

Это довольно упрощенный пример того, где и как я хотел бы это использовать; в других местах, где я оказался в этой должности, события гораздо более задействованы, и можно использовать больше типов состояний (6 событий и 5 состояний в одном случае).

У меня нет никаких планов, позволяющих расширять иерархию состояний или событий за пределы ядра, которое я пишу, поэтому мы можем предположить, что будет довольно фиксированное количество событий и состояний (несомненно, менее 10).

BrazierWithWhen

// === Point of Interest ===
sealed class BrazierWithWhenEvent
class WhenLitEvent : BrazierWithWhenEvent()
class WhenExtinguishedEvent : BrazierWithWhenEvent()

sealed class BrazierWithWhenState {
    abstract fun light(): List<BrazierWithWhenEvent>
    abstract fun extinguish(): List<BrazierWithWhenEvent>
    abstract fun lit(event: WhenLitEvent): BrazierWithWhenState
    abstract fun extinguished(event: WhenExtinguishedEvent): BrazierWithWhenState
    abstract fun afterApplying(event: BrazierWithWhenEvent): BrazierWithWhenState
}

class WhenLitState : BrazierWithWhenState() {
    override fun light(): List<BrazierWithWhenEvent> =
        emptyList()

    override fun extinguish(): List<BrazierWithWhenEvent> =
        listOf(WhenExtinguishedEvent())

    override fun lit(event: WhenLitEvent): BrazierWithWhenState =
        this

    override fun extinguished(event: WhenExtinguishedEvent): BrazierWithWhenState =
        WhenExtinguishedState()

    // === Point of Interest ===
    override fun afterApplying(event: BrazierWithWhenEvent): BrazierWithWhenState =
        when (event) {
            is WhenLitEvent -> lit(event)
            is WhenExtinguishedEvent -> extinguished(event)
        }
}

class WhenExtinguishedState : BrazierWithWhenState() {
    override fun light(): List<BrazierWithWhenEvent> =
        listOf(WhenLitEvent())

    override fun extinguish(): List<BrazierWithWhenEvent> =
        emptyList()

    override fun lit(event: WhenLitEvent): BrazierWithWhenState =
        WhenLitState()

    override fun extinguished(event: WhenExtinguishedEvent): BrazierWithWhenState =
        this

    // === Point of Interest ===
    override fun afterApplying(event: BrazierWithWhenEvent): BrazierWithWhenState =
        when (event) {
            is WhenLitEvent -> lit(event)
            is WhenExtinguishedEvent -> extinguished(event)
        }
}

class BrazierWithWhen(
    private val state: BrazierWithWhenState
) {

    companion object {
        fun fromEvents(events: List<BrazierWithWhenEvent> = emptyList()) =
            events.fold(
                BrazierWithWhen(WhenExtinguishedState()),
                BrazierWithWhen::afterApplying)
    }

    fun light(): List<BrazierWithWhenEvent> =
        state.light()

    fun extinguish(): List<BrazierWithWhenEvent> =
        state.extinguish()

    private fun afterApplying(event: BrazierWithWhenEvent): BrazierWithWhen =
        BrazierWithWhen(state.afterApplying(event))
}

BrazierWithWhenTests

import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class BrazierWithWhenTests {

    @Test
    fun `light extinguished brazier`() {
        val brazier = BrazierWithWhen.fromEvents()
        val events = brazier.light()

        assertTrue(events.single() is WhenLitEvent)
    }

    @Test
    fun `extinguish lit brazier`() {
        val brazier = BrazierWithWhen.fromEvents(listOf(
            WhenLitEvent()
        ))
        val events = brazier.extinguish()

        assertTrue(events.single() is WhenExtinguishedEvent)
    }

    @Test
    fun `light lit brazier`() {
        val brazier = BrazierWithWhen.fromEvents(listOf(
            WhenLitEvent()
        ))
        val events = brazier.light()

        assertTrue(events.isEmpty())
    }

    @Test
    fun `extinguish extinguished brazier`() {
        val brazier = BrazierWithWhen.fromEvents()
        val events = brazier.extinguish()

        assertTrue(events.isEmpty())
    }
}

Мангал с посетителем

// === Point of Interest ===
sealed class BrazierWithVisitorEvent {
    abstract fun visit(state: VisitorLitState): BrazierWithVisitorState
    abstract fun visit(state: VisitorExtinguishedState): BrazierWithVisitorState
}

// === Point of Interest ===
class VisitorLitEvent : BrazierWithVisitorEvent() {
    override fun visit(state: VisitorLitState): BrazierWithVisitorState =
        state.lit(this)

    override fun visit(state: VisitorExtinguishedState): BrazierWithVisitorState =
        state.lit(this)
}

// === Point of Interest ===
class VisitorExtinguishedEvent : BrazierWithVisitorEvent() {
    override fun visit(state: VisitorLitState): BrazierWithVisitorState =
        state.extinguished(this)

    override fun visit(state: VisitorExtinguishedState): BrazierWithVisitorState =
        state.extinguished(this)
}

sealed class BrazierWithVisitorState {
    abstract fun light(): List<BrazierWithVisitorEvent>
    abstract fun extinguish(): List<BrazierWithVisitorEvent>
    abstract fun lit(event: VisitorLitEvent): BrazierWithVisitorState
    abstract fun extinguished(event: VisitorExtinguishedEvent): BrazierWithVisitorState
    abstract fun afterApplying(event: BrazierWithVisitorEvent): BrazierWithVisitorState
}

class VisitorLitState : BrazierWithVisitorState() {
    override fun light(): List<BrazierWithVisitorEvent> =
        emptyList()

    override fun extinguish(): List<BrazierWithVisitorEvent> =
        listOf(VisitorExtinguishedEvent())

    override fun lit(event: VisitorLitEvent): BrazierWithVisitorState =
        this

    override fun extinguished(event: VisitorExtinguishedEvent): BrazierWithVisitorState =
        VisitorExtinguishedState()

    // === Point of Interest ===
    override fun afterApplying(event: BrazierWithVisitorEvent): BrazierWithVisitorState =
        event.visit(this)
}

class VisitorExtinguishedState : BrazierWithVisitorState() {
    override fun light(): List<BrazierWithVisitorEvent> =
        listOf(VisitorLitEvent())

    override fun extinguish(): List<BrazierWithVisitorEvent> =
        emptyList()

    override fun lit(event: VisitorLitEvent): BrazierWithVisitorState =
        VisitorLitState()

    override fun extinguished(event: VisitorExtinguishedEvent): BrazierWithVisitorState =
        this

    // === Point of Interest ===
    override fun afterApplying(event: BrazierWithVisitorEvent): BrazierWithVisitorState =
        event.visit(this)

}

class BrazierWithVisitor(
    private val state: BrazierWithVisitorState
) {
    companion object {
        fun fromEvents(events: List<BrazierWithVisitorEvent> = emptyList()): BrazierWithVisitor =
            events.fold(
                BrazierWithVisitor(VisitorExtinguishedState()),
                BrazierWithVisitor::afterApplying)
    }

    fun light(): List<BrazierWithVisitorEvent> =
        state.light()

    fun extinguish(): List<BrazierWithVisitorEvent> =
        state.extinguish()

    private fun afterApplying(event: BrazierWithVisitorEvent): BrazierWithVisitor =
        BrazierWithVisitor(state.afterApplying(event))
}

BrazierWithVisitorTests

import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class BrazierWithVisitorTests {

    @Test
    fun `light extinguished brazier`() {
        val brazier = BrazierWithVisitor.fromEvents()
        val events = brazier.light()

        assertTrue(events.single() is VisitorLitEvent)
    }

    @Test
    fun `extinguish lit brazier`() {
        val brazier = BrazierWithVisitor.fromEvents(listOf(
            VisitorLitEvent()
        ))
        val events = brazier.extinguish()

        assertTrue(events.single() is VisitorExtinguishedEvent)
    }

    @Test
    fun `light lit brazier`() {
        val brazier = BrazierWithVisitor.fromEvents(listOf(
            VisitorLitEvent()
        ))
        val events = brazier.light()

        assertTrue(events.isEmpty())
    }

    @Test
    fun `extinguish extinguished brazier`() {
        val brazier = BrazierWithVisitor.fromEvents()
        val events = brazier.extinguish()

        assertTrue(events.isEmpty())
    }
}

Обе реализации работают достаточно хорошо (1 мс для каждого теста), но меня больше всего интересует, является ли это лучшим способом избежать проверки типов для чего-то вроде этого.

Записывая все возможные комбинации, есть только пара интересных переходов между событием и состоянием (горит -> погашен, погашен -> горит), а некоторые другие не очень интересны (горит -> горит, погашен -> погашен). С парой команд и событий это не так уж плохо, но с 5 и 6 интерфейс станет более подробным с шаблоном посетителя (1 метод на команду, 1 метод на событие, на состояние), тогда как мы можем использовать when+else описывать только интересные случаи и игнорировать неинтересные.

0

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

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