Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Domain-Driven Design: ограниченные контексты для разработчика

Ограниченные контексты: где заканчивается одна модель

DDD для разработчика: ограниченный контекст, единый язык, агрегаты и границы согласованности, практическое применение без академизма.

Прямой ответ

Ограниченный контекст — это область, в пределах которой модель предметной области непротиворечива. Одно и то же слово в разных контекстах означает разные вещи, и попытка построить единую модель для всей системы приводит к объекту с тридцатью полями, из которых каждому сценарию нужны пять.

Классический пример — «товар»:

Контекст Что такое товар
Каталог Название, описание, фотографии, категория
Склад Ячейка хранения, остаток, габариты, вес
Доставка Объём, вес, ограничения по перевозке
Ценообразование Себестоимость, наценка, акции

Это четыре разные модели, связанные общим идентификатором. Попытка объединить их в один класс создаёт сущность, которую невозможно изменить без согласования с четырьмя командами.

Второе ключевое понятие — единый язык. Термины, которыми оперирует бизнес, должны совпадать с именами в коде. Если менеджер говорит «резерв», а в коде это hold_flag, расхождение накапливается в каждом обсуждении и в каждой ошибке.

Третье — агрегат. Это группа объектов, изменяемая как единое целое в одной транзакции:

class Order:                       # корень агрегата
    def __init__(self, order_id: str, customer_id: str):
        self.id = order_id
        self.customer_id = customer_id
        self._items: list[OrderItem] = []
        self.status = OrderStatus.NEW

    def add_item(self, sku: str, qty: int, price: Decimal) -> None:
        if self.status is not OrderStatus.NEW:
            raise DomainError("Нельзя менять состав подтверждённого заказа")
        self._items.append(OrderItem(sku, qty, price))

    def total(self) -> Decimal:
        return sum((i.price * i.qty for i in self._items), Decimal(0))

Правила агрегата:

  1. Один корень. Внешний код обращается только к нему; вложенные объекты недоступны напрямую.
  2. Ссылки по идентификатору. Агрегат ссылается на другой агрегат через идентификатор, а не через объект.
  3. Одна транзакция — один агрегат. Изменение двух агрегатов в одной транзакции означает, что граница проведена неверно.
  4. Инварианты внутри. Проверка бизнес-правил выполняется в корне агрегата, а не в вызывающем коде.

Практика: применение без академизма

1. Начинайте со стратегической части, а не с тактической. Границы контекстов важнее шаблонов кода. Неправильная граница обходится дороже, чем отсутствие value object.

2. Ищите границы по языку, а не по таблицам. Если одно слово в разных отделах означает разное — это разные контексты. Если бизнес-процесс не пересекает отдел — вероятно, это один контекст.

3. Не делайте агрегат большим. Крупный агрегат блокируется целиком и конфликтует при параллельной работе. Практический признак: если два пользователя регулярно мешают друг другу, граница слишком широкая.

4. Согласованность между агрегатами — через события. Требование «остаток на складе обновился сразу после заказа» почти всегда на самом деле означает «обновился в течение секунды». Явная согласованность нужна только внутри агрегата; о моделях согласованности — в статье про eventual consistency.

5. Контекст не равен сервису. Несколько контекстов могут жить в одном приложении; о границах модулей — в статье про модульный монолит. Обратное тоже верно: сервис может реализовывать часть контекста.

6. Фиксируйте единый язык в словаре. Расхождение терминов — источник ошибок в требованиях. Словарь терминов дешевле, чем разбор неверной реализации.

7. Не переносите чужую модель в свой контекст. Если внешняя система оперирует своими понятиями, их нужно транслировать, а не копировать внутрь; о приёме — в статье про антикоррупционный слой.

Рекомендации:

  1. Рисуйте карту контекстов до написания кода. Схема из десятка прямоугольников со связями выявляет проблемы быстрее, чем реализация.
  2. Проверяйте границы по частоте изменений. Если два модуля почти всегда меняются вместе, это один контекст, разделённый искусственно.
  3. Избегайте общей модели данных. Общая база на несколько контекстов делает их изменение согласованным — см. статью про общую базу как антипаттерн.
  4. Используйте идентификаторы для связей. Ссылка по идентификатору переживает изменение чужой модели; ссылка по объекту — нет.
  5. Не внедряйте DDD целиком. Агрегаты и контексты полезны почти всегда; полный набор тактических шаблонов оправдан только в сложной предметной области.

Практический сценарий: единая модель заказа тормозила все изменения

Контекст: в системе был один класс Order с 42 полями, который использовали каталог, доставка, бухгалтерия и поддержка. Любое изменение требовало согласования с четырьмя командами.

  1. Диагностика. Анализ коммитов показал, что 70 % изменений класса приходились на два сценария, а остальные поля использовались в отдельных местах. Число полей росло, поскольку каждый сценарий добавлял своё.
  2. Разделение по языку. Обсуждение с командами выявило, что «заказ» в доставке — это отправление с адресом и габаритами, в бухгалтерии — документ с суммами и ставками, в поддержке — обращение с историей действий.
  3. Действия. (а) Выделили три контекста с собственными моделями, связанными идентификатором заказа; (б) в каждом контексте оставили только нужные поля; (в) обмен организовали через события: создание заказа публиковало событие, остальные контексты строили собственное представление; (г) ввели словарь терминов, поскольку слово «позиция» в трёх контекстах означало разное; (д) инварианты перенесли в корень агрегата вместо разбросанных проверок.
  4. Результат. Средний размер модели сократился с 42 до 9–12 полей; изменение в доставке перестало затрагивать бухгалтерию, а число конфликтов слияния в общем файле исчезло.

Вывод: рост числа полей в общей модели — признак отсутствия границ, а не недостатка рефакторинга. Разделение по смыслу, который вкладывают в термин разные участники, даёт устойчивые границы.

FAQ

Обязателен ли отдельный сервис на каждый контекст? Нет. Контекст — это граница модели, а сервис — граница развёртывания. Несколько контекстов могут жить в модульном монолите.

Что делать, если бизнес настаивает на единой модели? Показать конкретные случаи, когда изменение в одной области ломало другую. Аргумент работает лучше теоретических рассуждений.

Как определить размер агрегата? По инварианту: всё, что должно оставаться согласованным в одной транзакции, входит в агрегат. Всё остальное — за его пределами.

Нужны ли value object и репозитории в простом CRUD? Не обязательно. Тактические шаблоны оправданы там, где есть нетривиальные бизнес-правила; для простого хранилища достаточно обычной структуры.

Источники: Martin Fowler (BoundedContext, DDD_Aggregate), domainlanguage.com — ссылки в шапке.