Представьте, что модули изолированы по каталогам, но разработчики регулярно обходят правила зависимостей. Как сделать архитектурную границу проверяемой автоматически?
Границу следует выразить как исполняемое архитектурное правило: автоматическую проверку допустимых зависимостей, публичных точек входа и запрещённых обращений. Такая проверка запускается в CI и блокирует изменение, нарушающее архитектурный контракт.
Одной проверки структуры каталогов недостаточно: важно анализировать реальные зависимости компонентов, включая импорты, вызовы, ссылки на типы и иногда зависимости через инфраструктуру.
Архитектурные схемы обычно описывают намерения команды, но не гарантируют, что код им следует. Со временем появляются обходные зависимости, новые исключения и общие утилиты, из-за чего фактическая архитектура начинает расходиться с проектной.
Подход эволюционной архитектуры и архитектурных проверок возник как ответ на эту проблему. Его цель — сделать архитектурные свойства проверяемыми на каждом изменении, а не полагаться только на начальное проектирование и ручной контроль.
Допустим, система разделена на доменный, прикладной и инфраструктурный модули. Команда договорилась, что доменный модуль не зависит от инфраструктурного, но формально это правило нигде не закреплено.
Через несколько итераций разработчик может напрямую использовать клиент базы данных или класс внешнего сервиса из доменного кода. Сборка при этом будет успешной, функциональные тесты могут пройти, а нарушение проявится только при попытке заменить инфраструктуру, выделить модуль или независимо его протестировать.
Ручная проверка на ревью плохо масштабируется: ревьюер может не увидеть транзитивную зависимость или не знать о старом архитектурном ограничении. Если правило не проверяется автоматически, его соблюдение постепенно становится необязательным.
Сначала формулируют архитектурные инварианты в проверяемом виде. Например: доменный модуль не может зависеть от инфраструктурного; модуль заказов не может обращаться к внутренним компонентам модуля оплаты; коммуникация между подсистемами проходит только через опубликованный контракт.
Затем строят граф зависимостей и проверяют его на каждом изменении. Проверка должна учитывать не только расположение файлов, но и фактические связи между пакетами, сборочными единицами, библиотеками или сервисами. При нарушении CI сообщает конкретный путь зависимости и не пропускает изменение без явного решения команды.
Полезно разделять несколько видов правил:
Для этого применяют статический анализ, анализ графа зависимостей и специализированные архитектурные тесты. В распределённой системе дополнительно проверяют владение схемами, допустимые направления вызовов и наличие обратной совместимости контрактов, но такие проверки уже не сводятся к одному графу исходного кода.
Важно различать архитектурное правило и случайное ограничение инструмента. Если зависимость действительно нужна, её не следует бездумно запрещать: нужно пересмотреть границу, выделить стабильный порт или оформить документированное исключение с владельцем и сроком пересмотра.
Исключения должны быть редкими, явными и наблюдаемыми. Без этого механизм превращается в список постоянных разрешений, который формально сохраняет зелёный статус CI, но перестаёт защищать архитектуру.
Основной компромисс — стоимость поддержки правил. Слишком простые проверки пропускают обходы, а слишком строгие создают шум и замедляют изменения. Практичный вариант — начинать с нескольких наиболее ценных инвариантов, измерять ложные срабатывания и расширять набор по мере появления реальных архитектурных рисков.
В системе расчёта тарифов были модули расчёта, заказов и интеграций. На диаграмме модуль расчёта не зависел от интеграций, но несколько его классов напрямую использовали клиент внешнего поставщика тарифов. После этого замена поставщика стала затрагивать доменную логику.
Рассматривались три варианта. Полагаться только на ревью было дёшево, но нарушение уже однажды прошло незамеченным. Проверять только структуру каталогов было проще, однако разработчики могли обходить правило через публичные типы из соседнего модуля. Полностью запретить все зависимости между модулями было надёжно, но исключало необходимые взаимодействия через стабильные контракты.
Выбрали архитектурные проверки с разрешёнными направлениями зависимостей и отдельным списком публичных контрактов. Для интеграционного модуля оставили порт, доступный прикладному слою, а прямые ссылки доменного кода на инфраструктуру запретили.
В результате нарушение стало обнаруживаться до слияния изменений. При этом правило не запрещало саму интеграцию: оно заставляло проводить её через предусмотренную границу, поэтому замена поставщика больше не требовала изменений в расчётной модели.
Нет. Каталоги — лишь один из способов организации кода и не обязательно совпадают с реальными компонентами. Зависимость может проходить через общий тип, транзитивную библиотеку, регистрацию компонента или публичный класс, размещённый в формально допустимом каталоге. Надёжная проверка должна анализировать фактический граф зависимостей и явно определять, что считается границей.
Исключение нужно сделать явным: указать зависимость, причину, владельца и условие её удаления или пересмотра. Временное разрешение можно ограничить конкретным модулем или направлением, чтобы оно не стало общим обходным путём. Если исключения постоянно повторяются, это сигнал, что правило или сама граница спроектированы неверно.
Проверка подтверждает только заранее выбранные свойства. Она может гарантировать отсутствие запрещённых импортов, но не показать, что интерфейс слишком широк, модуль владеет неподходящей ответственностью или взаимодействие создаёт скрытую связанность через общую базу данных.
Поэтому автоматические правила дополняют ревью, тестирование контрактов и наблюдение за изменяемостью системы. Их задача — не заменить архитектурное мышление, а дешёво и постоянно защищать наиболее важные инварианты от случайной деградации.