Ubiquitous Language и Bounded Context
Единый язык команды и кода внутри контекста. Разные контексты — разный смысл терминов. Anti-Corruption Layer как защита границ.
TL;DR
- Ubiquitous Language — общий словарь бизнеса и разработки. Если в коде user_id, а бизнес говорит "клиент" — уже разрыв.
- Слово имеет смысл только в контексте. Order в billing — счёт; Order в shipping — отправление. Разные модели.
- Bounded Context — не «модуль» и не «микросервис». Это семантическая граница, где термины консистентны.
- Anti-Corruption Layer защищает вашу модель от навязанной чужой. Дороже интеграции, дешевле хаоса.
Зачем эта глава
DDD часто ассоциируют с tactical patterns — Entity, Value Object, Aggregate. Это упрощение. Evans начинал с другого: язык. Без Ubiquitous Language tactical patterns — просто способ раскладки классов, а не модель домена. Стратегический DDD (Strategic Design) отвечает на самые дорогие вопросы: где вообще проходят границы системы.
Ошибка на этом уровне — сотни человеко-часов рефакторинга. Ошибка на уровне tactical — переписать один класс.
Ubiquitous Language
Общий язык, на котором говорят разработчики, доменные эксперты и код. Один термин — один смысл внутри одного контекста.
Принципы:
Термины бизнеса появляются в коде без перевода. Бизнес говорит «активная позиция тендера» — в коде ActiveTenderPosition, а не PositionInProgress или CurrentItem.
Термины кода появляются в разговорах. Если инженер на планёрке говорит «мы обновляем normalized_data», а бизнес не понимает — это разрыв.
Язык уточняется совместно. Разработчик может спросить «а точно ли „заявка” и „предложение” — разные вещи?» — и это часть работы над моделью.
Изменение языка = изменение модели. Появилось новое понятие в разговоре — оно должно попасть в код. Забыть — значит накопить долг.
A domain expert’s use of it should be crisp; a developer’s use of it should be accurate. When flaws in the language emerge, refactor the model — and the code.
Признаки нарушения языка
Признак 1 — «переводчик» между бизнесом и кодом. Аналитик пишет ТЗ: «клиент подтверждает заказ». Разработчик пишет user_service.confirm_order(user_id, order_id). user_id вместо customer_id, order вместо сделка (если бизнес говорит именно так). Каждый раз, читая ТЗ, разработчик переводит.
Признак 2 — синонимы. В базе users, в коде Account, в UI «профиль», в поддержке «клиент». Четыре имени для одной сущности. Через год никто не помнит, где какое имя.
Признак 3 — общие термины. process(), handle(), manage(), execute(). Не языковые термины, а маскировка неточности. Order.process() — что делает? Confirm? Cancel? Ship?
Признак 4 — технические термины в бизнес-логике. Order.sync_to_db(), User.serialize_for_api(). Технические детали протекают в domain.
Bounded Context
Явная граница, внутри которой модель домена консистентна, а Ubiquitous Language имеет однозначный смысл.
Ключевая мысль: один термин может означать разное в разных контекстах, и это нормально.
Пример: слово Order в реальной e-commerce системе:
| Контекст | Что такое Order | Ключевые поля |
|---|---|---|
| Sales | Оформленная заказ покупателя | items, customer, total_amount, status |
| Fulfillment | Заказ к упаковке и отправке | items, warehouse, packed_at, shipping_address |
| Billing | Финансовая транзакция | line_items, tax, invoice_number, paid_at |
| Analytics | Событие покупки | order_id, customer_id, revenue, category_tags |
Одно слово — четыре модели. В каждом контексте Order имеет собственный набор полей, инвариантов, поведения. Пытаться слить их в один «универсальный Order» — самая частая ошибка.
Как определить, где границы контекста
Три сигнала:
1. Один термин — разный смысл. Если разработчики из команд-соседей понимают Product по-разному — вы уже пересекли границу контекста.
2. Разные стейкхолдеры. Sales работает с одним департаментом, Billing — с бухгалтерией, Shipping — с логистами. Разные заказчики требований — обычно разные контексты.
3. Разная скорость изменений. Sales-модель может меняться каждую неделю (эксперименты с промо), Billing — раз в квартал (нормативы). Разная частота = разные жизненные циклы.
Если хотя бы два сигнала совпали — рисуйте границу.
Границы контекста ≠ границы микросервиса
Возможные комбинации:
- Один контекст = один микросервис. Наиболее чистый случай. Легко масштабировать и деплоить.
- Один контекст = несколько микросервисов. Например, split по нагрузке (Order-Command и Order-Query для CQRS). Один контекст, один язык — но разные сервисы.
- Модульный монолит: один процесс, несколько контекстов. Каждый контекст — отдельный модуль с чёткими границами. Хороший стартовый вариант.
- Плохо: один микросервис = несколько контекстов. Внутренний код становится distributed monolith. Термины ломаются на границе.
Как правильно: язык в коде
Возьмём анонимизированный микросервис. Плохо и хорошо:
# Плохо — общие термины, разрыв языка
class OrderProcessor:
async def process(self, data: dict) -> None:
result = await self._api.call(data)
await self._db.save(data['id'], result)
await self._mq.send(result)
# Хорошо — язык домена, конкретика
class OrderStandardizeUseCase:
async def standardize(self, item: OrderItemMessage) -> None:
properties = self._collect_standardizable(item)
normalized = await self._normalizer.normalize(properties)
await self._items.mark_as_standardized(item.id, normalized)
await self._events.publish(StandardizationCompleted(item.id))
Второй вариант читается почти как английский: «стандартизирует item, собрав нормализуемые свойства, отправляет нормализатору, отмечает как стандартизированный, публикует событие StandardizationCompleted». Это Ubiquitous Language в действии.
Anti-Corruption Layer
Изоляционный слой, который транслирует чужую модель в собственный язык. Защищает от «заражения» вашей модели чужими терминами и структурами.
Когда нужен ACL:
- Интеграция с legacy-системой, чей язык не соответствует вашему домену.
- Внешний API диктует термины, которые вам не подходят.
- Партнёр использует другой язык, вы не можете заставить его меняться.
- Вы переходите с одной модели на другую — старая ещё работает.
Пример: интеграция с внешним marketplace
Внешний API — marketplace-api. Его язык: sku, unit_price, stock_quantity. Ваш язык: product, catalog_price, available_units. Термины не совпадают.
Без ACL:
# use case тонет в чужих терминах
async def execute(self, cmd):
sku_response = await self._marketplace.get_sku(cmd.product_code)
stock_qty = sku_response['stock_quantity']
unit_price = sku_response['unit_price']
# ... 30 строк маппинга и переводов туда-сюда
Через полгода marketplace-api меняет unit_price на price_ex_tax — вы меняете код в 12 местах.
С ACL:
# app/infrastructure/marketplace/acl.py
class MarketplaceAdapter:
"""Anti-Corruption Layer: транслирует marketplace-язык в наш."""
async def get_product(self, product_code: ProductCode) -> Product:
raw = await self._client.get_sku(product_code.value)
return Product(
code=product_code,
catalog_price=Money(Decimal(raw['unit_price']), 'RUB'),
available_units=raw['stock_quantity'],
)
# app/application/use_cases/check_availability.py
async def execute(self, cmd):
product = await self._marketplace.get_product(cmd.product_code)
if product.available_units < cmd.required:
raise NotEnoughStock(product.code)
Use case говорит на вашем языке. Если marketplace-api завтра переименует unit_price — вы правите одну строчку в ACL.
▸ ACL стоит дорого — когда он оправдан
ACL — не бесплатный слой. Каждый вызов через ACL требует маппинга. Дублирование моделей, дополнительные тесты.
Когда стоит:
- Внешняя система — legacy или партнёр, вы не можете влиять на её язык.
- Термины серьёзно расходятся — не просто разные имена полей, а разная семантика.
- Планируется миграция — ACL позволит переключиться на другой источник данных незаметно для core.
Когда не стоит:
- Внешний API — ваш же микросервис, язык согласуется. Достаточно тонкого клиента.
- Простой CRUD с 3 полями и одинаковыми терминами. ACL здесь — оверхэд.
Как не надо
1. Универсальная модель через все контексты
class User(Base):
"""Универсальный пользователь для всех контекстов."""
id: Mapped[int]
email: Mapped[str]
password_hash: Mapped[str]
profile_photo_url: Mapped[str | None]
billing_address: Mapped[str | None]
credit_limit: Mapped[Decimal | None]
marketing_opt_in: Mapped[bool]
orders_count: Mapped[int]
last_login: Mapped[datetime | None]
# ... 30 полей
Один класс — четыре контекста. Изменения в auth ломают billing, изменения в marketing — auth. Каждая команда боится трогать.
Правильно: AuthUser в контексте auth, Customer в sales, Billee в billing, Subscriber в marketing. Каждый — со своим набором полей. Общий только UserId.
2. Общие термины в именах
class OrderManager:
def process_order(self, data): ...
def handle_order(self, order): ...
def do_stuff(self, order_id): ...
Manager, process, handle — маскировка неопределённости. Что именно делает handle_order? Confirm? Cancel? Reship?
Правильно: конкретные бизнес-глаголы. Order.confirm(), Order.cancel(), Order.request_refund(). Каждый метод отражает событие в домене.
3. Технический язык вместо доменного
class Order:
def sync_to_db(self): ...
def serialize_for_api(self): ...
def to_dict(self): ...
def from_row(cls, row): ...
Domain-класс знает про БД, API, dict. Половина методов — про технические детали. Ubiquitous Language погиб.
Правильно: Order не знает про БД. Persistence — задача Repository. Serialization — задача mapper’а в infrastructure. Domain — про бизнес-инварианты и события.
4. Отсутствие ACL при интеграции с чужой системой
async def execute(self, cmd):
resp = await self._legacy_api.query({'q': 'ORDR', 'params': {'id': cmd.order_id}})
if resp['status_code'] == 200:
data = resp['payload'][0]
our_order = OurOrder(id=data['ORDER_ID'], amt=Decimal(data['AMT']))
Use case знает про формат legacy-API, коды статусов, странные имена полей. Каждое изменение в legacy ломает вашу бизнес-логику.
Правильно: LegacyOrderAdapter инкапсулирует legacy-протокол, возвращает Order в вашем языке. Use case не знает, что за системой прячется — legacy или новый API.
Пример: карта контекстов e-commerce
Рассмотрим типичную e-commerce систему:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Sales │ │ Fulfillment │ │ Billing │
│ │────▶│ │────▶│ │
│ Order │ │ Shipment │ │ Invoice │
│ Customer │ │ Warehouse │ │ Payment │
│ Product │ │ Carrier │ │ Refund │
└──────────────┘ └──────────────┘ └──────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Marketing │ │ Accounting │
│ │ │ │
│ Campaign │ │ Ledger │
│ Segment │ │ Statement │
│ Coupon │ │ Tax │
└──────────────┘ └──────────────┘
- Sales говорит: «оформлен заказ на 3 позиции».
- Fulfillment переводит: «на склад пришёл shipment на комплектацию».
- Billing переводит: «сгенерирован invoice на 5000 ₽».
Всё это — про один «заказ», но каждый контекст видит своё. Термины не пересекаются.
Trade-offs
| Ситуация | Bounded Context оправдан | Один общий контекст |
|---|---|---|
| Разные стейкхолдеры для разных частей системы | ✓ | |
| Одно слово в разных ролях значит разное | ✓ | |
| Планируется microservices decomposition | ✓ | |
| Внутренний CRUD-инструмент на 2 недели | ✓ | |
| Домен из 5 сущностей без явной специализации | ✓ | |
| Один эксперт, одна команда, один язык | ✓ |
В твоём же коде
Анонимизированный микросервис — простой случай: один сервис = один контекст = стандартизация характеристик товарных позиций.
Что здесь хорошо:
- Термины бизнеса (
item,characteristic,normalization) появляются в коде. - Нет универсальных
Process,Manager,Data. - Внешний
marketplace-apiинкапсулирован вMarketplaceAdapter— примитивная форма ACL.
Что можно улучшить:
matcher-service(потребитель наших событий) диктует формат события (values[0].normalized,length_normalized). Мы транслируем это внутри use case — язык consumer’а протек в наш core. Правильно: ACL на исходящей стороне —OutgoingMessageMapperвinfrastructure/rabbitmq/.
Это — тот самый разбор из Case study → mapper-in-use-case.
Дальнейшее чтение
- Eric Evans. Domain-Driven Design. Part IV (Strategic Design) — оригинальные главы про Ubiquitous Language и Bounded Context.
- Vaughn Vernon. Implementing Domain-Driven Design. Chapter 2 — Domains, Subdomains, and Bounded Contexts.
- Vaughn Vernon. Domain-Driven Design Distilled. Chapters 2–3 — быстрое введение с примерами.
- Herberto Graça. Ubiquitous Language. Практика применения.
- Alberto Brandolini. EventStorming. Метод коллективного построения Ubiquitous Language.
Проверьте себя
Мини-quiz · закрепить
Проверьте себя
Q1. Что означает Ubiquitous Language?
Q2. Bounded Context — это...
Q3. Что делает Anti-Corruption Layer?
Q4. Признак нарушения Ubiquitous Language:
Q5. Универсальный класс User с полями auth, profile, billing, marketing — это...