Перейти к содержимому
Программа DDDDDD.1 Предметная область и единый язык

Единый язык: говорить и писать код словами предметной области

Что такое единый язык DDD и где он должен звучать — от разговора с экспертом до имён классов, событий и тестов; как вырабатывать его через интервью, примеры и глоссарий; как узнать синонимы, жаргон и двусмысленные термины. Главный артефакт урока — глоссарий онлайн-школы, на котором стоит вся программа.

УРОКddd/ddd-1-language/02-ubiquitous-language.mdx2 чУрок 2 из 3
После урока вы сможете
  • Объяснить, что такое единый язык, где он должен звучать и почему изменение языка — это изменение кода
  • Вырабатывать язык вместе с экспертами — интервью, примеры с числами, карта примеров — и вести глоссарий домена рядом с кодом
  • Находить синонимы, технический жаргон и термины с двумя смыслами и переписывать код на единый язык
  • Пользоваться глоссарием онлайн-школы — понятиями, статусами, действиями, событиями и правилами Набора

Методист онлайн-школы приходит к разработчику:

— Курс по Python стартует первого ноября. Если человек забронировал место и за двое суток не оплатил, бронь сгорает.

— Понял, — отвечает разработчик. — Крон будет удалять неоплаченные заказы старше 48 часов.

Оба уверены, что договорились. Но «курс» у методиста — конкретный запуск с датой старта, а у разработчика в базе courses — описание курса без дат. «Бронь сгорает» для методиста значит «место освобождается», а разработчик удаляет строку — и вместе с ней историю. Через месяц поддержка не может ответить студенту, записывался ли он вообще.

Такие недопонимания стоят дорого, потому что их не видно. Каждый говорит на своём языке и переводит в голове, а при переводе смысл теряется. В уроке DDD.1.1 мы видели, как одно правило о возврате, понятое двумя разработчиками по-разному, дало студентам разные деньги. Лекарство DDD — единый языкединый языкEN ubiquitous languageОбщий язык экспертов и разработчиков, построенный на модели, который используют везде: в разговорах, документах, именах классов и методов, событиях и тестах. Изменение языка — это изменение модели и кода. Встречаются переводы «вездесущий» и «повсеместный язык».: одни и те же слова у экспертов, в документах и в коде.

В этом уроке мы заведём глоссарий доменаглоссарий доменаEN domain glossaryЖивой документ рядом с кодом: термин единого языка, его имя в коде, определение, пример и запрещённые синонимы. Меняется в одном коммите с кодом. онлайн-школы. Им будут пользоваться все следующие уроки программы DDD и программы SDD, поэтому читайте его внимательно — к нему придётся возвращаться.

Эрик Эванс описывает единый язык (ubiquitous language) так: возьмите модель предметной области за основу языка и договоритесь всей командой говорить на нём везде — в разговорах, документах, диаграммах и в коде. Английское ubiquitous значит «вездесущий»; в русских текстах встречаются переводы «единый», «вездесущий» и «повсеместный язык». Мы говорим «единый».

Единый язык — не словарь бизнеса «как есть». Эксперты часто говорят расплывчато: «бронь», «заявка», «запись» и «заказ» у них могут означать одно и то же, а могут — разное. Работа команды — договориться о точных словах. Эванс распределяет роли так: эксперты возражают против терминов, которые неудобны или неверно передают смысл, разработчики ищут двусмысленности и противоречия, которые потом сломают код. Язык вырабатывают вместе, и он меняется вместе с моделью.

ГдеНа едином языкеБез единого языка
Разговор«запись ожидает оплаты»«заказ в нулевом статусе»
Задача в трекере«отменять неоплаченную запись через 48 часов»«крон чистит orders»
КлассEnrollment, CohortOrder, UserGroupLink
Методcohort.enroll(student_id, now)manager.process(data)
СобытиеEnrollmentCancelledorder_status_changed
Тестtest_enrollment_is_refused_when_no_free_seatstest_case_7
Ошибка«На потоке python-2026-11 нет свободных мест»IntegrityError, err: no_slots
Промпт ИИ-агенту«добавь отмену записи на поток по правилам Набора»«сделай отмену заказа»

Имя теста из третьего столбца ничего не говорит о бизнесе, а имя из второго — готовое правило. Авторы «Cosmic Python» формулируют проверку так: имена тестов и классов должны быть взяты из языка бизнеса, чтобы код можно было показать коллеге без технического образования, и он подтвердил бы, что поведение описано верно.

Слова языка и есть модель. Если эксперты начинают различать то, что раньше не различали, — например, «счёт» и «платёж», — модель изменилась, и код должен измениться вместе с ней. Если бизнес переименовал «поток» в «группу» по серьёзной причине, переименовывают всё: глоссарий, классы, события, тесты. «Просто помнить, что Cohort — это теперь группа» нельзя: через полгода в команде будут люди, которые этого не помнят.

Единый язык однозначен только внутри своей области. В Наборе человек, который учится, — «студент». В Уведомлениях тот же человек — «получатель письма», и его там интересует только адрес. В Оплате говорят о «плательщике» — им может быть и компания, которая оплачивает обучение сотрудника. Попытка заставить все части школы говорить одним словарём ломает язык: слова начинают значить слишком много. Границы, внутри которых язык однозначен, называются ограниченными контекстами — это тема урока DDD.2.1. Глоссарий ниже — язык Набора и соседних частей школы в том объёме, который нужен программе.

ТерминВ кодеЧто этоПример, правилаНе говорим
КурсCourseучебный продукт: тема, программа занятий, цена«Python для аналитиков». Курс сам не стартует — стартуют его потоки«продукт», «товар»
ПотокCohortконкретный запуск курса: дата старта и вместимость«Python для аналитиков, старт 2 ноября, 30 мест»«группа», «курс» в смысле запуска
СтудентStudentчеловек, который записывается на потоки и учитсяодин студент может учиться на нескольких потоках«пользователь», «клиент», «покупатель»
Запись на потокEnrollmentфакт, что студент занимает место в потоке; у записи есть статусстатусы — в таблице ниже«заказ», «бронь», «заявка», «покупка»
Лист ожиданияWaitlistочередь студентов на поток без свободных месткто раньше встал, тому раньше предложат место«очередь», «резерв»
СчётInvoiceтребование оплатить запись: сумма и сроквыставляется, когда студент записался«оплата», «чек», «заказ»
ПлатёжPaymentфакт, что деньги по счёту поступили через внешнюю платёжную системусчёт может быть выставлен, а платежа ещё нет«оплата», «транзакция»
СертификатCertificateдокумент о прохождении курсавыдаётся, когда студент выполнил условия«диплом»
ДеньгиMoneyсумма в копейках вместе с валютой30 000 руб. — это 3 000 000 копеек в валюте RUBчисло с плавающей точкой без валюты
ПериодPeriodинтервал времени: начало и конецдаты обучения потокадве отдельные даты «с» и «по»

Обратите внимание на «оплату»: в разговоре это слово означает то счёт («оплата создана»), то платёж («оплата прошла»). Поэтому в глоссарии его нет вовсе — вместо него два точных термина. Деньги хранятся в копейках целым числом: так не возникает ошибок округления вроде 0.1+0.2=0.300000000000000040.1 + 0.2 = 0.30000000000000004. Как устроены Money и Period в коде, разберём в уроке DDD.3.1.

СтатусВ кодеЗанимает место?Когда наступает
ожидает оплатыPENDING_PAYMENTдасразу после записи
подтвержденаCONFIRMEDдаплатёж по счёту получен
отмененаCANCELLEDнетстудент отменил запись или 48 часов прошли без оплаты
stateDiagram-v2
[*] --> PENDING_PAYMENT : записать
PENDING_PAYMENT --> CONFIRMED : платёж получен
PENDING_PAYMENT --> CANCELLED : отмена или 48 ч без оплаты
CONFIRMED --> CANCELLED : отмена студентом
CANCELLED --> [*]
Жизненный цикл записи на поток: ожидает оплаты → подтверждена; из обоих статусов возможна отмена

Действия — глаголы, это команды «сделай». События — факты в прошедшем времени, «это случилось». В названии события всегда прошедшее время: событие нельзя отменить, можно только совершить новое действие.

ДействиеВ кодеСобытиеИмя события
записать студента на потокenrollСтудентЗаписанStudentEnrolled
подтвердить записьconfirmЗаписьПодтвержденаEnrollmentConfirmed
отменить записьcancelЗаписьОтмененаEnrollmentCancelled
освободить местоreleaseМестоОсвободилосьSeatReleased
получить платёж (делает платёжная система)receiveПлатёжПолученPaymentReceived
выдать сертификатissueСертификатВыданCertificateIssued
предложить место из листа ожиданияoffer——
вернуть деньгиrefund——

Свойства тоже договорены: вместимость — capacity, место — seat, старт потока — starts_at, сумма — amount, валюта — currency, статус — status. Для двух последних действий событий пока нет: понадобятся ли они, решит Event Storming в уроке DDD.2.3.

Числа — условия учебного примера, а не отраслевой стандарт.

  1. Записей со статусами «ожидает оплаты» и «подтверждена» не больше вместимости потока.
  2. Студент не может записаться на один поток дважды.
  3. Запись на поток закрывается в момент старта потока.
  4. Неоплаченная запись отменяется через 48 часов после записи.
  5. При отмене за 7 суток (7 × 24 часа) и более до старта потока — полный возврат, позже — без возврата.
  6. Освободившееся место предлагается первому в листе ожидания (кто раньше встал), у него 24 часа на подтверждение.

У глоссария всегда есть открытые вопросы — то, что эксперты ещё не решили. Для школы, например: может ли студент, отменивший запись, снова записаться на тот же поток? Что происходит, если платёж пришёл через минуту после автоматической отмены? Что именно подтверждает студент из листа ожидания за 24 часа? Их записывают прямо в глоссарий, чтобы не додумать ответ за экспертов. Часть из них закроем в модулях DDD.2 и DDD.3.

Глоссарий — файл в репозитории рядом с кодом, например docs/glossary.md, а не страница в забытой вики. Правило простое: изменение термина и переименование в коде идут в одном коммите и проходят одно ревью. Одна запись выглядит так:

## Запись на поток (Enrollment)
**Что это:** факт, что студент занимает место в потоке.
**Статусы:** ожидает оплаты (PENDING_PAYMENT), подтверждена (CONFIRMED), отменена (CANCELLED).
**Правила:** занимают место только «ожидает оплаты» и «подтверждена»; неоплаченная отменяется
через 48 часов после записи.
**Пример:** Анна записалась на поток «Python для аналитиков, 2 ноября» 1 октября в 10:00.
**Не говорим:** заказ, бронь, заявка, покупка.
**Контекст:** Набор.
**Открытые вопросы:** можно ли записаться снова после отмены?

Похожее поле есть и в канве ограниченного контекста от DDD Crew: раздел «Ubiquitous Language» — ключевые термины контекста и их значения. С этой канвой вы поработаете в уроке DDD.2.1.

Эксперты предметной областиэксперт предметной областиEN domain expertЧеловек, который глубоко знает бизнес-процессы и их правила: методист, менеджер набора, бухгалтер, опытный сотрудник поддержки. Необязательно начальник. школы — методист, менеджер набора, бухгалтер, старший сотрудник поддержки. Несколько правил разговора:

  • Просите истории, а не определения. «Расскажите про последний случай, когда студент отменял запись» даёт больше, чем «как у вас устроены отмены?».
  • Записывайте точные слова. Не переводите сразу в свои термины: «бронь сгорает» — ценная цитата, даже если в глоссарий попадёт другое слово.
  • Уточняйте различия: «чем запись отличается от брони?», «счёт и оплата — одно и то же?».
  • Ищите края: «а если платёж пришёл в последнюю минуту?», «а если поток перенесли?».
  • Пересказывайте на языке модели: «правильно ли я понял: запись в статусе „ожидает оплаты“ занимает место?» Если эксперт поправляет — модель стала точнее.

Фрагмент такого интервью:

— Как человек попадает на поток?

— Записывается на сайте. Место за ним держится двое суток, пока он не оплатит.

— А если не оплатил?

— Бронь сгорает, место отдаём тому, кто ждёт.

— Кто ждёт — это кто?

— Ну, лист ожидания. Кто раньше встал, тому и предлагаем. У него сутки, чтобы решить.

— А если и он не решил?

— Следующему. Хм, а это у нас где-то записано?..

Из одного разговора — пять кандидатов в глоссарий («поток», «место», «лист ожидания», «отмена» вместо «бронь сгорает», «предложить место») и два правила с числами. А последняя реплика эксперта — частый и очень ценный результат: правило существовало только в головах.

Общая формулировка «неоплаченная запись отменяется через 48 часов» кажется ясной, пока не начнёшь подставлять числа. Подход «спецификация на примерах»спецификация на примерахEN specification by exampleСпособ описывать правила конкретными примерами с числами и ожидаемым результатом вместо общих фраз (книга Гойко Аджича, 2011). Примеры вскрывают неясности и затем становятся тестами. (specification by example — так называется книга Гойко Аджича, 2011) предлагает описывать правила конкретными примерами с ожидаемым результатом:

Поток (вместимость)Что уже естьЧто происходитОжидаемый результат
2 местаАнна — ожидает оплаты, Борис — подтвержденаВера записываетсяотказ: мест нет; Вере предлагают лист ожидания
2 местаАнна записалась 1 октября в 10:00, не оплатиланаступает 3 октября 10:00запись Анны отменена, место освободилось
2 местато же, Вера первая в листе ожиданияместо освободилосьВере предложено место, у неё 24 часа
старт 2 ноября 10:00мест многоГлеб записывается 2 ноября в 10:00отказ: запись закрыта

Каждая строка — вопрос к эксперту, на который нельзя ответить «ну, как обычно». Вторая строка сразу рождает новый: а если платёж Анны придёт 3 октября в 10:05? Вернуть деньги или восстановить запись? Пока ответа нет, это открытый вопрос в глоссарии. Позже такие примеры почти дословно становятся тестами.

Удобный формат встречи — карта примеровкарта примеровEN Example MappingКороткая встреча по одной истории (около 25 минут): карточки правил, примеров к каждому правилу и вопросов, на которые пока нет ответа. Технику предложил Мэтт Уинн из сообщества BDD. (Example Mapping) Мэтта Уинна. На стол кладут карточку истории («студент записывается на поток»), под ней — карточки правил, под каждым правилом — карточки примеров, а сбоку — карточки вопросов, на которые никто в комнате не может ответить. Одна история — одна короткая встреча. Много карточек вопросов значит, что историю рано отдавать в разработку.

Примеры можно записывать и в формате «Дано / Когда / Тогда» (его подробно разберёт урок SDD.2.1):

Сценарий: неоплаченная запись отменяется через 48 часов
Дано поток «Python для аналитиков» со стартом 2 ноября и вместимостью 2
И Анна записалась на поток 1 октября в 10:00 и не оплатила счёт
Когда наступает 3 октября 10:00
Тогда запись Анны отменена
И на потоке 2 свободных места
ПризнакКак выглядит в школеЧем опасенЧто делать
СинонимыOrder в одном модуле, Booking в другом, Enrollment в третьем; в разговоре — «группа» и «поток»люди думают, что это разные вещи; поиск по коду находит треть мествыбрать одно слово, остальные внести в «не говорим», переименовать
Технический жаргонOrderManager.process(data), st == 2, flag, handle_itemэксперт не может прочитать код; правило спрятано за техникойглаголы и существительные домена: cohort.enroll, EnrollmentStatus.CANCELLED
Один термин в двух смыслах«курс» — и описание курса, и его запуск; «оплата» — и счёт, и платёжбаги на стыке: доступ к занятиям открыли, когда счёт выставили, а не когда пришли деньгиразвести на два термина: курс и поток, счёт и платёж
Слово без определения«активный студент», «VIP», «скидка» в коде, которых нет в глоссариикаждый понимает по-своемуспросить экспертов: новое понятие — в глоссарий, случайное — убрать
Переводчик в головена встрече одни слова, в коде другиекаждое обсуждение требует перевода, ошибки перевода незаметныязык один — и в разговоре, и в коде

Последний признак легко проверить на себе: если, рассказывая эксперту о коде, вы переводите («ну, Order — это ваша запись»), языка ещё нет.

Переписываем фрагмент кода на единый язык

Заголовок раздела «Переписываем фрагмент кода на единый язык»

Вот фрагмент записи на поток из «старой» школы. Логика здесь простая, но прочитать её может только автор:

class OrderManager:
"""Работа с заказами."""
def __init__(self):
self.orders = []
def process(self, user, group, data):
taken = [o for o in self.orders if o["group"] == group["id"] and o["st"] != 2]
if data["now"] >= group["start"]:
return {"ok": False, "err": "closed"}
if len(taken) >= group["max"]:
return {"ok": False, "err": "no_slots"}
self.orders.append({"user": user, "group": group["id"], "st": 0, "ts": data["now"]})
return {"ok": True}

Та же логика на едином языке. Enum из стандартной библиотеки — это перечисление, фиксированный набор именованных значений: EnrollmentStatus.CANCELLED читается лучше, чем 2, а опечатку в имени Python поймает сразу. Классы с @dataclass и исключения вы знаете по урокам 0.2.3 и 0.2.4.

from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
class EnrollmentStatus(Enum):
"""Статус записи на поток — ровно три, как в глоссарии."""
PENDING_PAYMENT = "ожидает оплаты"
CONFIRMED = "подтверждена"
CANCELLED = "отменена"
class NoFreeSeats(Exception):
"""На потоке нет свободных мест."""
class EnrollmentClosed(Exception):
"""Запись на поток закрыта: поток уже стартовал."""
@dataclass
class Enrollment:
"""Запись на поток."""
student_id: str
enrolled_at: datetime
status: EnrollmentStatus = EnrollmentStatus.PENDING_PAYMENT
@dataclass
class Cohort:
"""Поток курса: дата старта и вместимость."""
cohort_id: str
starts_at: datetime
capacity: int
enrollments: list = field(default_factory=list)
def free_seats(self):
"""Свободные места: вместимость минус записи «ожидает оплаты» и «подтверждена»."""
taking_seats = [e for e in self.enrollments if e.status != EnrollmentStatus.CANCELLED]
return self.capacity - len(taking_seats)
def enroll(self, student_id, now):
"""Записать студента на поток."""
if now >= self.starts_at:
raise EnrollmentClosed(f"Запись на поток {self.cohort_id} закрыта: поток стартовал "
f"{self.starts_at:%d.%m.%Y %H:%M}")
if self.free_seats() == 0:
raise NoFreeSeats(f"На потоке {self.cohort_id} нет свободных мест")
enrollment = Enrollment(student_id, enrolled_at=now)
self.enrollments.append(enrollment)
return enrollment
cohort = Cohort("python-2026-11", starts_at=datetime(2026, 11, 2, 10, 0), capacity=2)
now = datetime(2026, 10, 1, 12, 0)
anna = cohort.enroll("anna", now)
cohort.enroll("boris", now)
print("Анна:", anna.status.value, "| свободных мест:", cohort.free_seats())
try:
cohort.enroll("vera", now)
except NoFreeSeats as error:
print("Вера:", error)
anna.status = EnrollmentStatus.CANCELLED # пока вручную; правила отмены — в модуле DDD.3
print("Анна отменила запись | свободных мест:", cohort.free_seats())
try:
cohort.enroll("gleb", datetime(2026, 11, 2, 10, 0))
except EnrollmentClosed as error:
print("Глеб:", error)
Анна: ожидает оплаты | свободных мест: 0
Вера: На потоке python-2026-11 нет свободных мест
Анна отменила запись | свободных мест: 1
Глеб: Запись на поток python-2026-11 закрыта: поток стартовал 02.11.2026 10:00

Что поменялось при переписывании:

  • OrderManager, orders, group → Cohort, enrollments, Enrollment — слова глоссария вместо синонимов;
  • process(user, group, data) → cohort.enroll(student_id, now) — глагол домена вместо жаргона, и сразу видно, кто что делает;
  • st != 2 → status != EnrollmentStatus.CANCELLED, max → capacity, no_slots → исключение NoFreeSeats с русским текстом, понятным и поддержке;
  • докстринги пересказывают правила словами методиста.

Строка anna.status = EnrollmentStatus.CANCELLED — честно помеченная временная мера: кто угодно может поставить любой статус в обход правил. Как сделать, чтобы правила нельзя было нарушить, — тема агрегатов в уроке DDD.3.2.

Тесты тоже пишутся на едином языке — имя теста формулирует правило, докстринг повторяет его по-русски:

from datetime import datetime
import pytest
from enrollment import Cohort, EnrollmentClosed, NoFreeSeats
STARTS_AT = datetime(2026, 11, 2, 10, 0)
def test_enrollment_is_refused_when_no_free_seats():
"""Записей «ожидает оплаты» и «подтверждена» не больше вместимости потока."""
cohort = Cohort("python-2026-11", STARTS_AT, capacity=1)
cohort.enroll("anna", datetime(2026, 10, 1, 12, 0))
with pytest.raises(NoFreeSeats):
cohort.enroll("boris", datetime(2026, 10, 1, 13, 0))
def test_enrollment_closes_when_cohort_starts():
"""Запись закрывается в момент старта потока."""
cohort = Cohort("python-2026-11", STARTS_AT, capacity=30)
with pytest.raises(EnrollmentClosed):
cohort.enroll("anna", STARTS_AT)

Если сохранить код записи в enrollment.py, а тесты — в test_enrollment.py рядом, pytest -q покажет 2 passed. Список имён тестов при этом читается как перечень правил Набора — его можно отправить методисту на проверку.

Глоссарий полезен, только если код ему следует. Следить за этим глазами на ревью утомительно, поэтому часть проверки можно автоматизировать. В задании вы напишете маленький «линтер языка»: он разбивает имена классов, методов, полей и констант на слова, сверяет каждое слово с глоссарием и находит синонимы, жаргон и слова, которых в глоссарии нет. Синонимы он умеет исправлять сам, а жаргон оставляет человеку: переименовать OrderManager.process осмысленно может только тот, кто понимает, что этот метод делает.

Функция public_names уже готова: она разбирает исходный код модулем ast из стандартной библиотеки, не выполняя его, и возвращает публичные имена. Вам нужно написать split_words, classify_word, to_domain_name и audit.

Линтер единого языка: синонимы, жаргон и глоссарий8 тестов
import ast
import re  # понадобится в split_words

# Глоссарий домена онлайн-школы: русский термин → имя в коде (урок DDD.1.2)
GLOSSARY = {
    "Курс": "Course",
    "Поток": "Cohort",
    "Студент": "Student",
    "Запись на поток": "Enrollment",
    "Лист ожидания": "Waitlist",
    "Счёт": "Invoice",
    "Платёж": "Payment",
    "Сертификат": "Certificate",
    "Деньги": "Money",
    "Период": "Period",
}
GLOSSARY_WORDS = {name.lower() for name in GLOSSARY.values()}   # все имена — из одного слова

# Свойства, действия и статусы, о которых договорились с экспертами (тоже из глоссария урока)
DOMAIN_WORDS = {
    "capacity", "seat", "start", "starts", "end", "ends", "status", "amount", "kopecks", "currency",
    "enroll", "confirm", "cancel", "release", "offer", "issue", "receive", "refund",
    "pending", "confirmed", "cancelled", "enrolled", "released", "issued", "received",
}
# Служебные слова, которые можно использовать в любых именах
COMMON_WORDS = {"id", "at", "by", "to", "of", "for", "from", "is", "can", "has", "max", "min",
                "count", "total", "free", "default", "days", "hours"}

# Синонимы, которыми эксперты НЕ говорят, → слово глоссария
SYNONYMS = {
    "order": "enrollment", "booking": "enrollment", "purchase": "enrollment", "registration": "enrollment",
    "group": "cohort", "stream": "cohort",
    "user": "student", "client": "student", "customer": "student", "learner": "student",
    "bill": "invoice",
    "transaction": "payment",
    "diploma": "certificate",
    "queue": "waitlist",
    "slot": "seat", "place": "seat",
    "product": "course",
}

# Технический жаргон: эксперт не поймёт, о чём речь
JARGON = {"data", "info", "item", "obj", "record", "row", "entity", "manager", "helper", "handler", "handle",
          "process", "proc", "do", "flag", "util", "utils", "tmp", "st", "dt", "ts", "trx", "usr", "grp", "calc"}

KIND_OK = "в глоссарии"
KIND_SYNONYM = "синоним"
KIND_JARGON = "жаргон"
KIND_UNKNOWN = "нет в глоссарии"

TECHNICAL_CODE = '''
MAX_USERS = 30

class Group:
    """Группа курса."""
    start_dt: str
    max_users: int

    def free_slots(self):
        ...

class OrderManager:
    """Всё про заказы."""

    def process(self, user, group, data):
        ...

    def cancel_order(self, order_id):
        ...

def handle_payment_data(trx):
    ...

def calc_discount(usr):
    ...
'''

DOMAIN_CODE = '''
DEFAULT_CAPACITY = 30

class Cohort:
    """Поток курса: дата старта и вместимость."""
    starts_at: str
    capacity: int

    def free_seats(self):
        ...

    def enroll(self, student_id):
        ...

class Enrollment:
    """Запись на поток."""
    status: str

    def confirm(self):
        ...

    def cancel(self, cancelled_at):
        ...

def receive_payment(payment):
    ...
'''


def split_words(name):
    """Слова идентификатора в нижнем регистре.

    snake_case и UPPER_CASE делим по «_», CamelCase — по заглавным буквам:
    "EnrollmentStatus" → ["enrollment", "status"], "MAX_USERS" → ["max", "users"],
    "starts_at" → ["starts", "at"]. Пустые куски (от «_» в начале имени) пропускаем.
    """
    # TODO: ваш код
    ...


def classify_word(word):
    """Вид слова — одна из констант KIND_OK, KIND_SYNONYM, KIND_JARGON, KIND_UNKNOWN.

    1) Приведите слово к нижнему регистру.
    2) Слово есть в GLOSSARY_WORDS, DOMAIN_WORDS или COMMON_WORDS → KIND_OK;
       в SYNONYMS → KIND_SYNONYM; в JARGON → KIND_JARGON.
    3) Не нашли, а слово оканчивается на «s», — повторите шаг 2 для слова без «s»
       (множественное число: users → user, seats → seat).
    4) Не нашли нигде → KIND_UNKNOWN.
    """
    # TODO: ваш код
    ...


def to_domain_name(name):
    """Заменить в имени синонимы словами глоссария, сохранив стиль имени.

    Каждое слово из split_words: синоним → SYNONYMS[слово]; синоним во множественном
    числе (orders) → слово глоссария + «s» (enrollments); остальные слова не трогаем —
    жаргон автоматически не исправить.
    Стиль: всё имя заглавными (MAX_USERS) → слова заглавными через «_»;
    первая буква заглавная (OrderStatus) → CamelCase; иначе → snake_case.
    """
    # TODO: ваш код
    ...


def public_names(source):
    """Публичные имена модели: классы, их методы и поля, функции и константы верхнего уровня.

    Имена, которые начинаются с «_», пропускаем. Готовая функция: модуль ast разбирает
    исходный код в дерево, не выполняя его.
    """
    names = []
    for node in ast.parse(source).body:
        if isinstance(node, (ast.ClassDef, ast.FunctionDef)):
            names.append(node.name)
        if isinstance(node, ast.ClassDef):
            for item in node.body:
                if isinstance(item, ast.FunctionDef):
                    names.append(item.name)
                elif isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
                    names.append(item.target.id)
        elif isinstance(node, ast.Assign):
            names.extend(t.id for t in node.targets if isinstance(t, ast.Name))
    return [n for n in names if not n.startswith("_")]


def audit(source):
    """Аудит языка: {имя: [(слово, вид), ...]} только для имён, где есть слова не из глоссария.

    Для каждого имени из public_names(source) разбейте его на слова (split_words),
    определите вид каждого слова (classify_word) и оставьте пары, где вид не KIND_OK.
    Слова идут в порядке появления в имени. Имена без замечаний в отчёт не попадают.
    """
    # TODO: ваш код
    ...


print("Слова имени OrderManager:", split_words("OrderManager"))
print("cancel_order →", to_domain_name("cancel_order"))
print("\nЗамечания к «техническому» коду:")
for name, problems in (audit(TECHNICAL_CODE) or {}).items():
    print(f"  {name:20} " + "; ".join(f"{w} — {kind}" for w, kind in problems))
print("\nЗамечания к коду на едином языке:", audit(DOMAIN_CODE))

Посмотрите на итоговый отчёт: после автоматического переименования остаются OrderManager, process, handle_payment_data, calc_discount и start_dt. Жаргон требует понять, что делает код. А слово discount — отдельный случай: это не жаргон и не синоним, а новое понятие, о котором никто не договорился. Правильная реакция — не придумывать слово самому, а спросить экспертов, есть ли в школе скидки, и если есть — внести термин в глоссарий.

Всё сказанное о людях верно и для ИИ-агентов, только сильнее. Агент выбирает имена по статистике кода, на котором училась модель, а там чаще всего встречаются Order, User, process_data. Без глоссария код, написанный агентом, дрейфует к самому распространённому словарю интернета — то есть к синонимам и жаргону из таблицы выше.

Что помогает на практике:

  • Глоссарий в контексте агента. Положите docs/glossary.md в репозиторий и сошлитесь на него из файла памяти проекта (CLAUDE.md, AGENTS.md — подробно в уроке SDD.1.2): «имена в коде — только из глоссария; новое понятие — сначала вопрос мне».
  • Промпты и спецификации на едином языке. Сравните «сделай отмену заказа» и «добавь отмену записи на поток: при отмене за 7 суток и более до старта потока — полный возврат, позже — без возврата; событие EnrollmentCancelled». Во втором случае агенту нечего додумывать.
  • Проверка, а не надежда. Линтер из задания можно запускать в тестах или в CI: агент, который назвал класс Booking, получит понятное сообщение и исправится сам.
  • Новое слово от агента — сигнал. Если агент ввёл discount_rate, это либо синоним, либо понятие, о котором забыли спросить экспертов. В обоих случаях решает человек.
Проверь себя6 вопросов
1 На ревью вы видите новый класс Booking. В глоссарии этого понятия нет, есть «Запись на поток → Enrollment», и методисты говорят «запись». Что правильно сделать?
2 Что из перечисленного — признаки беды с единым языком? Выберите все верные варианты.Можно выбрать несколько ответов
3 Сколько замечаний выдаст линтер из задания для имени handle_user_order_data? (целое число)
4 Бизнес решил разрешить рассрочку, и эксперты стали различать «счёт» и «платёж»: по одному счёту теперь бывает несколько платежей. В коде всё называется payment. Что это значит?
5 Почему правило лучше обсуждать с экспертом на примерах с числами («Анна записалась 1 октября в 10:00 и не оплатила»), а не общей фразой?
6 Почему в глоссарии школы нет слова «оплата»?
  • Единый язык — общий язык экспертов и разработчиков, построенный на модели. Он звучит везде: в разговорах, задачах, именах классов и методов, событиях, тестах, сообщениях об ошибках и промптах. Изменение языка — это изменение модели и кода, одним коммитом с глоссарием.
  • Глоссарий онлайн-школы — основа всей программы: десять понятий (Course, Cohort, Student, Enrollment, Waitlist, Invoice, Payment, Certificate, Money, Period), три статуса записи, действия и события, шесть правил Набора и открытые вопросы.
  • Язык вырабатывают вместе с экспертами: истории вместо определений, точные слова, пересказ на языке модели, примеры с числами и карта примеров. Примеры потом становятся тестами.
  • Признаки беды — синонимы, технический жаргон, один термин в двух смыслах, слова без определения. Часть проверки можно автоматизировать линтером, но жаргон и новые понятия решает человек вместе с экспертами.
  • Для ИИ-агента глоссарий — лучший контекст: без него код дрейфует к Order и process_data, с ним промпты и спецификации не оставляют места для догадок.

Дальше: урок DDD.1.3 — поддомены. Разберём, почему Набор и Обучение — сердце школы, а Оплату и Уведомления лучше купить, где допустим простой CRUD и как описать ядро системы на одной странице.