Ubiquitous language
Ubiquitous language — общий словарь разработчиков и доменных экспертов внутри одного bounded context. Модель И ЕСТЬ язык: дрейф имён сигнализирует о неверной границе или отсутствующем концепте.
Sales-команда B2B-платформы называла это «Quote». В коде это было ProposalDraft. Таблица в базе данных называлась pending_orders. В продуктовой спецификации — «Pre-Order». CRM-плагин генерировал события DealCreated. Пять имён для одного бизнес-концепта — и каждый перевод открывал возможность для недопонимания. Разработчик, читающий баг-репорт о «квоте, которую не отправили», искал в коде Quote, ничего не находил, в конце концов обнаруживал ProposalDraft, пытался понять, не одно ли это с «Quote», читал спецификацию — там было «Pre-Order», находил таблицу pending_orders и в итоге спрашивал коллегу. Тридцать минут на поиск концепта. На той же неделе продакт-менеджер изменил правило: «Quote истекает через 30 дней». Разработчик поправил логику истечения в ProposalDraft. Но CRM-интеграция слушала событие DealCreated со своим автоматом состояний и не знала, что у ProposalDraft есть срок. Когда сделка истекала в коде, CRM по-прежнему показывала её как открытую. На ревью никто не заметил: ревьюер думал в терминах «Quote», а код был в терминах ProposalDraft. Термин Эванса для починки обманчиво прост: «ubiquitous language». Но простота вводит в заблуждение. Это не соглашение об именовании. Это обязательство: язык, на котором говорят на встречах с доменными экспертами, — это в точности тот язык, который записан в коде, базе данных, событиях, тестах и документации — без слоя перевода нигде.
Что ubiquitous language означает на самом деле
«Ubiquitous» означает «встречающийся повсюду». Суть позиции Эванса: язык должен присутствовать — без перевода — в:
- Разговорах между разработчиками и доменными экспертами
- Коде: именах классов, методов, переменных, событий
- Схеме базы данных: именах таблиц, столбцов
- Тестах: описаниях тестов, именах тестовых данных
- Документации и спецификациях
- Метках пользовательского интерфейса (там, где они отражают доменные концепты)
Когда слово из разговора с доменным экспертом отличается от слова в коде, на границе каждого разговора происходит перевод. Переводы порождают ошибки. Они также маскируют проблемы модели: разработчик, использующий ProposalDraft, перестаёт задаваться вопросом, применимы ли правила «Quote» доменного эксперта, — потому что имена не совпадают.
Ubiquitous language не изобретается разработчиками и не объясняется потом доменным экспертам. Он вырабатывается совместно. Доменные эксперты используют естественный язык неточно: «заказ подан» может означать многое. Процесс построения ubiquitous language заставляет обе стороны быть точными. «Заказ подан» становится: «Sales Order переходит в состояние SubmittedForApproval, когда менеджер нажимает Confirm и все обязательные позиции имеют согласованную цену». Эта точность входит в код как имя метода (submitForApproval()), значение enum (OrderStatus.SUBMITTED_FOR_APPROVAL) и доменное событие (SalesOrderSubmittedForApproval).
▸Почему это работает
Почему «модель И ЕСТЬ язык», а не «модель реализует язык»? Потому что если между ними есть разрыв — язык говорит «Quote», а модель говорит ProposalDraft — у вас два артефакта для поддержки. Каждый раз, когда язык эволюционирует (доменные эксперты открывают новый концепт или уточняют существующий), нужно обновлять и модель, и языковой артефакт. Когда они одно и то же, открытие концепта немедленно становится переименованием класса, новым методом или новым доменным событием. Именно поэтому Эванс настаивает на «ubiquitous» — «вездесущем», а не «согласованном» или «задокументированном»: он должен быть везде, а не в глоссарии, живущем отдельно от кода.
Модель как язык: как это выглядит в коде
В B2B-платформе Sales-контекст вырабатывает ubiquitous language вокруг концептов: Quote, LineItem, NegotiatedPrice, AccountExecutive, QuoteExpiry, QuoteSubmission. Код отражает это буквально:
- Класс
Quoteс методамиaddLineItem(),negotiate(),submitForApproval(),expire() - Value object
NegotiatedPrice(неBigDecimalс комментарием) - Доменное событие
QuoteSubmittedForApproval(неStatusUpdatedс полем в payload) - Репозиторий
QuoteRepositoryс методомfindExpiringBefore(date: LocalDate)
Ни одно из этих имён не требует словаря. Доменный эксперт, читающий тест quote_expires_when_no_approval_within_30_days, понимает его немедленно. Когда доменный эксперт меняет правило на «Quote от enterprise-клиентов истекает через 60 дней», разработчик точно знает, какой класс и какой метод менять.
Дрейф имён как сигнал
Самое ценное в ubiquitous language — не то, что он говорит, когда работает, а то, что он говорит, когда ломается. Когда язык начинает дрейфовать, с моделью что-то не так.
Сигнал 1: слову начинают требоваться уточнения. Когда разработчики начинают говорить «Sales Order» и «Billing Order», чтобы различать две вещи, которые оба называют «Order» — они обнаружили, что «Order» означает разное в двух местах. Это сигнал о границе bounded context, а не проблема именования. Решение — не переименовать оба варианта, а поместить каждый в свой контекст, где «Order» однозначно обозначает концепт этого контекста.
Сигнал 2: один концепт получает разные имена в разных файлах. ProposalDraft в доменной модели, Quote в сервисном слое, pending_order в базе данных. Это провал ubiquitous language: модель не совпадает с языком, на котором реально говорит команда.
Сигнал 3: доменные эксперты используют слово, которого нет в коде. Если доменный эксперт говорит об «approval escalation», а в коде нет класса ApprovalEscalation, нет метода escalate(), нет события QuoteEscalated — этот концепт живёт только в головах экспертов и в куче условий, разбросанных по сервисным методам. Модель упускает концепт, который реально существует в домене.
▸lesson.inset.note
Ubiquitous language ограничен одним bounded context. Язык Billing-контекста отделён от языка Sales-контекста — намеренно. «Customer» в Sales означает нечто иное, чем в Billing, — и это нормально. Каждый контекст имеет свой точный язык. Ошибка — пытаться построить один унифицированный язык для всех контекстов: это порождает ту же проблему God-объекта на 47 полей, но на уровне словаря. Язык внутри контекста точен; перевод между контекстами явен.
Рефакторинг к языку
Команда, у которой есть разрыв ubiquitous language, имеет задачу в бэклоге: переименовать ProposalDraft в Quote, переименовать pending_orders в quotes, переименовать события StatusUpdated в QuoteSubmittedForApproval и QuoteExpired. Это не косметика. Каждое переименование — исправление модели, снижающее стоимость перевода для каждого будущего разговора с доменным экспертом.
Дисциплина такова: когда доменный эксперт использует новый термин в разговоре — следующий шаг добавить его в модель, не в глоссарий, не в комментарий, а в код. Если термин раскрывает отсутствующий концепт — создать класс. Если раскрывает отсутствующий переход состояния — добавить метод. Если раскрывает отсутствующее событие — добавить тип события. Модель и язык эволюционируют вместе.
Разработчик переименовывает класс `ProposalDraft` в `Quote` после разговора с доменным экспертом. Тимлид возражает: «Это просто косметическое переименование — оно не добавляет бизнес-ценности и рискует внести баги». Что не так с этой формулировкой?
На обзоре спринта доменный эксперт говорит: «Когда крупный enterprise-клиент размещает заказ, мы сначала делаем кредитную проверку, а для мелких клиентов просто одобряем автоматически». Разработчик смотрит в код и находит `if (customer.tier == 'ENTERPRISE') { creditService.check(order); }`, разбросанный по трём сервисным методам. Что это говорит о модели?
У команды два bounded context: Sales и Billing. В Sales концепт называется «Quote» с жизненным циклом Draft → Submitted → Approved → Expired. В Billing тот же документ называется «Invoice» с жизненным циклом Pending → Sent → Paid → Overdue. Новый разработчик говорит: «Это явно один и тот же документ — нужно объединить в одну модель, чтобы не дублировать». Почему это неверно?
- 01Что означает «ubiquitous» в «ubiquitous language» и почему важен именно этот выбор слова?
- 02Каковы три сигнала дрейфа ubiquitous language и что каждый из них означает?
- 03Почему ubiquitous language должен быть ограничен одним bounded context?
Пять имён для одного концепта из вступительной истории — Quote, ProposalDraft, pending_order, Pre-Order, DealCreated — были не провалом соглашения об именовании. Это был провал модели: общий язык не был согласован, поэтому каждая команда изобрела свой термин, и каждая точка интеграции требовала мысленного перевода.
Ubiquitous language решает это, делая словарь доменного эксперта словарём кода — без перевода нигде. Имена классов, методов, событий, описания тестов и столбцы баз данных используют те же термины, что появляются в разговорах с доменными экспертами. Когда доменное правило меняется, разработчик немедленно находит нужный код, потому что имена совпадают.
Язык — мощный диагностический инструмент. Когда слово требует уточнений — граница bounded context стоит не там. Когда один концепт имеет разные имена в разных файлах — модель не совпадает с пониманием команды. Когда термин живёт в разговорах с экспертами, но не в коде — в модели отсутствует концепт.
Следующий урок рассматривает, как применять согласованность модели внутри bounded context — в частности, паттерн агрегата, определяющего транзакционную границу вокруг кластера связанных объектов.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.