Перейти к содержимому
Программа DDDDDD.2 Стратегическое проектирование

Карта контекстов: как контексты живут вместе

Что такое карта контекстов, кто upstream, а кто downstream, девять шаблонов отношений между контекстами и командами — от партнёрства до «большого кома грязи», как выбрать шаблон по силе команд и стабильности чужого API и как предохранительный слой не пускает модель внешней платёжной системы в код онлайн-школы.

УРОКddd/ddd-2-strategic/02-context-mapping.mdx2 ч 10 минУрок 2 из 3
После урока вы сможете
  • Нарисовать карту контекстов онлайн-школы, указать upstream и downstream и подписать отношения шаблонами
  • Объяснить девять шаблонов карты контекстов и три вида отношений команд и приводить пример каждого
  • Выбирать шаблон по силе влияния команд, качеству и стабильности чужой модели и важности своего контекста
  • Написать предохранительный слой, который переводит ответы внешней платёжной системы в объекты языка школы

Первую версию оплаты в онлайн-школе написали за два дня: прочитали документацию платёжной системы и стали использовать её ответы прямо в коде. В Наборе появилось if payload["state"] == "CAPTURED", в отчётах — order_ref, в письмах — сумма float(payload["amount"]). Через год код с полями платёжной системы нашёлся в четырнадцати местах. Потом платёжная система выпустила вторую версию API и переименовала состояния. Школа узнала об этом утром, когда записи перестали подтверждаться. А когда финансовый директор предложил перейти к поставщику подешевле, команда оценила переезд в три месяца: чужая модель проросла во все контексты.

Контексты из прошлого урока не живут поодиночке. Набору нужны цены из Каталога, Оплата зависит от платёжной системы, Обучению нужно знать, кто подтвердил запись. Каждая такая связь — это не только вызов функции, но и отношения между моделями и между командами: кто под кого подстраивается, кто может попросить изменения, кто узнаёт о поломке последним. Карта контекстов делает эти отношения видимыми, а шаблоны дают им имена и готовые решения. Главное из них для школы — предохранительный слой вокруг Оплаты. Его вы напишете в практике.

Карта контекстовкарта контекстовEN context mapСхема и описание связей между ограниченными контекстами и командами: кто от кого зависит (upstream или downstream) и по какому шаблону отношений они взаимодействуют. Рисуют местность как есть, а не как хотелось бы. (context map) — схема и описание того, какие ограниченные контексты есть в системе и как они связаны: кто от кого зависит и по какому шаблону отношений. Эванс советует называть каждый контекст и делать эти имена частью единого языка, описывать точки соприкосновения моделей и явно указывать, где модели переводятся, где что-то общее, а где — изоляция. И добавляет важное правило: наносите на карту местность как она есть, а преобразования оставьте на потом. Карта, на которой нарисовано желаемое, бесполезна: она прячет проблемы.

Главное понятие карты — направление влияния, upstream и downstreamвышестоящий и нижестоящий контексты (upstream / downstream)EN upstream / downstreamНаправление влияния между контекстами, как течение реки: изменения вышестоящего (upstream) отражаются на нижестоящем (downstream), а обратное неверно. Upstream может преуспеть независимо от судьбы downstream.. Представьте реку: если выше по течению сбросить краску, вода окрасится ниже, но не наоборот. Контекст upstream (выше по течению) влияет на downstream (ниже по течению): его изменения отражаются на соседе, а изменения соседа на нём почти не сказываются. В справочнике Эванса сказано: upstream может добиться успеха независимо от судьбы downstream.

В школе платёжная система — upstream для Оплаты: она меняет API по своему графику, и школа должна подстраиваться. Каталог — upstream для Набора: если Каталог поменяет формат цены, сломается Набор, а не наоборот. Upstream — это не «главнее» и не «раньше в процессе», это «чьи решения расходятся по течению».

DDD Crew выделяет три вида отношений между командами:

ОтношениеЧто значитПример в школе
взаимозависимые (mutually dependent)части должны выйти вместе, иначе не работает ни однаНабор и Обучение при запуске переноса студента на другой поток
upstream / downstreamодна сторона влияет на другую, обратное слабоплатёжная система → Оплата; Каталог → Набор
свободные (free)изменения одной стороны никак не влияют на другуюКаталог и Обучение после решения о раздельных путях (см. ниже)

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

Большинство публикаций сообщества DDD описывает девять шаблонов карты контекстов. Ниже они собраны по смыслу; примеры — из онлайн-школы. Сокращения в скобках принято писать прямо на карте.

ПартнёрствопартнёрствоEN PartnershipШаблон карты контекстов: две команды успешны или проваливаются только вместе, поэтому планируют разработку совместно и вместе развивают интерфейс между контекстами. (Partnership). Если провал любого из двух контекстов означает провал обоих, команды договариваются о партнёрстве: вместе планируют разработку, вместе развивают интерфейс и выпускают зависимые изменения к одному релизу. В школе так бывает с Набором и Обучением, когда нужен перенос студента на другой поток: Набор переносит запись, Обучение переносит прогресс и доступ к занятиям, и выпустить половину нельзя. Цена партнёрства — постоянная координация; держать его стоит только пока зависимость действительно взаимная.

Общее ядрообщее ядроEN Shared KernelШаблон карты контекстов: небольшая явно выделенная часть модели (и её код), которую две команды используют совместно и меняют только по договорённости. Держать маленьким и под общими тестами. (Shared Kernel, SK). Небольшую часть модели — вместе с её кодом, а иногда и схемой данных — две команды явно договариваются использовать совместно. Это ядро особенное: его не меняют без согласия другой стороны, а тесты у него общие. В школе общим ядром разумно сделать объекты-значения Money и Period в пакете school/shared_kernel/: Набор и Оплата должны одинаково понимать деньги в копейках с валютой, и двум копиям Money тут разойтись хуже, чем договариваться. Главное правило — держать ядро маленьким. Общее ядро размером с половину модели — это снова одна модель на всех. DDD Crew замечает, что команды с общим ядром обычно взаимозависимы и им стоит договориться о партнёрстве.

Upstream и downstream: заказчик–поставщик, конформист, предохранительный слой

Заголовок раздела «Upstream и downstream: заказчик–поставщик, конформист, предохранительный слой»

Заказчик–поставщикзаказчик–поставщикEN Customer / Supplier DevelopmentШаблон карты контекстов: нижестоящая команда — заказчик, вышестоящая — поставщик; потребности заказчика входят в план поставщика, их обсуждают и закладывают в сроки. (Customer / Supplier, CUS/SUP). Upstream-команда — поставщик, downstream — заказчик, и потребности заказчика входят в план поставщика: их обсуждают, оценивают и закладывают в сроки. Так устроены отношения Каталога и Набора: Набору нужно событие «поток открыт для записи» с датой старта, вместимостью и ценой, и команда «Витрина» планирует эти изменения вместе с командой Набора. Шаблон работает, когда у команд общее руководство или общие цели и поставщик действительно заинтересован в заказчике.

КонформистконформистEN ConformistШаблон карты контекстов: нижестоящая команда без перевода принимает модель вышестоящей. Интеграция предельно проста, но чужая модель становится частью вашей. (Conformist, CF). Downstream-команда без всякого перевода принимает модель upstream как есть. Эванс формулирует честно: это стесняет downstream и вряд ли даёт идеальную модель, зато интеграция становится предельно простой, а с upstream у вас появляется общий язык. Пример в школе — Уведомления и купленный сервис рассылок: его модель «получатель, шаблон, переменные шаблона» нас вполне устраивает, Уведомления — типовой поддомен, и писать перевод ради красоты незачем. Конформист — нормальный выбор, если чужая модель хороша, а ваш контекст не основной.

Предохранительный слойпредохранительный слойEN Anticorruption Layer (ACL)Шаблон карты контекстов: нижестоящий контекст ставит изолирующий слой, который переводит модель чужой системы в термины собственной модели (и обратно). Чужие поля, статусы и слова не проходят дальше слоя. Встречаются переводы «предохранительный уровень» и «антикоррупционный слой». (Anticorruption Layer, ACL). Downstream строит изолирующий слой, который даёт системе возможности upstream, но в терминах собственной модели. Слой говорит с чужой системой через её существующий интерфейс, ничего не требуя от неё, а внутри переводит модели в одну или обе стороны. В школе это слой между Оплатой и внешней платёжной системой: снаружи — order_ref, CAPTURED и рубли строкой, внутри — счёт, платёж и Money. В русских текстах встречаются и другие переводы: «предохранительный уровень», «антикоррупционный слой».

Открытость: служба с открытым протоколом и общедоступный язык

Заголовок раздела «Открытость: служба с открытым протоколом и общедоступный язык»

Служба с открытым протоколомслужба с открытым протоколомEN Open Host Service (OHS)Шаблон карты контекстов: вышестоящий контекст открывает для всех потребителей единый документированный протокол (например, публичный API) вместо отдельной интеграции под каждого. (Open-host Service, OHS). Когда потребителей у контекста много, писать отдельную интеграцию под каждого слишком дорого. Upstream открывает один протокол — набор операций, доступный всем, — и развивает его под общие потребности. Если у одного потребителя особые нужды, Эванс советует сделать для него отдельный переводчик, а общий протокол держать простым. Платёжная система — классическая служба с открытым протоколом: её публичный API одинаков для тысяч магазинов. DDD Crew уточняет: потребители такой службы сами решают, быть им конформистами или строить предохранительный слой.

Общедоступный языкобщедоступный языкEN Published Language (PL)Шаблон карты контекстов: хорошо документированный формат обмена, через который контексты переводят свои модели, например iCalendar или vCard. Часто сочетается со службой с открытым протоколом. (Published Language, PL). Хорошо документированный общий формат, через который контексты обмениваются данными и в который переводят свои модели. Общеизвестные примеры из README DDD Crew — iCalendar для событий календаря и vCard для контактов. В школе Обучение может отдавать расписание занятий потока в формате iCalendar: студент подписывается на него в любом календаре, и школе не нужно ничего знать о его приложении. Формат уведомлений платёжной системы из её документации — тоже общедоступный язык. Часто PL и OHS идут вместе: открытый протокол говорит на опубликованном языке.

Без интеграции: раздельные пути и «большой ком грязи»

Заголовок раздела «Без интеграции: раздельные пути и «большой ком грязи»»

Раздельные путираздельные путиEN Separate WaysШаблон карты контекстов: интеграция стоит дороже пользы, поэтому контексты сознательно не связывают и решают задачу каждый по-своему, даже ценой дублирования. (Separate Ways, SW). Иногда интеграция стоит дороже пользы. Маркетинг хотел показывать на витрине «уже учатся 1 240 студентов» по живым данным Обучения. Команды прикинули: нужна новая модель чтения, события, кэш — неделя работы ради одной цифры. Решили разойтись: маркетолог раз в месяц обновляет число вручную. Это не лень, а осознанное решение: контексты свободны друг от друга, и каждый ищет простое решение у себя.

«Большой ком грязи»«большой ком грязи»EN Big Ball of MudСистема без различимой структуры: всё зависит от всего, правила размазаны и продублированы, любое изменение страшно. Название — из статьи Брайана Фута и Джозефа Йодера (1997). (Big Ball of Mud, BBoM) — знакомое по уроку DDD.1.1 понятие, но на карте у него особая роль: это пометка. Ею обводят часть системы со смешанными моделями и размытыми границами, чтобы все видели: оттуда грязь не должна расползаться. У школы есть старая CRM отдела корпоративных продаж, где менеджеры годами вели заявки компаний. Её модель никто не понимает целиком, но корпоративные записи приходят именно оттуда. На карте она помечена как ком грязи, а Набор получает из неё данные только через свой предохранительный слой.

Соберём всё на одной карте. Подписи у стрелок — шаблоны; U — upstream, D — downstream, стрелка идёт по течению, от upstream к downstream.

flowchart LR
PS["Платёжная система<br/>(внешняя)<br/>U: OHS, PL"] -->|"ACL"| P["Оплата<br/>D: ACL"]
ES["Сервис рассылок<br/>(внешний)<br/>U: OHS"] -->|"CF"| U["Уведомления<br/>D: CF"]
K["Каталог"] -->|"CUS/SUP<br/>поток открыт для записи"| N["Набор"]
P -->|"ПлатёжПолучен"| N
N -->|"выставить счёт,<br/>вернуть деньги"| P
P === SK["общее ядро:<br/>Money, Period"] === N
N <-->|"партнёрство"| L["Обучение"]
L -->|"CF"| C["Сертификация"]
L -->|"OHS, PL:<br/>iCalendar"| CAL["Календари студентов"]
CRM["Старая CRM<br/>корпоративных продаж<br/>BBoM"] -->|"ACL"| N
K -.-|"SW"| L
N -.->|"события"| U
C -.->|"СертификатВыдан"| U
Карта контекстов онлайн-школы «как есть». Пунктир с SW — раздельные пути: связи нет намеренно

Несколько пояснений:

  • Набор и Оплата — в одной команде (см. схему команд в прошлом уроке), поэтому договориться им легко: команды и событие между ними — внутренняя договорённость, а Money лежит в общем ядре;
  • Сертификация — конформист к Обучению: она берёт событие «курс пройден» и процент сданных заданий в модели Обучения, без перевода. Обе части ведёт команда «Учёба», модель Обучения Сертификацию устраивает;
  • предохранительных слоёв на карте два: один стоит в Оплате перед платёжной системой, второй — в Наборе перед старой CRM. Чужая модель поставщика и ком грязи — два классических повода для ACL.

На практике такую карту редко рисуют целиком. DDD Crew советует маленькие карты под конкретный вопрос: «как модель платёжной системы расходится по школе?», «на кого влияет команда Витрина?», «где у нас политика, а не техника?». Большая карта со всеми шаблонами быстро превращается в перегрузку, и объяснять её разным людям тяжело. Второй совет — заранее договориться, какими шаблонами вы пользуетесь, и объяснить их всем, кто будет читать карту: названия вроде «конформист» неочевидны для людей вне DDD. Шпаргалку DDD Crew (Context Map Cheat Sheet) удобно повесить рядом.

Шаблон не выбирают по вкусу. Он следует из трёх вопросов: может ли downstream влиять на upstream, насколько хороша и стабильна чужая модель и насколько важен ваш собственный контекст.

flowchart TB
Q0{"Интеграция<br/>окупается?"} -->|"нет"| SW["Раздельные пути"]
Q0 -->|"да"| Q1{"Команды успешны<br/>только вместе?"}
Q1 -->|"да"| PA["Партнёрство<br/>(иногда + общее ядро)"]
Q1 -->|"нет"| Q2{"Upstream учитывает<br/>ваши потребности?"}
Q2 -->|"да"| CS["Заказчик–поставщик"]
Q2 -->|"нет"| Q3{"Чужая модель хороша и стабильна,<br/>а ваш контекст не core?"}
Q3 -->|"да"| CF["Конформист"]
Q3 -->|"нет"| ACL["Предохранительный слой"]
Упрощённый выбор шаблона со стороны downstream. Ком грязи upstream всегда ведёт к предохранительному слою

То же в виде таблицы — по силе команд и стабильности API:

СитуацияРазумный шаблонПочему
у команд общий руководитель и общие целизаказчик–поставщикпоставщик может и хочет учесть ваши нужды
поставщик — внешняя компания или огромная платформа, его модель хороша и стабильна, ваш контекст типовойконформистперевод не окупится; общий язык с поставщиком — даже плюс
поставщик вас не слушает, его модель чужая или часто меняется, а ваш контекст важенпредохранительный слойчужие изменения и слова остаются в одном месте
у вашего контекста много потребителейслужба с открытым протоколом и общедоступный языкодин документированный протокол дешевле десяти частных интеграций
две части ломаются только вместепартнёрствобез совместного планирования релизы будут срываться
небольшая часть модели обязана совпадать до битаобщее ядродешевле договариваться, чем синхронизировать две копии
интеграция дороже пользыраздельные путиручная работа или дублирование дешевле
upstream — ком грязипредохранительный слойгрязь не должна проходить дальше границы

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

Почему Оплату покупают и защищают предохранительным слоем

Заголовок раздела «Почему Оплату покупают и защищают предохранительным слоем»

В уроке DDD.1.3 мы отнесли Оплату к типовым поддоменам и решили купить платёжный сервис, а свой код свести к адаптеру и слою перевода. Теперь видно, почему именно так:

  1. Типовой поддомен. Безопасность карт, банки, чеки и требования закона давно решены платёжными сервисами. Писать это самим — тратить силы не на ядро.
  2. Мы downstream без влияния. Платёжная система — служба с открытым протоколом для тысяч магазинов. Под нужды одной школы она API не поменяет: заказчиком–поставщиком тут не стать.
  3. Чужая модель, чужие слова. У поставщика «заказ» (order_ref), состояния AUTHORIZED и CAPTURED, суммы строкой в рублях и коды валют числами. В глоссарии школы «заказа» нет вовсе — это запрещённый синоним записи на поток. Стать конформистом значит впустить эти слова в ядро.
  4. Чужой график изменений и риск смены поставщика. Новая версия API или переход к другому поставщику должны затронуть один слой, а не четырнадцать мест по всей школе.

Отсюда и решение: Оплата — тонкий контекст с ролью шлюза и переводчика (в терминах канвы DDD Crew — Gateway и Interchanger), а сердце этого контекста — предохранительный слой.

Вот как выглядел код из начала урока. Обработчик уведомления платёжной системы жил прямо в Наборе:

def on_provider_webhook(payload):
"""Уведомление от платёжной системы. Писали за два дня."""
enrollment = find_enrollment_by_invoice(payload["order_ref"]) # «order» — слово чужой системы
if payload["state"] in ("CAPTURED", "AUTHORIZED"): # её состояния — в ядре школы
enrollment.status = "CONFIRMED"
send_email(enrollment.student_id, f"Оплачено {float(payload['amount'])} руб.") # float для денег

Здесь сразу четыре беды: слова и состояния поставщика в Наборе; деньги во float; ошибочное решение «AUTHORIZED — тоже оплачено», хотя деньги только заблокированы на карте и ещё могут не дойти; незнакомое состояние (скажем, CHARGEBACK — оспаривание платежа) просто молча игнорируется.

Эванс описывает внутреннее устройство слоя как сочетание фасада, адаптеров и переводчиков. В Python-проекте школы это три небольших модуля в пакете school/payment/:

ЧастьФайлЧто делаетЧьи слова
адаптерprovider_client.pyHTTP-запросы к платёжной системе, проверка подписи уведомлений, повторытолько поставщика
переводчикacl.pyчистые функции: словарь поставщика → объекты школы и обратнона входе поставщика, на выходе школы
фасадapi.pyто, что видят другие контексты: «выставить счёт», «вернуть деньги», событие ПлатёжПолучентолько школы
flowchart LR
PS["Платёжная система<br/>order_ref, state, amount"] --> AD["Адаптер<br/>provider_client.py<br/>HTTP, подпись"]
AD --> TR["Переводчик<br/>acl.py<br/>dict → PaymentResult"]
TR --> FA["Фасад Оплаты<br/>api.py"]
FA -->|"ПлатёжПолучен<br/>(invoice_id, Money)"| N["Набор"]
Предохранительный слой Оплаты: чужие слова живут левее переводчика, правее — только язык школы

Переводчик — самая важная и самая тестируемая часть: в нём нет сети и базы, только данные на входе и объекты на выходе. Набор после этого получает событие ПлатёжПолучен со своим номером счёта и Money и понятия не имеет, какая платёжная система стоит за Оплатой. Правила перевода — это решения предметной области, а не техника: что AUTHORIZED для школы ещё не платёж, решает бухгалтерия, и это решение записано в одном месте.

Перевод работает в обе стороны. Когда Набор просит вернуть деньги, фасад принимает команду на языке школы — номер счёта и Money, — а переводчик формирует запрос в формате поставщика. Мы сосредоточимся на входящем направлении: оно опаснее, потому что чужие данные приходят к нам сами.

Хороший предохранительный слой подчиняется трём правилам:

  1. Не угадывать. Незнакомое состояние, пропавшее поле, сумма в странном формате — это отказ с понятным сообщением, а не «наверное, оплачено». Молчаливое угадывание и есть тот самый тихий баг.
  2. Не пропускать чужое. За слоем нет order_ref, txn_id, CAPTURED и сырых словарей. Лишние поля поставщика — номер карты, комиссия — просто не переносятся.
  3. Не терять смысл. Деньги — целые копейки, время — с часовым поясом, номер счёта проверен.

Напишите переводчик предохранительного слоя. В заготовке уже есть язык школы — Money, PaymentOutcome (итог платежа: «ждём платёж», «платёж получен», «платёж не прошёл», «деньги возвращены»), PaymentResult и событие PaymentReceived — и выдержка из документации платёжной системы. Обратите внимание: PaymentOutcome и PaymentResult — слова контекста Оплата, которых нет в глоссарии Набора. Так и должно быть: у каждого контекста свой язык, и Оплата говорит о платежах подробнее.

Вам нужно написать:

  • STATE_TO_OUTCOME и translate_state — перевод шести состояний поставщика, отказ на незнакомом;
  • parse_money — сумма строкой и цифровой код валюты по ISO 4217 → Money в копейках, без float;
  • translate_response — всё уведомление → PaymentResult с проверкой обязательных полей, номера счёта и времени;
  • received_event — событие ПлатёжПолучен только тогда, когда деньги действительно получены.

Тесты проверяют перевод, отказы на незнакомых состояниях и испорченных ответах и то, что чужие термины не просачиваются в результат.

Предохранительный слой: ответы платёжной системы на языке школы8 тестов
"""Предохранительный слой между контекстом Оплата и внешней платёжной системой.

Платёжная система присылает уведомления о платежах в своём формате и своими словами.
Слой переводит их на язык школы — и дальше этого файла чужие поля, статусы и слова не проходят.
"""
import re
from dataclasses import dataclass
from datetime import datetime
from enum import Enum


# ===================== Язык школы (контекст Оплата) — уже готово =====================

class UntranslatableResponse(ValueError):
    """Ответ внешней платёжной системы нельзя перевести на язык школы."""


@dataclass(frozen=True)
class Money:
    """Деньги: сумма в копейках и валюта (подробно — в уроке DDD.3.1)."""
    amount: int
    currency: str

    def __post_init__(self):
        if not isinstance(self.amount, int) or self.amount < 0:
            raise ValueError(f"Сумма — целое неотрицательное число копеек, получено {self.amount!r}")
        if not re.fullmatch(r"[A-Z]{3}", self.currency):
            raise ValueError(f"Валюта — три заглавные латинские буквы, получено {self.currency!r}")


class PaymentOutcome(Enum):
    """Итог платежа по счёту — так об этом говорят в Оплате школы."""
    AWAITING = "ждём платёж"
    RECEIVED = "платёж получен"
    FAILED = "платёж не прошёл"
    REFUNDED = "деньги возвращены"


@dataclass(frozen=True)
class PaymentResult:
    """Что школа узнала о платеже по своему счёту."""
    invoice_id: str
    outcome: PaymentOutcome
    amount: Money
    occurred_at: datetime


@dataclass(frozen=True)
class PaymentReceived:
    """Доменное событие ПлатёжПолучен: деньги по счёту поступили."""
    invoice_id: str
    amount: Money
    received_at: datetime


# ============ Как говорит внешняя платёжная система (выдержка из её документации) ============
#
# Уведомление о платеже — JSON-объект (в Python — словарь) с полями:
#   txn_id     номер операции в платёжной системе (школе не нужен)
#   order_ref  «номер заказа» продавца; школа передаёт сюда номер своего счёта: INV-…
#   state      состояние операции:
#                NEW         операция создана, покупатель ещё не платил
#                AUTHORIZED  деньги заблокированы на карте покупателя, но ещё не списаны
#                CAPTURED    деньги списаны и будут перечислены продавцу
#                DECLINED    банк отказал
#                EXPIRED     покупатель не заплатил вовремя
#                REFUNDED    деньги возвращены покупателю
#   amount     сумма строкой в основных единицах валюты, ровно два знака после точки: "30000.00"
#   currency   цифровой код валюты по ISO 4217: "643" — рубль, "840" — доллар США, "978" — евро
#   updated    время последнего изменения операции, ISO 8601 с часовым поясом: "2026-10-01T10:15:00+00:00"
#   card_mask, fee и другие поля школе не нужны.

SAMPLE_RESPONSE = {
    "txn_id": "tx_8f3a91c2",
    "order_ref": "INV-2026-0042",
    "state": "CAPTURED",
    "amount": "30000.00",
    "currency": "643",
    "updated": "2026-10-01T10:15:00+00:00",
    "card_mask": "220000******0004",
    "fee": "870.00",
}

CURRENCY_BY_CODE = {"643": "RUB", "840": "USD", "978": "EUR"}     # ISO 4217: цифровой код → буквенный
REQUIRED_FIELDS = ("order_ref", "state", "amount", "currency", "updated")
INVOICE_PREFIX = "INV-"                                            # так начинаются номера счетов школы


# ===================== Предохранительный слой — ваша часть =====================

# TODO: словарь «состояние платёжной системы → PaymentOutcome» для всех шести состояний из документации.
# Подумайте, что для школы значит AUTHORIZED: получены ли деньги?
STATE_TO_OUTCOME = {}


def parse_money(amount, currency_code):
    """Перевести сумму и код валюты платёжной системы в Money школы: "30000.00", "643" → Money(3000000, "RUB").

    Сумма — строка «рубли.копейки», ровно две цифры после точки; иначе UntranslatableResponse.
    Код валюты переводится через CURRENCY_BY_CODE; незнакомый код — UntranslatableResponse.
    Не используйте float: float("0.29") * 100 == 28.999999999999996, и копейка теряется.
    """
    # TODO: ваш код
    raise NotImplementedError("parse_money ещё не написана")


def translate_state(state):
    """Перевести состояние операции платёжной системы в PaymentOutcome школы.

    Незнакомое состояние не угадываем: UntranslatableResponse, в сообщении — само состояние.
    """
    # TODO: ваш код
    raise NotImplementedError("translate_state ещё не написана")


def translate_response(response):
    """Перевести уведомление платёжной системы в PaymentResult — понятие Оплаты школы.

    1. Каждое поле из REQUIRED_FIELDS должно быть в ответе; нет поля — UntranslatableResponse с его именем.
    2. order_ref — номер счёта школы; не начинается с INVOICE_PREFIX — UntranslatableResponse.
    3. updated → datetime через datetime.fromisoformat; не тот формат или нет часового пояса
       (у результата tzinfo is None) — UntranslatableResponse.
    4. state → translate_state, amount и currency → parse_money.
    Лишние поля (txn_id, card_mask, fee…) не переносятся. Исходный словарь не меняется.
    """
    # TODO: ваш код
    raise NotImplementedError("translate_response ещё не написана")


def received_event(result):
    """Доменное событие ПлатёжПолучен (PaymentReceived), если по счёту получен платёж; иначе None."""
    # TODO: ваш код
    raise NotImplementedError("received_event ещё не написана")


# Кнопка «Запустить» переводит пример уведомления
try:
    result = translate_response(SAMPLE_RESPONSE)
    print(f"Итог: {result.outcome.value} | счёт {result.invoice_id} | {result.amount.amount / 100:.2f} {result.amount.currency}")
    print("Событие:", received_event(result))
except NotImplementedError as error:
    print("Пока не готово:", error)

Когда тесты пройдут, посмотрите на решение глазами команды Набора: в нём нет ни одного импорта из адаптера, ни одного слова поставщика за пределами словаря STATE_TO_OUTCOME и проверок формата. Если завтра школа сменит платёжную систему, перепишется этот файл и адаптер, а Набор и его тесты не заметят переезда.

Два вопроса для размышления. Первый: что делать, если платёжная система пришлёт одно и то же уведомление дважды — такое бывает при повторах после сбоя сети? Подсказка: получатель уведомлений должен быть идемпотентным, повторная обработка не должна подтверждать запись второй раз; это тема урока DDD.4.2. Второй: что должна сделать школа, когда слой отказал на незнакомом состоянии CHARGEBACK? Сохранить уведомление, сообщить дежурному и бухгалтерии — и вместе с экспертами решить, как назвать это на языке школы.

Проверь себя6 вопросов
1 Каталог поменял формат события «поток открыт для записи», и в Наборе перестала работать запись. Изменения в Наборе Каталог никогда не ломали. Кто здесь upstream?
2 Школа подключает сервис рассылок. Его модель «получатель, шаблон, переменные» удобна и стабильна, Уведомления — типовой поддомен, а повлиять на сервис школа не может. Какой шаблон разумнее?
3 В каких случаях предохранительный слой оправдан? Выберите все верные варианты.Можно выбрать несколько ответов
4 Обучение отдаёт расписание занятий потока в формате iCalendar, и студенты подписываются на него в любом календаре. Какие шаблоны здесь видны?
5 Платёжная система прислала уведомление с состоянием PARTIALLY_REFUNDED, которого нет в документации, на которую опирался слой. Что должен сделать предохранительный слой?
6 Сколько копеек получится, если перевести сумму «1.15» через int(float("1.15") * 100)? (целое число)
  • Карта контекстов показывает, какие контексты есть и как они связаны: кто upstream, кто downstream и по какому шаблону. Её рисуют «как есть» и лучше несколькими маленькими картами под конкретные вопросы.
  • Отношения команд бывают взаимозависимыми, upstream/downstream и свободными. Upstream — тот, чьи решения расходятся по течению, а не тот, кто важнее.
  • Девять шаблонов: партнёрство и общее ядро (сотрудничество); заказчик–поставщик, конформист и предохранительный слой (upstream/downstream); служба с открытым протоколом и общедоступный язык (открытость); раздельные пути и пометка «большой ком грязи».
  • Шаблон выбирают по трём вопросам: влияем ли мы на upstream, хороша ли и стабильна ли его модель, важен ли наш контекст. Предохранительный слой — для чужих моделей, угрожающих ядру; он не бесплатен.
  • Оплату школы купили и защитили предохранительным слоем: адаптер говорит с платёжной системой, переводчик превращает её ответы в PaymentResult, Money и событие ПлатёжПолучен, и дальше слоя чужие слова не проходят.

Дальше: урок DDD.2.3 — Event Storming. Выложим процесс «записаться на поток и оплатить» на стену событий вместе с экспертами, найдём горячие точки, закроем открытые вопросы глоссария и увидим, как из стикеров вырастают контексты и агрегаты.

Закончили урок?Урок пройден

Отметьте его — и 5 карточек из урока попадут в колоду повторения. · 5 карточек — в колоде повторения

Первоисточники 5

  • EN
    DDD Crew — Context Mapping (README и Context Map Cheat Sheet)
    README: три вида отношений команд (Mutually Dependent, Upstream/Downstream, Free), девять шаблонов с цитатами из справочника Эванса, советы «маленькие карты под конкретный вопрос». Шпаргалка resources/context-map-cheat-sheet.png. Текст README — CC BY 4.0, сама шпаргалка помечена CC BY-SA 4.0адаптация · CC-BY-SA-4.0
  • EN
    Eric Evans — Domain-Driven Design Reference (2015), раздел Context Mapping for Strategic Design
    определения Context Map, Partnership, Shared Kernel, Customer/Supplier, Conformist, Anticorruption Layer, Open-host Service, Published Language, Separate Ways, Big Ball of Mud; подробно — глава 14 «синей книги»пересказ
  • EN
    Vaughn Vernon — Domain-Driven Design Distilled (2016), глава 4
    глава «Strategic Design with Context Mapping»: шаблоны карты контекстов на примерах и способы интеграции контекстов; рус. пер. — «Предметно-ориентированное проектирование: самое основное». Ссылка — по списку литературы SAP curated-resources-for-domain-driven-designпересказ
  • EN
    Vlad Khononov — Learning Domain-Driven Design (O’Reilly, 2021), глава 4
    глава «Integrating Bounded Contexts»: шаблоны сгруппированы в сотрудничество (партнёрство, общее ядро), заказчик–поставщик (конформист, предохранительный слой, служба с открытым протоколом) и раздельные путипересказ
  • EN
    Context Mapper — примеры карт контекстов в текстовом виде
    карты контекстов, описанные на языке CML и хранимые в репозитории; генерация схем из текста. Инструмент упомянут в README DDD Crewссылка · Apache-2.0

Лицензия текста урока: CC-BY-SA-4.0 · обновлено 2026-10-06