Карта контекстов: как контексты живут вместе
Что такое карта контекстов, кто upstream, а кто downstream, девять шаблонов отношений между контекстами и командами — от партнёрства до «большого кома грязи», как выбрать шаблон по силе команд и стабильности чужого API и как предохранительный слой не пускает модель внешней платёжной системы в код онлайн-школы.
Зачем это нужно
Заголовок раздела «Зачем это нужно»Первую версию оплаты в онлайн-школе написали за два дня: прочитали документацию платёжной системы и стали
использовать её ответы прямо в коде. В Наборе появилось if payload["state"] == "CAPTURED", в отчётах —
order_ref, в письмах — сумма float(payload["amount"]). Через год код с полями платёжной системы нашёлся в
четырнадцати местах. Потом платёжная система выпустила вторую версию API и переименовала состояния. Школа узнала об
этом утром, когда записи перестали подтверждаться. А когда финансовый директор предложил перейти к поставщику
подешевле, команда оценила переезд в три месяца: чужая модель проросла во все контексты.
Контексты из прошлого урока не живут поодиночке. Набору нужны цены из Каталога, Оплата зависит от платёжной системы, Обучению нужно знать, кто подтвердил запись. Каждая такая связь — это не только вызов функции, но и отношения между моделями и между командами: кто под кого подстраивается, кто может попросить изменения, кто узнаёт о поломке последним. Карта контекстов делает эти отношения видимыми, а шаблоны дают им имена и готовые решения. Главное из них для школы — предохранительный слой вокруг Оплаты. Его вы напишете в практике.
Карта контекстов
Заголовок раздела «Карта контекстов»Карта контекстовкарта контекстовEN context mapСхема и описание связей между ограниченными контекстами и командами: кто от кого зависит (upstream или downstream) и по какому шаблону отношений они взаимодействуют. Рисуют местность как есть, а не как хотелось бы. (context map) — схема и описание того, какие ограниченные контексты есть в системе и как они связаны: кто от кого зависит и по какому шаблону отношений. Эванс советует называть каждый контекст и делать эти имена частью единого языка, описывать точки соприкосновения моделей и явно указывать, где модели переводятся, где что-то общее, а где — изоляция. И добавляет важное правило: наносите на карту местность как она есть, а преобразования оставьте на потом. Карта, на которой нарисовано желаемое, бесполезна: она прячет проблемы.
Upstream и downstream
Заголовок раздела «Upstream и downstream»Главное понятие карты — направление влияния, 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
Несколько пояснений:
- Набор и Оплата — в одной команде (см. схему команд в прошлом уроке), поэтому договориться им легко: команды и
событие между ними — внутренняя договорённость, а
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["Предохранительный слой"]То же в виде таблицы — по силе команд и стабильности API:
| Ситуация | Разумный шаблон | Почему |
|---|---|---|
| у команд общий руководитель и общие цели | заказчик–поставщик | поставщик может и хочет учесть ваши нужды |
| поставщик — внешняя компания или огромная платформа, его модель хороша и стабильна, ваш контекст типовой | конформист | перевод не окупится; общий язык с поставщиком — даже плюс |
| поставщик вас не слушает, его модель чужая или часто меняется, а ваш контекст важен | предохранительный слой | чужие изменения и слова остаются в одном месте |
| у вашего контекста много потребителей | служба с открытым протоколом и общедоступный язык | один документированный протокол дешевле десяти частных интеграций |
| две части ломаются только вместе | партнёрство | без совместного планирования релизы будут срываться |
| небольшая часть модели обязана совпадать до бита | общее ядро | дешевле договариваться, чем синхронизировать две копии |
| интеграция дороже пользы | раздельные пути | ручная работа или дублирование дешевле |
| upstream — ком грязи | предохранительный слой | грязь не должна проходить дальше границы |
Шаблоны меняются со временем. Пока платёжная система была одна и стабильна, можно было жить конформистом. Когда школа захотела второго поставщика для рассрочки, без предохранительного слоя стало не обойтись. Карта, на которой видны такие решения, помогает пересматривать их осознанно, а не узнавать о них из аварии.
Почему Оплату покупают и защищают предохранительным слоем
Заголовок раздела «Почему Оплату покупают и защищают предохранительным слоем»В уроке DDD.1.3 мы отнесли Оплату к типовым поддоменам и решили купить платёжный сервис, а свой код свести к адаптеру и слою перевода. Теперь видно, почему именно так:
- Типовой поддомен. Безопасность карт, банки, чеки и требования закона давно решены платёжными сервисами. Писать это самим — тратить силы не на ядро.
- Мы downstream без влияния. Платёжная система — служба с открытым протоколом для тысяч магазинов. Под нужды одной школы она API не поменяет: заказчиком–поставщиком тут не стать.
- Чужая модель, чужие слова. У поставщика «заказ» (
order_ref), состоянияAUTHORIZEDиCAPTURED, суммы строкой в рублях и коды валют числами. В глоссарии школы «заказа» нет вовсе — это запрещённый синоним записи на поток. Стать конформистом значит впустить эти слова в ядро. - Чужой график изменений и риск смены поставщика. Новая версия 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.py | HTTP-запросы к платёжной системе, проверка подписи уведомлений, повторы | только поставщика |
| переводчик | 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, — а переводчик формирует запрос в формате поставщика. Мы сосредоточимся на входящем направлении: оно
опаснее, потому что чужие данные приходят к нам сами.
Хороший предохранительный слой подчиняется трём правилам:
- Не угадывать. Незнакомое состояние, пропавшее поле, сумма в странном формате — это отказ с понятным сообщением, а не «наверное, оплачено». Молчаливое угадывание и есть тот самый тихий баг.
- Не пропускать чужое. За слоем нет
order_ref,txn_id,CAPTUREDи сырых словарей. Лишние поля поставщика — номер карты, комиссия — просто не переносятся. - Не терять смысл. Деньги — целые копейки, время — с часовым поясом, номер счёта проверен.
Практика
Заголовок раздела «Практика»Напишите переводчик предохранительного слоя. В заготовке уже есть язык школы — Money, PaymentOutcome (итог
платежа: «ждём платёж», «платёж получен», «платёж не прошёл», «деньги возвращены»), PaymentResult и событие
PaymentReceived — и выдержка из документации платёжной системы. Обратите внимание: PaymentOutcome и
PaymentResult — слова контекста Оплата, которых нет в глоссарии Набора. Так и должно быть: у каждого контекста свой
язык, и Оплата говорит о платежах подробнее.
Вам нужно написать:
STATE_TO_OUTCOMEиtranslate_state— перевод шести состояний поставщика, отказ на незнакомом;parse_money— сумма строкой и цифровой код валюты по ISO 4217 →Moneyв копейках, безfloat;translate_response— всё уведомление →PaymentResultс проверкой обязательных полей, номера счёта и времени;received_event— событие ПлатёжПолучен только тогда, когда деньги действительно получены.
Тесты проверяют перевод, отказы на незнакомых состояниях и испорченных ответах и то, что чужие термины не просачиваются в результат.
"""Предохранительный слой между контекстом Оплата и внешней платёжной системой.
Платёжная система присылает уведомления о платежах в своём формате и своими словами.
Слой переводит их на язык школы — и дальше этого файла чужие поля, статусы и слова не проходят.
"""
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? Сохранить уведомление,
сообщить дежурному и бухгалтерии — и вместе с экспертами решить, как назвать это на языке школы.
Проверь себя
Заголовок раздела «Проверь себя»- Карта контекстов показывает, какие контексты есть и как они связаны: кто upstream, кто downstream и по какому шаблону. Её рисуют «как есть» и лучше несколькими маленькими картами под конкретные вопросы.
- Отношения команд бывают взаимозависимыми, upstream/downstream и свободными. Upstream — тот, чьи решения расходятся по течению, а не тот, кто важнее.
- Девять шаблонов: партнёрство и общее ядро (сотрудничество); заказчик–поставщик, конформист и предохранительный слой (upstream/downstream); служба с открытым протоколом и общедоступный язык (открытость); раздельные пути и пометка «большой ком грязи».
- Шаблон выбирают по трём вопросам: влияем ли мы на upstream, хороша ли и стабильна ли его модель, важен ли наш контекст. Предохранительный слой — для чужих моделей, угрожающих ядру; он не бесплатен.
- Оплату школы купили и защитили предохранительным слоем: адаптер говорит с платёжной системой, переводчик превращает
её ответы в
PaymentResult,Moneyи событие ПлатёжПолучен, и дальше слоя чужие слова не проходят.
Дальше: урок DDD.2.3 — Event Storming. Выложим процесс «записаться на поток и оплатить» на стену событий вместе с экспертами, найдём горячие точки, закроем открытые вопросы глоссария и увидим, как из стикеров вырастают контексты и агрегаты.