DDD: bounded context, ubiquitous language и command guard
Код в этой главе записан в прежней поверхности языка — со словами
категория,объект,утилита. Сегодняшний компилятор её слова читает, но программой такой файл не считает: файл, где есть только утилиты,flang checkотклоняет. Разбор задачи в главе верен; синтаксис переносится по таблице из главы «Старые модели».
Глава разбирает, где категория FTS совпадает с bounded context из DDD, а где заканчивается: FTS хорошо описывает факты и переходы состояния, но не заменяет агрегат, репозиторий и шину событий.
Минимальный пример
категория «Исполнение заказа»
объект Заказ
номер является строкой
«готов к отгрузке» является состоянием «Готов к отгрузке»
морфизм «Готовый заказ можно отгрузить»
если «Готов к отгрузке»
то «Отгрузить заказ разрешено»
Что делает компилятор
fts check разбирает категорию, объект и морфизм, проверяет, что состояние
«Готов к отгрузке» типизировано, а условие морфизма ссылается на реально
объявленный факт объекта. Если правило в морфизме ссылается на несуществующее
состояние, компиляция не пройдёт: fts check вернёт диагностику раньше, чем
код доедет до code review. fts prove (или fts certify/fts verify с
context) идёт дальше: он проверяет, что для конкретного экземпляра заказа факт
«готов к отгрузке» действительно равен да в данных, а не только объявлен в
типе.
Категория как bounded context
Одна категория .fts — это один bounded context, а не вся предметная область
компании. «Исполнение заказа» отвечает за переход от готовности к разрешению
отгрузки. Оплата, бухгалтерия и складской учёт — другие контексты со своими
моделями, даже если они читают то же поле заказы[номер] из общей базы.
Термины внутри категории обязаны совпадать с языком, на котором говорит
бизнес. Если склад говорит «подтвердил приёмку», в модели должно быть
«склад подтвердил», а не warehouseFlag или status === 3. Ubiquitous
language в FTS не декларация в глоссарии — она исполняется: fts test
падает, если пример не совпадает с правилом, записанным теми же словами, что
использует предметный эксперт.
Морфизм как разрешённый переход состояния
Морфизм в FTS — это не произвольная функция, а конкретное разрешённое превращение факта в другой факт: «если готов к отгрузке, то отгрузка разрешена». Он не выполняет отгрузку и не двигает деньги — он утверждает допустимость перехода. Реальный переход (запись в БД, вызов курьерской службы) остаётся кодом снаружи. Это прямое соответствие DDD-принципу: правила предметной области отделены от инфраструктурных эффектов.
Command guard: почему допускает доказательство, а не проверка в сервисе
examples/spec/shipment-guard/guard.mjs показывает, как выглядит допуск
команды на практике. Функция shipmentTheorem(orderNumber) строит текст
теоремы, decideShipment компилирует его и вызывает prove:
export function decideShipment(orderNumber, context) {
const source = shipmentTheorem(orderNumber)
try {
const proof = prove(compile(source), context)
return { allowed: true, source, proof }
} catch (error) {
return { allowed: false, source, reason: error.message, diagnostics: error?.diagnostics ?? [] }
}
}
Разница с обычным if (order.readyToShip) ship() в том, что здесь решение —
не булево значение внутри сервиса, а результат независимого доказательства на
переданном снимке данных. Запуск на реальной модели заказа:
node examples/spec/shipment-guard/guard.mjs ЗК-7781
даёт "allowed": true вместе с деривацией пути заказы[номер="ЗК-7781"].готов к отгрузке. Если склад не подтвердил приёмку, тот же номер заказа с другим
context:
node examples/spec/shipment-guard/guard.mjs ЗК-7781 --blocked
возвращает "allowed": false и диагностику FTS_WITNESS_MISMATCH с точным
путём и сравнением expected true, got false. Процесс завершается кодом 1,
поэтому guard пригоден и для shell, и для CI, и для агентского пайплайна: ни
объяснение агента, ни текст ответа не могут заменить эту проверку.
Отдельная деталь: номер заказа перед подстановкой в текст теоремы проверяется
регулярным выражением /^[\p{L}\p{N}-]{1,32}$/u. Без этой проверки кавычки
внутри значения могли бы сломать разбор и подменить формулировку теоремы —
guard.test.mjs отдельно проверяет, что такой номер отклоняется до
компиляции.
Сертификат или proof не заменяет optimistic lock. Между снимком данных и записью состояние может измениться, поэтому транзакционная граница и version check остаются обязанностью приложения, а не FTS.
Где проходит граница агрегата
FTS не хранит состояние и не управляет параллельным доступом. Модель
описывает факты и правило перехода; application service собирает снимок
агрегата, вызывает prove/verify и только при успехе выполняет команду.
Размещайте .fts рядом с bounded context, а не в общей папке правил:
src/order-execution/
domain/
application/
policies/
shipment.fts
shipment.context.schema.json
Версионируйте изменения политики как код: pull request с правкой .fts
должен показывать изменённое правило, изменённые примеры и влияние на
сгенерированные артефакты.
Практика в песочнице
Замените номер заказа в теореме и context на несуществующий и посмотрите на диагностику.
Типичные ошибки
FTS_NATURAL_DECLARATION— блок начинается не собъект,структура,морфизм,теоремаилиутилита». Опечатка вродесущность Покупка` ловится компилятором до валидации типов.FTS_WITNESS_MISMATCH— факт, на который ссылается теорема, не совпадает со значением в context (пример выше с--blocked). Это основной способ, которым command guard отказывает в допуске.- Плохой, но не диагностируемый компилятором антипаттерн: морфизм, который
«доказывает» закон только потому, что он объявлен в
.fts, без реального бизнес-обоснования у эксперта предметной области. Компилятор проверяет типовую согласованность, а не то, верен ли закон в реальности.
Упражнение
Возьмите команду с реальным бизнес-отказом из своего проекта — approve,
refund, publish или deploy. Опишите минимальный факт допуска, один
морфизм и конкретный snapshot, на котором prove должен и не должен
проходить. Отдельно, текстом вне .fts, перечислите технические условия
(лок, идемпотентность запроса, ретраи), которые остаются вне модели.
Чек-лист
- Категория покрывает один bounded context, а не всю систему.
- Имена полей и состояний совпадают с языком предметного эксперта дословно.
- Морфизм описывает переход факта в факт, а не вызывает эффект.
- Command guard использует
prove/certify+verifyна актуальном снимке, а не хранит «уже посчитанный» булев флаг. - Optimistic lock и транзакционная граница остаются в application service.
.ftsлежит рядом с bounded context и ревьюится как код.