08 июл. 2025 г.·5 мин чтения

Отсутствие границ модулей в проектах с генерируемым кодом

Отсутствие границ модулей превращает небольшие изменения в генерируемом коде в широкие диффы. Узнайте признаки, простой план исправления и быстрые проверки перед каждым слиянием.

Отсутствие границ модулей в проектах с генерируемым кодом

Почему проблема быстро разрастается

Слабые границы превращают небольшие изменения в огромные диффы.

Разработчик открывает один файл, чтобы обновить форму, а в итоге меняет общий помощник, папку utils, скрипт сборки и маппер данных в другой части проекта. Генерируемый код делает такое разрастание видимым, но обычно не создаёт проблему. Истинная беда — проект без чёткой границы между логикой UI, бизнес-правилами, структурами данных и скриптами, которые должны были оставаться отдельными.

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

Владение становится размытым сразу после этого. UI-команда правит серверные файлы, потому что валидация лежит рядом с кодом запросов. Бэкенд команда лезет в логику экранов, потому что общие типы находятся в той же папке. Ревью замедляются, и обсуждение смещается от самой правки к спорам о структуре.

Исправления багов тоже ухудшаются. Если баг может скрываться в шести местах, разработчики перестают делать аккуратные патчи. Они либо правят больше файлов, чем нужно, чтобы чувствовать себя в безопасности, либо вообще избегают рискованных файлов. Со временем это приучает команду принимать грязные изменения как норму. Тогда кодовая база начинает насчитывать процент на каждое исправление.

Почему генерируемый код делает грязь заметной

Генераторы копируют вашу структуру быстро. Если шаблон указывает одну фичу в api/, ui/, helpers/ и scripts/, генератор будет писать во все эти места каждый раз.

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

Небольшая правка, например добавление поля в форму, должна оставаться в одном модуле или максимум в двух. В беспорядочном проекте та же правка разливается в валидацию, общие форматтеры, админские экраны, тестовые фикстуры и какой‑то старый скрипт, к которому никто не хотел прикасаться.

Свободные импорты усугубляют ситуацию. Фича тянет из utils/ или common/ один хелпер и невольно подтягивает связи с кодом, который не принадлежит изменению. Широкие хелперы дают тот же эффект: один помощник начинается как удобный, а потом становится точкой пересечения для авторизации, биллинга, джобов и админских экранов. Когда генератор обновляет один модуль, все эти скрытые связи двигаются вместе.

Часто этот паттерн видно в одном сгенерированном коммите. Одна фича правит файлы в четырёх‑пяти папках. Изменение помощника вызывает обновления в несвязанных экранах. Скрипты вне папки фичи тоже меняются. Те же файлы появляются при каждом прогоне генератора.

Именно последний момент важнее всего. Разовый беспорядок неприятен. Генератор повторяет беспорядок каждый раз. Он продолжает учить кодовую базу размазываться.

Как выглядят отсутствующие границы модулей

Эту проблему часто видно ещё до чтения кода. Откройте дерево проекта — и вы видите папки utils, common, shared или misc. Эти имена не говорят, что там должно быть, поэтому люди продолжают бросать туда по одному файлу, пока папка не превращается в ящик для хлама.

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

Блуждающие скрипты — ещё одно предупреждение. Репозиторий набирает файлы вроде scripts/cleanup.js, scripts/fix-users.ts, one-off-import.py и несколько shell-скриптов без владельца. Некоторые читают продакшн‑данные, некоторые патчат битые записи, некоторые переписывают импорты. Все они лежат вне понятного модуля, поэтому никто не знает, какие изменения безопасны, а какие могут сломать рабочий поток.

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

Код оформления заказа показывает это легко. Форма лежит в components/checkout, правила налогов — в utils, задача отправки квитанций — в jobs, а скрипт возврата — в scripts. Добавьте одно правило купона и изменение разольётся по UI, логике ценообразования, джобам и скриптам. Тогда команда не сможет быстро ответить на простой вопрос: "Куда это относить?"

Этот вопрос важнее, чем многие признаются. Если каждая новая задача начинается с спора о папках, люди выбирают ближайшее пустое место и двигаются дальше. Через месяц проект будет выглядеть случайно.

Простой пример на одной фиче

Команда добавляет поле в форму оплаты: "VAT ID". На бумаге это звучит просто: одно поле ввода, одно правило валидации и одна колонка в базе.

В кодовой базе со слабыми границами изменение разрастается быстро. Форма оплаты лежит в общей папке forms, схема API — в shared, а фоновые джобы используют те же типы из всеохватывающего пакета common. Нет папки, которая явно владеет биллингом.

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

Теперь в pull request смешались фронтенд‑форма, локальная валидация, сгенерированные типы запросов и ответов, бэкенд‑обработчики, модели базы, джобы выставления счетов, джобы экспорта и перебор импорта по репозиторию от какого‑то вспомогательного скрипта. Дело не только в размере. Ревью становится расплывчатым: один ревьювер проверяет форму, другой просматривает бэкенд. Никто не уверен в джобах, потому что они выглядят машинно и лежат рядом с безобидной вознёй импортов.

Вот как смешение ответственности превращает задачу из одного поля в рискованное слияние. Неправильная правка может скрыться в шуме: переименованное поле в джобе выставления счетов, старый путь импорта или изменение значения по умолчанию, которое прошло мимо из‑за слишком широкого диффа.

Если одно поле биллинга касается половины репозитория, проблема не в поле. Проблема — в структуре.

Как перерисовать границы

Redraw the Repo Boundaries
Get a practical cleanup plan without rewriting the whole project.

Начните с продукта, а не с кода. Выпишите части, с которыми пользователи взаимодействуют каждый день: аккаунты, биллинг, проекты, уведомления, админка. Каждая часть получает свою папку, и имя папки должно совпадать с языком продукта. billing — ясно. core, utils и misc — нет.

Внутри модуля разделяйте работу по типу. Держите экраны и компоненты в одном месте, бизнес‑правила в другом, джобы и синки — в третьем. Страница оформления заказа не должна сидеть в той же плоской папке, что и правила налогов и ночной скрипт выставления счетов. Эти части меняются по разным причинам, поэтому им лучше жить отдельно.

Простая структура обычно работает лучше продвинутой. Модуль billing с папками ui, domain и jobs подойдёт многим командам. Если вы меняете текст счета — дифф остаётся в ui. Если меняете логику прайсинга — в domain. Если подправляете воркер ретраев — в jobs. Папка сама по себе становится ограничителем.

Команды обычно делают одну и ту же ошибку слишком рано: они переносят код в shared при первом повторении шаблона. Ждите, пока два модуля действительно не начнут нуждаться в одном коде. И даже тогда двигайте минимальный возможный кусок. Форматтер дат может попасть в общий код. Правила статуса счета вряд ли должны туда идти.

Полезно дать каждому модулю простое правило владения. Billing владеет счетами, платежами и налоговыми расчётами. Projects владеет настройкой проекта, статусом и ролями участников. Notifications владеет шаблонами сообщений и задачами доставки. Это быстро решает споры: если код не соответствует правилу, переместите его.

Большого рефакторинга не нужно. Выберите одну фичу, переименуйте папки так, чтобы людям было понятно, разделите UI, правила и фоновые задачи и запишите, чем владеет модуль. После этого новому коду есть куда идти, и диффы перестают разливаться по всему репозиторию.

Как укротить папки, хелперы и скрипты

Stop Shared Folder Sprawl
Replace vague folders with product areas and make reviews easier.

Начните с имён, которые скрывают слишком много. Папки вроде utils, common, shared и misc превращают мелкие правки в поиски по кладовой. Переименуйте их прежде, чем переносить логику. Скучные конкретные имена лучше: billing, auth, notifications, import-jobs.

Блуждающие скрипты тихо расширяют диффы. Скрипт в /scripts, который переписывает файлы биллинга, обновляет шаблоны писем и патчит вывод схемы, за один прогон затронет несвязанные области. Положите этот скрипт рядом с модулем, который он меняет, даже если сначала это выглядит менее аккуратно.

Та же логика помогает и сгенерированному коду. Держите выходные файлы рядом с шаблоном, схемой или конфигом, который их создаёт. Если разработчик правит один исходник, дифф должен остаться в одном районе. Ревьювер должен видеть причину и следствие, не прыгая по всему репозиторию.

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

Глубокие импорты — ещё один признак. Если orders лезет в billing/internal/helpers/format.ts, граница уже нарушена. Импортируйте из публичной точки входа модуля или вынесите действительно общий код в маленькое, явно названное место, которое смогут использовать оба модуля.

Хороший тест прост: если вы меняете одну фичу, можете ли вы примерно предсказать дифф до его открытия? Если нет — папки, хелперы или скрипты всё ещё скрывают слишком много.

Ошибки, которые расширяют диффы

Широкие диффы редко начинаются с одного большого изменения. Они растут из сокращений.

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

Другая проблема — когда генераторы пишут в файлы, которые люди также редактируют вручную. Разработчик правит копию текста, корректирует запрос или добавляет защиту. Следующий прогон генерации перезаписывает файл, и Git показывает шумную смесь ручной работы и машинного вывода. Ревью становятся медленнее, потому что никто не понимает, что изменено намеренно.

Команды также прячут правила продукта в хелперах, потому что utils кажется нейтральным. Тогда правило возврата, лимит регистрации или проверка прав оказываются далеко от модуля‑владельца. Позже одна политика заставляет правки в местах, которые выглядят общими.

Старые скрипты дают тот же разлёт. Репозиторий собирает setup‑скрипты, sync‑скрипты, seed‑скрипты и помощники миграций, которыми никто больше не пользуется. Люди всё равно запускают их «на всякий случай», и эти скрипты продолжают трогать пути и файлы, которые текущий рабочий процесс уже не использует.

Даже перемещение файлов может расширить диффы, если команда никогда не фиксирует владение. Беспорядок просто меняет адрес. Никто не говорит, какой модуль владеет валидацией, API‑формами или фоновыми задачами, поэтому каждая правка всё ещё пересекает границы.

Контраст прост. Чистое изменение checkout затрагивает модуль checkout, его тесты и одну сгенерированную схему. Грязное изменение также правит общие хелперы, корневой maintenance‑скрипт, вручную подправленный сгенерированный файл и несколько перемещённых файлов со старыми импортами. Та же фича — гораздо шире дифф.

Хорошие границы кажутся немного жёсткими. Это обычно знак, что они работают.

Быстрые проверки перед слиянием

Clean Up Generator Paths
Find the inputs and output paths that keep touching unrelated files.

Перед слиянием потратьте десять минут на форму диффа. Отсутствие границ часто видно там первым.

Начните с количества файлов и их распределения. Одна фича должна в основном оставаться внутри одного модуля и близких тестов. Немного общих файлов — нормально. Половина репозитория — нет.

Потом проверьте каждый сгенерированный файл и спросите, какой источник его создал. Каждый выход должен соответствовать одной схеме, шаблону, конфигу или prompt'у. Если никто не может указать источник, поток генерации слишком свободен.

Относитесь к скриптам с повышенным подозрением. Скрипт должен трогать модуль, к которому относится, а не шататься по несвязанным папкам потому что так было удобно.

Попросите ревьювера объяснить каждую изменённую область одной простой фразой. "UI поменялось из‑за новой формы" — ясно. "Эта папка тоже переместилась, потому что генератор вроде бы этого хотел" — тревожный знак.

Ещё один тест: если удалить эту фичу завтра, сможете ли вы откатить её, не сломав другие фичи? Если нет — изменение слишком связано.

Простой пример: добавьте опцию "export CSV" в отчёты. Чистый дифф может затронуть UI отчётов, один сервис отчётов, одно входное описание генератора и сгенерированный клиент отчётов. Грязный дифф ещё поменяет помощники авторизации, корневой maintenance‑скрипт и несвязанные сгенерированные типы. Такое слияние часто создаёт баги через неделю.

Эта проверка скучна, но эффективна. Когда дифф маленький, прослеживаемый и легко откатываемый, ревью идут быстрее и неожиданные проблемы в продакшне падают.

Часто задаваемые вопросы

How can I tell if weak module boundaries are the real issue?

Посмотрите на недавний pull request. Если одно небольшое изменение продукта затронуло файлы UI, бизнес-правила, сгенерированные типы и старые скрипты в разных папках, границы модулей слабы.

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

Are wide diffs always a code generation problem?

Нет. Генераторы чаще просто быстрее обнажают проблему, потому что они следуют структуре, которую вы им дали.

Если один шаблон пишет в ui, api, helpers и scripts, генератор будет распространять каждое небольшое изменение по всем этим местам.

Which folders should I fix first?

Начните с расплывчатых папок вроде utils, common, shared и misc. Такие имена приглашают клать туда всё подряд.

Переименуйте их в продуктовые области, например billing, auth или notifications. Чёткие имена облегчают последующую очистку.

Should I move repeated code into shared right away?

Нет. Подождите, пока действительно два модуля не будут нуждаться в одном и том же коде.

Даже тогда переносите как можно меньшую часть. Небольшой форматтер дат может подойти для общего кода, но правила выставления счетов должны оставаться в billing.

Where should generated files live?

Держите сгенерированные файлы рядом с источником, который их создаёт. Поместите их рядом со схемой, шаблоном, конфигурацией или prompt'ом, который управляет генерацией.

Так легче видеть причину и следствие: разработчик меняет один исходный файл и сразу видит связанные выходные файлы рядом.

What should I do with stray scripts?

Переместите каждый скрипт рядом с модулем, который он меняет. Скрипт для починки в billing должен жить в billing, а не в корневой папке scripts.

Затем удалите скрипты, которыми никто не пользуется. Старые cleanup и sync файлы часто продолжают трогать участки репозитория, которые нынешний рабочий процесс уже не использует.

How should I split a module without making it too complex?

Простое разделение обычно работает лучше всего. Держите UI-код в одном месте, продуктовые правила — в другом, фоновые задачи и джобы — в третьем.

Например, модуль billing может иметь ui, domain и jobs. Так текст счета, налоговые правила и воркеры ретраев не смешаются.

Can I fix this without rewriting the whole project?

Да. Возьмите одну шумную фичу и очистите только её путь.

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

What should reviewers check before they merge?

Посмотрите на форму диффа перед тем, как разбирать изменения построчно. Одна фича должна в основном оставаться внутри одного модуля и его тестов.

Задайте простой вопрос про каждый сгенерированный файл: какой источник его создал? Если никто не может быстро ответить, поток генерации слишком свободен.

When does it make sense to bring in a Fractional CTO or advisor?

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

Короткий обзор от опытного CTO может сэкономить время, если команда всё ещё спорит о папках вместо того, чтобы доставлять продукт. Например, Oleg Sotnikov на oleg.is работает со стартапами и малыми компаниями по вопросам границ модулей, рабочих процессов генерации кода и поддержки Fractional CTO.