8 lines
17 KiB
JSON
8 lines
17 KiB
JSON
{
|
||
"index": 140,
|
||
"slug": "editorial-2024-02-mechanism-modular-monolith",
|
||
"title": "Модульный монолит: как удержать границы до распила на сервисы",
|
||
"excerpt": "Папки по доменам не защищают код от скрытых связей. Разбираем публичную поверхность модуля, разрешённые направления, цикл зависимостей и проверку, которую можно встроить в обычный review.",
|
||
"contentHtml": "<p>В монолите проблема часто начинается с маленького импорта. Код оформления заказа берёт внутренний formatter каталога, потому что нужный API ещё не готов. Потом платежи вызывают helper оформления заказа, а общий пакет получает третий набор исключений. Сборка проходит. Папки по-прежнему выглядят как отдельные домены.</p>\n<p>Симптом появляется позже: изменение внутреннего типа каталога требует искать потребителей в заказах и платежах. Ревьюер не может ответить, какой вызов разрешён, а какой только терпят ради срока. Цена ошибки — скрытый контракт. Он увеличивает область каждого изменения, усложняет откат и делает будущий перенос модуля дороже.</p>\n<p>Тезис простой: модульный монолит держится не на глубине каталогов, а на явном договоре. Для каждой связи нужно назвать потребителя, владельца и поверхность доступа: <code>consumer → owner.publicApi</code>. Отдельно нужно перечислить разрешённые направления. Тогда правило можно обсуждать по конкретному вызову, а не по впечатлению от дерева файлов.</p>\n<h2>Что именно считается границей</h2>\n<p>Модуль владеет смыслом операции, своими данными и публичным входом. Публичный вход не равен каждому символу с модификатором <code>public</code>. Это обещание для другого модуля. Им может быть команда, запрос, событие, порт или функция. Вход должен выражать потребность, а не раскрывать внутреннее хранение.</p>\n<p>В учебной модели есть четыре модуля: <code>catalog</code>, <code>checkout</code>, <code>payments</code> и <code>notifications</code>. У каждого есть поверхность <code>*.api</code> и внутренняя часть <code>*.internal</code>. Разрешены только три связи: <code>checkout → catalog.api</code>, <code>checkout → payments.api</code> и <code>payments → notifications.api</code>. Это пример формы правила, а не описание реальной системы.</p>\n<table><caption>Граница читается по четырём вопросам</caption><thead><tr><th>Вопрос</th><th>Пример ответа</th><th>Зачем он нужен</th></tr></thead><tbody><tr><td>Кто вызывает?</td><td><code>checkout</code></td><td>Фиксирует потребителя и его сценарий</td></tr><tr><td>Кто владеет смыслом?</td><td><code>catalog</code></td><td>Назначает ответственность за изменение контракта</td></tr><tr><td>Через что вызывают?</td><td><code>catalog.api</code></td><td>Не даёт подменить API внутренним типом</td></tr><tr><td>Разрешено ли направление?</td><td><code>checkout → catalog</code></td><td>Останавливает случайные обратные связи</td></tr></tbody></table>\n<p>Одна стрелка без поверхности слишком широка. Запись «checkout зависит от catalog» допускает и запрос карточки, и чтение репозитория, и вызов приватного форматтера. Запись <code>checkout → catalog.api</code> задаёт проверяемый объект. Если потребителю нужен новый смысл, владелец решает, добавлять ли узкий метод, событие или оставить операцию внутри своего модуля.</p>\n<figure><img src='/assets/editorial/2024/modular-monolith-2024-allowed-directions.svg' alt='Схема разрешённых направлений модульного монолита: checkout вызывает catalog.api и payments.api, payments вызывает notifications.api, обращение к catalog.internal запрещено' loading='lazy' /><figcaption>Учебная схема показывает направление и поверхность связи. Она не является результатом сканирования исходников и не описывает production-систему.</figcaption></figure>\n<h2>Механизм: поверхность плюс направленный граф</h2>\n<p>Сначала команда описывает карту модулей. Для каждого модуля она записывает имя, публичную поверхность, внутренние пакеты и владельца. Затем добавляет разрешённые рёбра. Проверка каждой ссылки отвечает на четыре вопроса: существует ли источник, существует ли получатель, совпадает ли поверхность с опубликованной и есть ли такое направление в карте.</p>\n<p>Направление нужно хранить отдельно от физического пути. В одном языке internal-пакет можно закрыть средствами компилятора, в другом останется только соглашение и архитектурный тест. Оба слоя полезны. Видимость защищает от части ошибочных обращений, а карта объясняет, почему разрешён сам маршрут.</p>\n<pre><code>const allowed = new Set(['checkout>catalog:catalog.api', 'checkout>payments:payments.api', 'payments>notifications:notifications.api']); function check(link) { if (link.surface !== link.to + '.api') return 'non-public surface'; return allowed.has(link.from + '>' + link.to + ':' + link.surface) ? 'allowed' : 'forbidden direction'; }</code></pre>\n<p>Код выше — учебный пример проверки заранее описанной карты. Он не читает репозиторий, не строит граф импортов и не доказывает отсутствие нарушений в приложении. В настоящем проекте анализатор должен получить фактические ссылки из подходящего инструмента языка, сопоставить их с картой и сохранить результат проверки. Если такого анализа пока нет, честный результат — «карта описана, фактические импорты не проверены».</p>\n<p>Цикл проверяют на том же графе. Если карта разрешает <code>checkout → payments</code>, а затем добавляет <code>payments → checkout</code>, две области начинают знать друг о друге. Цикл не означает, что нужно немедленно выделить микросервис. Он означает, что не назван владелец процесса. Сначала уточняют orchestration, границу инварианта и направление обмена. Иногда помогает событие. Иногда — перенос операции к владельцу. Иногда — узкий контракт без обратного вызова.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><caption>Практическая диагностика границы</caption><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>Потребитель импортирует <code>catalog.internal</code></td><td>API не выражает нужную операцию или деталь показалась удобнее</td><td>Сверить surface вызова со списком API и назвать сценарий</td><td>Вернуть операцию владельцу либо добавить узкий контракт</td></tr><tr><td>Появилась обратная стрелка</td><td>Не определён владелец процесса или смешаны ответственности</td><td>Построить граф и найти цикл</td><td>Выбрать orchestration, событие или перенос операции</td></tr><tr><td>Все импортируют <code>common</code></td><td>Общий пакет стал обходом границы</td><td>Проверить, кто владеет каждым типом и кто меняет его</td><td>Разделить контракты или вернуть код владельцу</td></tr><tr><td>API повторяет таблицы владельца</td><td>Публичная поверхность раскрывает реализацию</td><td>Проверить, может ли владелец изменить хранение без consumer</td><td>Сузить данные до операции, результата или события</td></tr><tr><td>Тест зелёный, но импорт неизвестен</td><td>Проверена только модель, а не исходный код</td><td>Проверить источник фактических ссылок и дату evidence</td><td>Не выдавать модель за аудит; добавить реальный анализ</td></tr></tbody></table>\n<h2>Порядок внедрения</h2>\n<ol><li><strong>Выберите один болезненный стык.</strong> Возьмите изменение, которое регулярно цепляет чужую внутренность. Не начинайте с перестройки всего монолита.</li><li><strong>Запишите потребность.</strong> Назовите consumer, ожидаемый результат и модуль-владелец. Если результат нельзя описать без внутреннего класса, граница ещё не сформулирована.</li><li><strong>Опишите поверхность.</strong> Оставьте минимальный вход: команду, запрос, событие или порт. Не публикуйте namespace целиком.</li><li><strong>Добавьте направление.</strong> Запишите <code>from → to.surface</code> и отдельно укажите запрещённую обратную связь. У каждого исключения должен быть владелец и дата пересмотра.</li><li><strong>Проверьте существующие ссылки.</strong> Используйте анализатор языка, правила сборки или архитектурный тест, который видит реальные импорты. Учебная карта сама по себе этого не делает.</li><li><strong>Переведите один вызов.</strong> Оставьте обратимый путь, проверьте отсутствие старого потребителя и только потом удаляйте внутренний доступ.</li><li><strong>Закрепите правило.</strong> Добавьте проверку в место, где она запускается вместе с изменением кода. Документ без проверки быстро становится устным соглашением.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Граф границ не отвечает за транзакции, задержку, права доступа, размер payload, версионирование событий и качество данных. Разрешённая стрелка может вести к медленной операции. Запрещённая стрелка может стать оправданной после смены владельца. Поэтому зелёный статус архитектурной проверки не заменяет нагрузочный, security или интеграционный тест.</p>\n<p>Отрицательный путь нужно сохранять рядом с правилом. Вызов <code>catalog.internal</code> должен завершаться понятным отказом, а не молча проходить через исключение. Неизвестный модуль, дубликат связи и цикл тоже должны иметь отдельные сообщения. Если проверка пропускает пустую поверхность или принимает произвольный путь к файлу, она защищает только видимость, но не границу.</p>\n<p>Java Platform Module System даёт физический пример: именованный модуль объявляет экспортируемые пакеты и зависимости. Spring Modulith показывает похожую идею для Java/Spring: API модуля отделяется от внутренних пакетов и разрешённых зависимостей. Эти механизмы нельзя перенести в любой стек без изменений. Их полезный общий принцип уже достаточен: доступ должен быть назван, ограничен и проверяем.</p>\n<h2>Критерий готовности</h2>\n<p>Граница готова, если для каждого межмодульного вызова команда может показать четыре записи: сценарий потребителя, владельца смысла, точную публичную поверхность и разрешённое направление. Реальная проверка должна пройти по фактическим ссылкам и отдельно показать отрицательные случаи: internal-протечку, неизвестный модуль и цикл. Учебная модель может проверить только формулировку правила и обязана так себя называть.</p>\n<p>Если один из четырёх ответов отсутствует, не расширяйте API и не выделяйте сервис. Сначала уточните владельца или оставьте код локальным. Модульный монолит приносит пользу именно в этот момент: команда получает ясную границу и может менять внутренность без скрытых потребителей.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href='https://docs.oracle.com/javase/specs/jls/se17/html/jls-7.html' target='_blank' rel='noopener noreferrer'>Java Language Specification, Java SE 17, глава 7: Packages and Modules</a> — официальное описание пакетов, модулей, экспортов и зависимостей.</li><li><a href='https://docs.spring.io/spring-modulith/reference/fundamentals.html' target='_blank' rel='noopener noreferrer'>Spring Modulith: Fundamentals</a> — официальная документация о module API, внутренних пакетах и разрешённых зависимостях.</li><li><a href='https://www.archunit.org/userguide/html/000_Index.html' target='_blank' rel='noopener noreferrer'>ArchUnit User Guide</a> — официальный guide для выражения архитектурных правил в тестах Java.</li></ul>"
|
||
}
|