DarkRiDDeR12 мин

Полевой разбор границ: три импорта и одна карта модульного монолита

АрхитектураИнженерные практики

Проблема полевого разбора обычно выглядит невинно: в pull request есть три маленьких импорта. Первый берёт карточку товара, второй — приватный formatter, третий просит платёжный модуль вызвать checkout обратно. Каждый отдельно можно объяснить дедлайном. Вместе они меняют карту: одна связь расширяет API, другая привязывает consumer к детали, третья замыкает направление. Цена ошибки — не «нарушение чистоты». Следующее изменение оплаты или каталога начинает требовать координации нескольких областей, а ревью вынуждено вспоминать неявные договорённости вместо того, чтобы проверить правило.

Ниже не история о реальном репозитории и не результат CI. Это synthetic кейс с фиксированными именами `catalog`, `checkout`, `payments`, `notifications`. Он нужен, чтобы показать порядок разбора перед тем, как открыть настоящий код. Кейс не использует production-трассы, файлы, git, network, HTTP или чужие метрики. Его сила не в реалистичных цифрах, а в том, что каждый вывод привязан к тройке `source → target.surface` и может быть отвергнут, если данных для него нет.

Три входа в review

Первый synthetic импорт: `checkout → catalog.api`. Он попадает в разрешённую матрицу. Проверка не заканчивается словом «зелёный»: reviewer уточняет, что checkout действительно просит опубликованную каталожную возможность, а не переносит туда вычисление заказа. Действие — оставить связь и зафиксировать owner API. Второй: `checkout → catalog.internal`. Он может решать ровно ту же ближайшую задачу, но нарушает поверхность. Проверка показывает, что target не равен `catalog.api`. Действие — не делать internal публичным по умолчанию; сначала выбрать, нужен ли новый узкий метод или логика должна остаться у каталога.

Третий synthetic импорт: `payments → checkout.api`. Его surface формально публична, но направления нет в карте. Если одновременно `checkout → payments.api` уже существует, появляется цикл. Симптом — не ошибка компиляции в учебной модели, а невозможность объяснить, кто владеет процессом между оплатой и checkout. Причина — новая обратная связь добавлена как техническая деталь. Проверка — нарисовать оба ребра и рассмотреть владение состоянием. Действие — вынести orchestration в одну сторону или спроектировать явный event contract; не добавлять взаимную зависимость с обещанием «потом разберёмся».

Ledger учебного review
СсылкаСимптомПроверка моделиПредлагаемое действиеЧто остаётся неизвестным
checkout → catalog.apiновый межмодульный вызовsurface и direction есть в fixed картеоставить как candidate API contractсодержимое реального API, нагрузка и права
checkout → catalog.internalconsumer тянет implementation detailsurface не совпадает с опубликованным catalog.apiвернуть вызов владельцу либо спроектировать узкий APIпочему internal пока технически доступен
payments → checkout.apiобратная зависимость к уже используемому consumerdirection не разрешён; вместе с checkout → payments образует synthetic cycleназначить orchestration или event contract до кодакакой вариант соответствует реальному домену
catalog → payments.apiбоковой обход без сценарияpublic surface есть, direction отсутствуетотклонить до появления объяснимой потребностинужен ли другой владелец процесса
любой → unknown.apiкарта не знает получателяunknown-moduleобновить модель только после отдельного reviewсуществует ли такой компонент в кодовой базе
Петля boundary review: fixed synthetic input проходит проверку формы, public surface, разрешённого направления и цикла, затем выдаёт только proposal на review карты. Отдельной подписью указано: нет чтения файлов, CI, сети, trace или production.
Схема показывает порядок учебной проверки. Она не является pipeline, отчётом CI или следом фактического импорта.

Не путать API с разрешением на любой сценарий

Самая неудобная часть review — допустимый API-вызов может быть неверным по сути. `checkout → catalog.api` проходит boundary rule, но API всё ещё может отдавать слишком много данных, скрывать медленную операцию или использовать чужой инвариант. Архитектурная проверка отвечает на узкий вопрос: разрешено ли этому consumer обращаться к этой заявленной поверхности. Она не отвечает на продуктовый вопрос, корректность данных или performance. Это разделение экономит время: rule не раздувают до имитации полного design review, а критические свойства проверяют отдельными доказательствами.

Обратная ошибка — считать, что возникший internal import обязательно доказывает плохой модуль. Иногда consumer обнаружил настоящую недостающую возможность. Но это повод спросить владельца, не повод объявить formatter стабильным контрактом. Хорошее действие сохраняет выбор обратимым: ввести временный adapter с именем потребности, перевести один consumer, затем решить судьбу API при известном сценарии. Плохое действие — экспортировать весь internal namespace или добавить shared package без owner и срока пересмотра.

Fixed fixture как защита от самообмана

Функция `inspectSyntheticModuleBoundaries()` намеренно принимает только versioned fixed карту и список reference records в памяти. Она отвергает неизвестный `repositoryUrl`, изменённый module map и поле `path` в ссылке. Эти случаи не «плохие данные реального проекта»; это охрана границы примера. Если fixture принял бы URL или путь, читатель мог бы решить, что он смотрит на исходники. Вместо этого функция отказывается от такого входа и оставляет `filesystem=not-inspected`, `network=not-used`, `projectScan=not-performed`.

import { runModularMonolithFixture } from './upgrade-2024-02.mjs';

const report = runModularMonolithFixture();
if (!Object.values(report.assertions).every(Boolean)) throw new Error('fixture failed');

console.log(report.samples.valid.model.allowedDirections);
console.log(report.samples.internalReference.model.violations[0]);
// non-public-or-unknown-surface:checkout>catalog:catalog.internal

// Внутри модуля только fixed JS-данные. Нет чтения файлов, package graph,
// репозитория, CI, сети, HTTP, trace или production-конфигурации.

node web/scripts/upgrade-2024-02.mjs --verify-fixture

# PASS подтверждает лишь согласованность fixed synthetic политики и отрицательных веток.

Набор assertions покрывает и обычную ветку, и отрицательные случаи: non-synthetic input, лишнее network-like поле, другой map version, подмена API, internal surface, forbidden direction, unknown module, duplicate link, malformed record, self-dependency и directed cycle. Вход обязан содержать точный набор полей и плотный список ссылок: пустой слот не должен пройти потому, что `every()` его пропустил. Перед восстановлением snapshot снова сверяется с versioned fixed картой; report с `accepted=true`, но неполным snapshot отклоняется. Восстановление возвращает копию договора, не делает rollback исходников, не меняет CI и не управляет релизом. Такой fixture пригоден для сопровождения текста: он проверяет, что автор не противоречит собственной карте. Он не заменяет source analysis.

Маршрут разбора в настоящем review

  1. Остановите спор на точной ссылке. Выпишите consumer, owner, surface и цель вызова; «модули связаны» не является проверяемым описанием.
  2. Отделите факт от модели. Реальный import, если он обнаружен разрешённым инструментом, храните как evidence отдельно. Synthetic fixture не приклеивайте к нему как доказательство.
  3. Сверьте public surface. Если вызывается internal, выберите: локальная операция у owner-а, узкий API или изменение ownership. Не экспортируйте детали автоматически.
  4. Сверьте направление и цикл. Добавьте новую стрелку в карту до merge. Обратная связь требует объяснить orchestration, а не только новый интерфейс.
  5. Выберите наименьшее обратимое действие. Переведите одного consumer-а, сохраните путь назад и критерий удаления временного адаптера.
  6. Запишите решение. В decision record оставьте владельца, scenario, API, allowed direction, исключения, дату пересмотра и evidence, которое действительно было получено.

Граница проверки и ответственность reviewer-а

Reviewer не обязан предсказать всю будущую архитектуру. Его обязанность — не подписывать неясную связь как «просто import». Если аргумент строится на реальной трассе, сборке, file scan или production-эффекте, нужно проверить их в соответствующей разрешённой системе и назвать источник. Нельзя заменить этот труд скриншотом диаграммы или PASS fixture. В обратную сторону тоже важно: отсутствие файлового скана в учебном пакете не доказывает отсутствия нарушения. Это значит ровно то, что написано — такой scan не выполнялся.

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

Ограничения и следующий шаг

Synthetic кейс не доказывает архитектуру, чистоту кода, реальный список imports, результаты тестов, CI, трассы, performance, безопасность или delivery. Он не читает project files, package manager, environment, network, HTTP, trace, clock или production. Ссылки на Java SE 17, Spring Modulith и ArchUnit дают исторический и инструментальный контекст: они не назначают boundaries вашего домена. Реальная проверка должна пользоваться точечным анализом в пределах полномочий команды и оставлять evidence рядом с решением.

Следующий шаг — сделать один boundary review не как обсуждение названий папок, а как запись из пяти колонок: ссылка, scenario, public surface, direction, removal plan. После этого можно выбрать реальный анализатор и сформулировать его rule на языке проекта. Если rule не умеет отличить `api` от `internal`, сначала улучшайте карту. Инструмент должен проверять уже понятную политику, а не изобретать её по import-ам.

Историческая граница февраля 2024

На феврале 2024 доступны Java SE 17, Spring Modulith 1.1.0 и ArchUnit 1.1.0. Этого достаточно, чтобы говорить о явных поверхностях и структурной проверке, но недостаточно для заявлений о runtime behaviour несуществующей системы. Автор M7 в этом разборе не выдает synthetic sample за production-кейс: он показывает цену ссылки, границу модели и следующий воспроизводимый вопрос для review.

Проверяемые источники

  • Java Language Specification, Java SE 17, chapter 7: Packages and Modules — Первичный нормативный текст Oracle для Java SE 17: иерархия имён пакетов сама по себе не создаёт привилегированный доступ; именованный модуль явно задаёт exported packages и зависимости. Это правило языка Java, а не готовая модель предметных модулей для любого стека.
  • Spring Modulith 1.1: Fundamentals — Версионная официальная документация Spring Modulith 1.1, выпущенного 24.11.2023: показывает module API, internal packages и allowed dependencies. Это framework-specific пример для Spring Boot, а не доказательство устройства неизвестного репозитория и не требование применять Spring.
  • Spring Modulith 1.1.0 release, 24.11.2023 — Официальный release проекта фиксирует, что версия 1.1.0 существовала до февраля 2024. Он подтверждает историческую доступность версии, но не подтверждает состав или поведение чужого приложения.
  • ArchUnit 1.1.0 release, 09.08.2023 — Официальный release инструмента архитектурных тестов существовал до февраля 2024. Он показывает, что проверка структурных правил могла быть выделена в тест, но не делает конкретный DSL универсальным и не доказывает запуск CI.