К основному содержимому
к выпуску
Конспект выпуска2022Fellow

Learning Domain-Driven Design — глава 2, часть 1

Второе заседание клуба по книге Влада Хононова: Владимир Иванов, Вова Чистяков, Александр Поломодов и Илья разбирают три главы тактической части — Transaction Script и Active Record, доменную модель с её строительными блоками и Event Sourcing. Из выпуска: почему инкремент счётчика — это распределённая транзакция, чем value object отличается от entity и как удалить данные из неизменяемого журнала.

Code of Architecture · книжный клуб7 минут

Конспект сокращён относительно полного выпуска; примеры и оценки передают опыт и позиции участников.

Основная линия материала
01

Простая логика и мера слова «просто»

Влад начинает тактическую часть с пересказа паттернов Фаулера, и первый — Transaction Script: бизнес-слой организован вокруг процедур, каждый метод достаёт данные, выполняет шаги и фиксирует результат, а главное требование — атомарность: либо всё дошло до хранилища, либо всё откатилось. Александр вспоминает, как писал такие скрипты в начале карьеры, и раскладывает ловушки по нарастающей: явная транзакция в реляционной базе — простой случай; запись в базу плюс в лог или на диск — уже распределённая транзакция; самая коварная — неявная, когда система из браузера, сети, сервера и хранилища не идемпотентна и повторно дёрнутый инкремент увеличивает счётчик непонятно на сколько. Вова добавляет, что при чтении главы всплывает вся предыдущая книга Клеппмана — задержки, несинхронные часы, select for update. Скрипт хорош для supporting- и generic-поддоменов и для интеграции с чужим решением внутри защитного слоя, а антипаттерном становится, когда на нём строят core-логику с дублированием и расползающимися правилами.

Active Record — тот же скрипт, но для структур посложнее: объектов и деревьев, которые дешевле собирать через ORM. Владимиру сразу вспоминается Spring Data REST, выбрасывающий наружу ручки к базе, Вове — Django с его любовью к паттерну; для CRUD, отчётов и небольших сервисов это уместно, а граница, за которой всё переписывают на сервисы, плавающая. Участникам нравится, что Влад не клеймит анемичную модель антипаттерном, а называет инструментом: в DDD-сообществе за анемичную модель обычно убеждают «делать нормально», хотя для простой части домена простое решение честнее. Александр формулирует прагматично: не размахивать молотком там, где домен не относится к ядру, — взять Active Record, прикрутить фронт, выкатить и забыть, потому что ядро всё равно потребует другого. Илья добавляет, что DDD преподносится как бизнес-инструмент, и если ценность проще доставить простым способом, это и есть правильный выбор; Владимир предлагает посчитать, сколько раз за десять минут прозвучало слово «просто».

02

Доменная модель: язык, value objects и агрегаты

Со сложной логикой начинается собственно доменная модель — Александр ведёт историю от книги Фаулера, где Эванс ещё только упомянут, к его собственной книге. Признаки сложного домена: много переходов состояния, инварианты, которые нужно соблюдать при каждом переходе, и конкурентное преимущество, которое трудно повторить. Отсюда деление сложности на существенную, принадлежащую домену, и случайную, которую придумали себе сами; чтобы не плодить вторую, бизнес-логика живёт в чистых объектах, инфраструктура спрятана, и в коде остаются существительные и глаголы предметной области — тот самый единый язык. Вова рассказывает про австрийские компании, где домен был написан по-немецки, и он, так и не заговорив по-немецки, написал плагин для IntelliJ IDEA, который через API Яндекса переводил выделенное слово. Совет отсюда простой: выбрать язык, на котором говорит компания, и держать на нём весь код; Илья вспоминает идею Влада про плагин со словарём единого языка.

Value object определяется своими значениями и не нуждается в идентификаторе: имя, телефон, email как отдельные типы несут правила проверки внутри, и новичку не приходится искать, где строку уже проверили. Вова показывает границу на себе: синий цвет всегда синий, а Володя Чистяков в мире не один, поэтому людям нужен идентификатор и entity, атрибуты которого меняются при сохранении тождества — вплоть до корабля Тесея. Александр вспоминает систему для миграционной службы с дедупликацией людей по ФИО, дате рождения и истории документов через нечёткий поиск и четверых Андреев Ивановых в своих командах, которым хотелось выдать футболки с номерами. Из книги — «используйте value objects всегда, когда можете», из практики — inline-классы Kotlin как тестирование через типизацию. Агрегат — entity с границей консистентности: иерархия обновляется одной транзакцией, снаружи состояние меняет только корень, чужие агрегаты хранятся по ссылке и сходятся со временем. Пример из книги — тикет, который переназначают на другого сотрудника поддержки, если тот не ответил вовремя; вопрос из чата про сущности в разных хранилищах решается той же логикой: если между ними допустим промежуток, это разные агрегаты.

03

Domain service, события и Event Sourcing

Domain service — третий кубик: расчёт, завязанный на несколько агрегатов, например время реакции сотрудника с учётом его расписания и тикетов. Александру нравится, как Влад снимает возражение о нарушении правила «один агрегат — одна транзакция»: сервис читает значения нескольких агрегатов и считает, но состояние не меняет, а меняют его по-прежнему по одному. Вова замечает, что формальной защиты от обновления двух агрегатов из такого сервиса нет — это конвенция, а Илья добавляет доменные события, которые описывают случившееся с агрегатом и несут потребителям нужные данные. Отдельная находка главы — управление сложностью через степени свободы, как в физике: чем больше системе разрешено, тем труднее предсказать её поведение, а инварианты в агрегатах сужают пространство состояний и не дают данным побиться так, что никто не понимает откуда. Дальше — измерение времени: Вова пересказывает мысль Клеппмана, что база данных — лишь снимок, а поток событий — источник истины, и в event-sourced модели те же кубики, только состояние хранится как последовательность доменных событий.

Плюсы известны: путешествие во времени при разборе багов, новые представления поверх уже случившихся событий и аудит. Версию агрегата Влад предлагает завести прямо в модели, и Владимир, поначалу считавший это дублированием оптимистичной блокировки, оценил, что при конфликте можно достать пришедшие события и решить, катиться ли дальше. Минусы — крутая кривая обучения и эволюция схемы событий, под которую написана отдельная и, как шутят участники, вечно недописанная книга. Из раздела вопросов: производительность лечат снапшотами, с которых догоняют хвост событий, а партиционируют по идентификатору агрегата; удаление данных по требованию регулятора — забываемой полезной нагрузкой, когда чувствительные поля шифруют отдельно и уничтожают ключ, либо событиями, которые несут только идентификаторы. На вопрос, почему не писать лог и состояние параллельно, ответ тот же, что у Клеппмана: две таблицы — два источника истины, кто-нибудь обновит один из них, а CDC покажет, что поля поменялись, но не объяснит почему. Следующая встреча — архитектурные паттерны; книга, по замечанию Александра, читается вдвое быстрее Клеппмана.

Выводы

Что стоит унести с собой

  1. 01Transaction Script и Active Record — честный выбор для supporting- и generic-логики; их ловушки — распределённые и неявные транзакции, неидемпотентные повторы и дублирование правил, когда на них строят ядро.
  2. 02Анемичная модель — инструмент, а не антипаттерн: для простой части домена простое решение честнее, а полноценную доменную модель стоит приберечь для core.
  3. 03Value object определяется значениями и не нуждается в идентификаторе, entity сохраняет тождество при изменениях, а агрегат — граница консистентности, которую снаружи меняют только через корень.
  4. 04Event Sourcing делает поток событий источником истины и даёт аудит и новые проекции, но требует снапшотов, эволюции схем и отдельного плана удаления персональных данных.

Источники