Раздел 27 · Gleam на практике

Event Modeling: от свимлейна на доске к типам Gleam

middle~25 мин

открытый урокЭтот раздел читается без входа. Войди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Event Modeling: от свимлейна на доске к типам Gleam

Первая ката второго блока. Десять кат назад мы строили код. Сегодня откладываем редактор и берём маркер. Event Modeling это способ договориться о системе с не-программистом до первого теста, и язык, на котором написан весь второй блок серии.

Сцена · почему сначала доска, а не код

В первом блоке мы шли снизу: тип Email, потом Money, потом агрегат, потом HTTP. Это работало, потому что домен был знаком. Но в реальном проекте ты приходишь к эксперту (менеджеру отеля), и у вас нет общего словаря. Если начать с кода, ты закодируешь своё неверное понимание, и узнаешь об этом через месяц.

Event Modeling переворачивает порядок: сначала рисуем на доске, как система ведёт себя во времени, проверяем картину с экспертом, и только потом переводим стикеры в типы. Перевод занимает минуты, потому что вся сложная работа (понять домен) уже сделана на доске.

Это не то же, что Event Storming Брандолини, хотя их часто путают. Event Storming это разведка боем по незнакомому домену, шумный воркшоп со стеной стикеров. Event Modeling строже: временная ось, четыре цвета, сценарий читается слева направо. Нам нужен второй: он напрямую ложится в Decider урока 28.

Карта урока · что заберёшь через 2.5 часа

  • Освоишь четыре типа стикеров и научишься читать свимлейн как фильм.
  • Нарисуешь свимлейн для брони (place, cancel, check-in, check-out).
  • Переведёшь стикеры в типы Gleam: команда это конструктор Command, событие это конструктор Event.
  • Запишешь сценарии в форме given/when/then, готовой к Decider.

Концепт · четыре цвета и временная ось

Event Modeling кладёт время по горизонтали. Слева направо это история одной брони. По вертикали свимлейны: UI сверху, команды и события в середине, read-models снизу. Стикеры бывают четырёх типов:

  1. Команда (синяя). Запрос на изменение в настоящем времени: PlaceReservation, CheckInGuest. Команду можно отвергнуть. Её инициирует пользователь через экран или другой процесс.
  2. Событие (оранжевая). Факт в прошедшем времени: ReservationPlaced, GuestCheckedIn. Событие уже случилось, отменить нельзя. Команда, если прошла, порождает одно или несколько событий.
  3. Read-model (зелёная). Денормализованный вид под конкретный вопрос UI: daily occupancy, in-house guests. Собирается из событий.
  4. Экран UI (жёлтая). То, что видит человек: форма брони, список заселённых. Экран читает read-model и шлёт команды.

Поток всегда один и тот же: экран шлёт команду, команда порождает событие, событие обновляет read-model, read-model показывается на экране. Это кольцо и есть скелет любой системы в этом стиле.

Собери свимлейн сам: разложи команды, события и read-model по порядку и поймай неверный порядок:

Языковые механики · стикер становится конструктором

Перевод механический, и это его главное достоинство. Каждый синий стикер становится вариантом типа Command, каждый оранжевый, вариантом Event. Мы это уже сделали в уроке 21, теперь видно, откуда взялась форма:

// examples/ddd-hotel/gleam/src/domain/events.gleam

pub type Command {
  PlaceReservation(
    id: ReservationId,
    guest: CustomerId,
    room: RoomNumber,
    range: DateRange,
  )
  CancelReservation(id: ReservationId)
  CheckInGuest(id: ReservationId)
  CheckOutGuest(id: ReservationId)
}

pub type Event {
  ReservationPlaced(
    id: ReservationId,
    guest: CustomerId,
    room: RoomNumber,
    range: DateRange,
    occurred_at: Int,
  )
  ReservationCancelled(id: ReservationId, occurred_at: Int)
  GuestCheckedIn(id: ReservationId, occurred_at: Int)
  GuestCheckedOut(id: ReservationId, occurred_at: Int)
}

Два наблюдения. Команда в настоящем времени, событие в прошедшем: грамматика на стикере отражается в имени конструктора. И у события есть occurred_at, потому что факт случился в конкретный момент, а у команды его нет: команда это намерение, времени свершения у неё ещё нет.

Задача · свимлейн брони

Нарисуй на бумаге (или в любом редакторе диаграмм) временную ось одной брони. Вот целевой результат в ASCII, твоя задача воспроизвести его руками и понять каждую стрелку:

UI:        [Форма брони]      [Стойка ресепшен]    [Стойка выезда]
               |                    |                    |
команда:   PlaceReservation    CheckInGuest        CheckOutGuest
               |                    |                    |
               v                    v                    v
событие:   ReservationPlaced   GuestCheckedIn      GuestCheckedOut
               |                    |                    |
               +--------+-----------+--------------------+
                        v
read-model:        in-house guests (зелёная)
                        |
                        v
UI:                [Список заселённых]

Контракт свимлейна:

  • Каждая команда стоит над событием, которое она порождает. Если команда не порождает события, это не команда, это запрос.
  • CancelReservation это ветка: из состояния Reserved уходит не в check-in, а в ReservationCancelled и дальше никуда. Дорисуй эту ветку.
  • Зелёный in-house guests питается от GuestCheckedIn (добавить) и GuestCheckedOut или ReservationCancelled (убрать). Проведи эти стрелки.

Подсказки

  • Читай свимлейн слева направо вслух, как историю: “гость заполнил форму, система приняла бронь, на ресепшен гость заселился, при выезде выселился”. Если история звучит криво, модель кривая.
  • Команда и событие почти всегда парные, но не всегда один к одному. Одна команда может породить два события. Это нормально, рисуй обе оранжевые карточки.
  • Не рисуй стрелку от команды к команде. Команды не вызывают команды напрямую. Между ними всегда событие (а в уроке 31 процесс-менеджер, который слушает событие и шлёт новую команду).
  • Read-model не порождает события. Стрелки в него только входят.

Разбор · что мы только что описали

Свимлейн выше это полная спецификация write-стороны брони, и она ровно соответствует коду:

  • Три синие команды это три конструктора Command (плюс CancelReservation четвёртым).
  • Четыре оранжевых события это четыре конструктора Event.
  • Зелёный in-house guests это проекция, которую мы напишем в уроке 30 (projections/occupancy.gleam).
  • Логика “какая команда из какого состояния разрешена” это Decider урока 28.

Иначе говоря, доска уже содержит весь второй блок. Урок 28 формализует синие-к-оранжевым переходы (decide), урок 29 хранит оранжевые как стрим фактов, урок 30 собирает зелёные из оранжевых. Мы не придумываем архитектуру, мы переписываем доску в типы.

Концепт · given / when / then как мост к Decider

Последний шаг воркшопа: каждый сценарий со свимлейна записываем в форме, которую завтра проверит тест. Это given/when/then:

Сценарий: заселить забронированного гостя
  given:  ReservationPlaced
  when:   CheckInGuest
  then:   GuestCheckedIn

Сценарий: нельзя заселить дважды
  given:  ReservationPlaced, GuestCheckedIn
  when:   CheckInGuest
  then:   отказ (InvalidStateTransition from CheckedIn)

Сценарий: нельзя отменить заселённого
  given:  ReservationPlaced, GuestCheckedIn
  when:   CancelReservation
  then:   отказ (InvalidStateTransition from CheckedIn)

given это список прошлых событий, when это команда, then это новые события или отказ. В уроке 28 эта запись станет тестом дословно: given свернётся в состояние через evolve, decide прогонит команду, then сравнится с результатом.

Критика · что не покрыто

  • Только один агрегат. Свимлейн брони не показывает границу с Billing. Полная карта отеля это три свимлейна (Reservations, Front Desk, Billing) и стрелки между ними. Их рисуют отдельно, чтобы не утонуть (ДЗ).
  • Нет процессов. Стрелка “событие в одном контексте запускает команду в другом” (charge-on-checkout) это процесс-менеджер урока 31. На свимлейне это отдельная дорожка между контекстами.
  • Доска не код. Event Modeling не генерирует код автоматически. Перевод ручной, и в нём ты ещё раз проверяешь модель. Это фича, а не баг.
  • Read-models мы только наметили. Какие именно зелёные стикеры нужны, зависит от экранов. Список под каждый вопрос UI это ДЗ.

Takeaway

Одна фраза:

Event Modeling это договорённость о поведении системы во времени до кода: синие команды порождают оранжевые события, из них собираются зелёные read-models. Перевод стикеров в типы Gleam механический, и given/when/then с доски становится тестом Decider один в один.

ДЗ

Дальше

Следующая ката · 28. Decider Жереми Шассена. Формализуем синие-к-оранжевым переходы: decide(state, command) решает, какие события породить, evolve(state, event) применяет факт к состоянию. Три функции, и из них собирается агрегат, FSM, процесс-менеджер.

Параллельно полезно перечитать:

  • 21. Доменные события, где впервые появились Command и Event. Сегодня стало видно, откуда взялась их форма.
  • 16. Functional DDD intro, раздел про команды и события как два языка. Свимлейн это та идея, нарисованная на доске.