Содержание
Пятница, шесть вечера. В модуле коммерческого предложения нужно поменять скидку одному клиенту. Файл на восемьсот строк. Рядом с формулой цены там проверка прав, запрос в базу, письмо менеджеру и сборка ответа для браузера. Вы правите три строки. Проверки на счастливом пути зелёные. В понедельник бухгалтерия видит чужую скидку на другом договоре.
Кухню, в которой уже стоит ужин, не сносят в субботу. Переставляют один шкаф, подписывают полки и оставляют плиту работать. С кодом та же дисциплина. Рефакторинг двигает внутреннее устройство и не меняет то, что видит пользователь. Декомпозиция решает, какая полка за что отвечает. Переписывание с нуля выселяет дом в гостиницу на год, и почти всегда выясняется, что в старой кухне жили правила, которых нет ни в одном документе.
Дальше о том, как отличить эти три работы, где проводить границу и как пройти путь на одном перегруженном модуле, не останавливая систему. Именованные шаги Фаулера разобраны в выжимке «Рефакторинга». Здесь решения: когда хватает функции, когда нужен модуль, когда оправдан отдельный сервис, и где агент помогает, а где подменяет смысл.

Ключевые выводы
Рефакторинг, декомпозиция и переписывание — три разных решения. Первое сохраняет наблюдаемое поведение и меняет форму. Второе проводит границы ответственности. Третье меняет модель или технологию и обязано доказать, что старые правила не потерялись.
Длинный файл сам по себе ничего не доказывает. Плохо, когда одно изменение бизнес-правила задевает права, хранение, письма и формат ответа. Хорошо, когда редкий сценарий лежит длинным, но локальным куском, который трогают раз в год.
Сначала фиксируют фактическое поведение, потом двигают стены. Если требования потеряны, характеризующие тесты записывают то, что система делает сейчас, включая исторические странности. Дефект и «так исторически сложилось» разделяют явно, до переноса кода.
Границу ставят вокруг направления изменений, а не вокруг технических папок. Модуль силён, когда его обязанности тесно связаны между собой, и слаб, когда каждый сосед лезет внутрь. Физическое разнесение по сервисам имеет смысл только после такой границы.
Маленький проверяемый шаг держит систему живой. Один логический приём, прогон проверок, отдельный коммит на структуру и отдельный на поведение. Агент может выполнить механику. Эквивалентность поведения и смысл правила остаются на инженере.
Три работы, которые путают в одном запросе

В запросе на слияние часто лежит сразу всё: переименовали поля, «заодно» поменяли порядок скидок и вынесли половину файла в новый каталог. Ревьюер не видит, где форма, а где деньги клиента. Кухня в том же состоянии, если в одну субботу переставили шкаф, переписали рецепт борща и заказали новую плиту. Гости не поймут, от чего испортился ужин.
Рефакторинг сохраняет то, что снаружи
Рефакторинг меняет внутреннее устройство при том же наблюдаемом поведении. Пользовательский сценарий, бизнес-результат, ответы на тех же входных данных остаются прежними. Это уже не «причёсывание»: косметическая правка имён без границы и без проверки легко маскирует сдвиг формулы.
Практический признак простой. После структурного коммита набор характеризующих проверок даёт те же результаты, что до него. Если результат изменился, вы уже в другой работе, даже если сообщение коммита говорит о порядке в файле.
Каталог приёмов нужен как общий язык команды. «Извлечь функцию», «ввести объект параметров», «спрятать изменяемую переменную». Это имена движений. Вкусовщиной они становятся только без проверки. Подробный разбор двух шляп, запахов и шагов живёт в выжимке книги Фаулера. Здесь важно следствие: в один момент времени вы либо двигаете стены, либо меняете рецепт.
Декомпозиция называет, кто за что отвечает
Декомпозиция отвечает на вопрос, кто за что отвечает. Вы выделяете самостоятельные обязанности, называете границу и решаете, что снаружи видно, а что спрятано. Физический разрез на файлы, пакеты или сервисы может совпасть с этой границей. Может и не совпасть: два класса в одном файле с узким вызовом между ними уже декомпозиция, если соседи не лезут в чужие поля.
Путаница начинается, когда «разнести по папкам» считают архитектурой. Папки повторяют технические слои: контроллеры, службы, хранилища. Правило скидки при этом размазано по трём папкам и меняется в трёх местах. Граница прошла по типу файла, а не по причине изменения.
Когда структуры уже мало
Рефакторинга перестаёт хватать в четырёх случаях.
Модель предметной области врёт. Скидка, лимит и налог сидят тремя несвязанными числами, и ни одно имя в коде не совпадает с тем, как это называет коммерция.
Архитектура не соответствует текущей нагрузке. Модуль, который считал одно предложение в минуту, теперь считает пакеты для витрины, и синхронное письмо внутри расчёта тормозит кассу.
Технология упёрлась в потолок, который нельзя убрать перестановкой функций. Здесь уже разговор о замене компонента, а не о красоте метода.
И четвёртое: правила так переплетены со случайными деталями хранения, что локальный приём только переносит клубок. Тогда оправдано постепенное перепроектирование одного контура. Снос всей системы оправдан редко и только для куска с понятной границей и планом сверки. Как это выглядит на масштабе корпоративной системы, разобрано в плейбуке постепенного замещения.

| Вопрос | Рефакторинг | Декомпозиция | Переписывание компонента |
|---|---|---|---|
| Что меняется | Форма кода | Границы ответственности | Модель или технология |
| Поведение | То же | То же, пока не решите иначе | Новое, его надо сверить со старым |
| Признак готовности | Те же результаты проверок | Сосед вызывает узкий контракт | Расхождения классифицированы до переключения |
| Типичный провал | Формула уехала в том же коммите | Папки вместо границ | Год параллельной жизни двух правд |
Длинный файл ещё не диагноз

Команда приносит отчёт статического анализа: метод на четыреста строк, цикломатическая сложность за порогом, дубли в двух службах. Отчёт полезен как карта. Диагнозом он становится только вместе с вопросом, что больно менять на этой неделе.
Симптомы, которые чувствует тот, кто правит
Большая функция, которая и проверяет права, и считает, и пишет в базу, и шлёт письмо. Дубли одного бизнес-правила: потолок скидки живёт в расчёте предложения и ещё раз в ночном отчёте, и они уже разъехались на процент. Цепочка условий, куда страшно добавить ветку, потому что соседние ветки держатся на порядке проверок. Цикл зависимостей: модуль цен импортирует счета, счета импортируют цены. Одна функция продукта заставляет открыть двенадцать файлов. Маленькая правка даёт побочный эффект в месте, которое вы не трогали.
Любой из этих признаков по отдельности ещё терпим. Вместе они означают, что стоимость следующего изменения непредсказуема. Это и есть налог, в который превращается отложенная структура. Цена такого сюрприза в проде разобрана в экономике отказа.
Запахи, которым стоит дать имя
Имена нужны, чтобы в ревью говорить о структуре, а не о вкусе.
Божественный модуль. На один тип свалено всё: транспорт, правила, хранение, письма. Любая задача проходит через него, и он знает слишком много о соседях.
Правка дробью. Одно бизнес-изменение требует синхронных правок в разных концах системы. У Фаулера приём называется Shotgun Surgery. По-русски достаточно фразы: правило не лежит в одном месте.
Зависть к чужим данным. Логика модуля А постоянно читает внутренние поля модуля Б и почти не пользуется своими. Значит, поведение хочет жить рядом с данными Б. Приём называется Feature Envy.
Одержимость примитивами. Договор, деньги и период представлены россыпью строк и чисел без собственного типа. Проверки, где идентификатор, а где сумма, расползаются по вызывающим. Приём называется Primitive Obsession.
Протекающая абстракция. Снаружи обещали «посчитать предложение», а наружу торчат детали таблицы и коды ошибок драйвера базы. Соседи начинают зависеть от того, что должно было остаться внутри.
Связанность здесь простая: модули знают друг о друге больше, чем нужно для вызова. Связность смотрит внутрь модуля. Обязанности либо про одно и то же изменение, либо это случайные соседи по файлу.
Когда компромисс нормален
Длинный файл с одним сценарием миграции, который запускают раз в год, может быть честнее дюжины крошечных типов. Дубль двух похожих формул иногда дешевле общей абстракции: через квартал правила разъедутся, а общий метод обрастёт флагами. Простое условие на три ветки лучше преждевременной иерархии классов. Универсальный каркас «на все будущие каналы продаж» почти всегда кодирует канал, которого ещё нет.
Решение опирается на три вопроса. Как часто это место меняется? Какова цена ошибки? Сколько стоит сопровождение той формы, которую вы хотите ввести сегодня? Редкий код с низкой ценой ошибки оставляют явным и даже дублированным. Частое правило с высокой ценой ошибки заслуживает имени, типа и отдельной проверки.
Сначала запишите, как система ведёт себя сейчас

Перестановка шкафа при включённой плите начинается с инвентаризации. Какие кастрюли реально стоят на огне, а какие числятся в старом списке покупок. В коде то же самое. Пока вы не знаете фактический путь от входа до результата, любая «более чистая» структура рискует выкинуть исключение, на котором держится отчёт.
Пройдите путь данных
Возьмите один живой сценарий, лучше тот, который болит. От точки входа (запрос, сообщение очереди, ночное задание) дойдите до записи, ответа и побочных действий. Отметьте внешние зависимости: база, почта, платёжный шлюз, соседний модуль. Выпишите правила, которых нет в вики: «золотому клиенту скидка только если сумма больше порога и промокод не применён». Нарисуйте, кто кого вызывает. Карта на одну страницу полезнее схемы всей системы, которую никто не обновляет.
По ходу всплывают точки, где модуль знает слишком много. Проверка прав внутри формулы цены. Формат письма, собранный из полей таблицы. Это будущие швы. Их не режут в тот же час, когда нашли. Сначала их называют.
Характеризующие тесты держат старое поведение
Майкл Физерс в книге «Эффективная работа с унаследованным кодом» (Working Effectively with Legacy Code) описал приём для системы, у которой требования потеряны. Характеризующий тест фиксирует то, что код делает сейчас, а не то, что он по вашему мнению должен делать. Вы подаёте вход, записываете выход и закрепляете эту пару. После структурного шага пара обязана совпасть.
Такие проверки пишут на том шве, который реально можно захватить. Если модуль не даёт вызвать расчёт без базы и почты, первый шаг часто не «покрыть всё юнитами», а вытащить чистую формулу так, чтобы её можно было вызвать с числами. Пока формула не отделена, характеризующий тест может быть тонким срезом через точку входа: один заказ, одна скидка, одна ошибка клиента, один пограничный ноль.
Отдельная развилка: это дефект или историческое поведение? Если отчёт за прошлый год сошёлся именно на странном округлении, «починить заодно» сломает сверку. Запишите странность в тест и заведите отдельную задачу на смену поведения. Смена поведения требует другой шляпы и другого коммита.
Метрики, которые отвечают на стоимость изменения
Покрытие тестами показывает, какие ветки вы уже прижали. Оно не показывает, легко ли добавить новое правило. Рядом смотрите на вещи, которые чувствует команда.
Сколько модулей приходится открыть для типичной правки. Насколько глубоко вложены условия в горячей функции. Сколько времени занимает типовое изменение от задачи до проверки. Как часто после маленькой правки приезжает регрессия. Насколько больно повторить проверку руками, если автоматической нет.
Цикломатическое число и длина метода остаются подсказками, не целью. Рефакторинг ради того, чтобы анализатор замолчал, легко плодит функции по три строки без нового шва. Цель другая: следующее изменение того же правила затрагивает меньше мест и проверяется быстрее.
Границу проводят там, где код меняется вместе

Принцип единственной ответственности полезен как вопрос. Квота «один класс, один метод» здесь ни при чём. Спросите, по какой причине этот кусок меняется. Если причины две и они приезжают от разных людей в разные недели, кусок совмещает роли. Если причины всегда приезжают вместе, резать рано.
Для прикладного модуля роли обычно такие. Транспорт принимает запрос и отдаёт ответ. Прикладной сценарий собирает шаги одного случая. Бизнес-правила считают деньги и запреты. Доступ к данным читает и пишет хранилище. Внешние интеграции шлют письма и ходят в чужие службы. Когда одна функция делает три роли из этого списка, она уже просит шов, даже если файл ещё короткий.
Группируйте по направлению изменений
Папки по техническому типу удобны, пока правил мало. Как только скидка, налог и лимит начинают жить своей жизнью, код стоит собирать вокруг этих правил. Внутри такого модуля связность высокая: типы, проверки и формулы меняются одной задачей. Между модулями связанность низкая: сосед передаёт уже посчитанный результат или просит операцию по имени, а не лезет в таблицу.
Практический тест границы. Назовите изменение, которое придёт в следующем квартале. Если для него вы трогаете один модуль и его публичный контракт стабилен, граница на месте. Если для него вы ищете одну и ту же константу в четырёх каталогах, границы нет, сколько бы интерфейсов вы ни завели.
От функции к модулю, без прыжка через ступени

Порядок обычно такой, и перескакивать дорого.
Сначала чистые функции: на входе числа и решения, на выходе деньги и причины отказа, без базы и сети. Затем извлечение методов из длинной процедуры, чтобы имена совпали с правилами. Затем тип предметной области, если несколько функций держат одни и те же инварианты. Затем явный контракт для соседей. Внутренние детали хранения и почты за этот контракт не выглядывают.
Интерфейс заводят, когда есть вторая реализация или жёсткая граница, которую нужно подменить в проверке. Интерфейс на каждый класс без второй реализации и без шва для теста добавляет файлы и не снижает связанность.
Принципы без догмы
SOLID и соседние формулировки полезны как словарь ограничений. Единственная ответственность спрашивает про причину изменения. Открытость к расширению предупреждает, что новая ветка скидки не должна переписывать старые ветки, если правила правда независимы. Инверсия зависимостей напоминает, что расчёт не должен сам открывать соединение с базой: соединение приходит снаружи.
Композиция вместо наследования спасает от иерархий «золотой клиент, золотой клиент с промокодом, золотой клиент с промокодом и ручной скидкой». Набор маленьких правил, применённых по очереди, проще, чем дерево классов на каждую комбинацию.
Паттерны из выжимки банды четырёх подключают, когда в коде уже есть повторяющаяся боль, которую паттерн называет. Замена условий полиморфизмом оправдана, когда ветки стабильно означают разные поведения и плодятся. Три ветки в одной функции полиморфизма не просят.
| Что болит | Ближайший шов | Чего не делать в тот же день |
|---|---|---|
| Функция на десятки строк с одной причиной меняться | Извлечь функции рядом | Новый сервис и общая библиотека |
| Несколько правил меняются вместе и их зовут снаружи | Модуль с узким контрактом | Интерфейс на каждый внутренний тип |
| Две реализации одной операции или нужен шов для теста | Контракт и подмена | Иерархия на все будущие варианты |
| Контракты уже есть, выпуск и отказы общие | Модульный монолит, запрет лишних импортов | Сеть между модулями «на вырост» |
| Разный цикл выпуска, масштаб или команда | Отдельный сервис у этой границы | Сначала разрезать всё приложение |
Шаг, который можно откатить за минуту

Хороший приём умещается в голове и в диффе. Если вы не можете сказать, какое поведение обязано остаться прежним, шаг слишком большой.
Базовые движения
Extract Function вынимает кусок с именем правила. Inline Function возвращает обёртку, которая ничего не прячет и только мешает чтению. Переименование и смена сигнатуры делают намеренье видимым: total становится totalAfterDiscounts. Introduce Parameter Object собирает россыпь аргументов, которые всегда едут вместе, в один тип. Encapsulate Variable прячет изменяемое поле, чтобы соседи перестали писать в него в обход правила. Безымянные пороги (1000, 0.9) получают имена, когда смысл не очевиден из места.
Replace Conditional with Polymorphism оставляют на потом. Сначала условие должно стабилизироваться как набор настоящих вариантов, а не как временный список акций.
Дублирование бизнес-правила и дубль текста
Ищите повтор смысла, а не повтор строк. Две функции считают скидку золотого клиента и уже расходятся на округлении. Это одно правило, которое должно жить в одном месте. Две функции похожи по циклу, но одна считает коммерческое предложение, а другая считает складской резерв. Общий «универсальный калькулятор» с десятком флагов склеит разные смыслы. Через месяц флаг, добавленный для склада, изменит цену в предложении.
Оставляйте независимые реализации, пока не назовёте общее правило одной фразой без «и» и без флага. Если фраза не строится, общего правила нет.
Зависимости сделайте явными
Адаптер прячет чужой протокол за словами вашей области: не «вызвать почтовый SDK», а «сообщить, что предложение готово». Фасад собирает пачку внутренних вызовов в одну операцию для соседа, которому не нужны промежуточные шаги. Глобальное соединение, глобальный пользователь и глобальный почтовик заменяются аргументами. Тогда характеризующий тест подставляет память вместо базы и записывает письма в список, а не на реальный ящик.
Побочные эффекты, которые раньше случались «где-то внутри», становятся строками сценария: проверить права, посчитать, сохранить, уведомить. Порядок виден. Его можно поменять отдельным коммитом поведения, когда бизнес правда этого просит.
Порядок, который не смешивает шляпы
Сначала проверка, которая падает, если поведение уедет. Потом одно структурное движение. Потом прогон. Потом коммит, в сообщении которого нет слов про новую функцию. Поведение, если оно вообще нужно, приезжает следующим коммитом, и дифф там короткий.
Откат такого коммита возвращает предыдущую форму, а не смесь из трёх задач. В ветке это выглядит скучно. Скука здесь и есть способ не останавливать плиту.
Цикл на одну задачу, который можно повесить в описание работы:
- Назовите конкретную боль, а не «привести модуль в порядок».
- Найдите куски, которые меняются вместе.
- Проследите внешние контракты и побочные действия.
- Зафиксируйте текущее поведение тестами на этом шве.
- Выберите минимальную границу, которую готовы проверить сегодня.
- Сделайте одно структурное движение.
- Прогоните тесты и статические проверки.
- Посмотрите, не вылез ли эффект в соседнем сценарии.
- Повторяйте, пока типовая правка не станет локальной.
- Запишите решение: какая граница теперь считается публичной и что не режем в следующий раз.
Снос и новостройка проигрывают скрытым правилам

Переписать модуль с чистого листа хочется, когда файл стыдно открывать. Стыд плохой заказчик. В старом коде сидят исключения, о которых помнит один человек в коммерции: нулевая цена для внутреннего перемещения, скидка, которую запрещено суммировать с промокодом, письмо, которое обязан получить не клиент, а куратор региона. Пока новая версия пишется, старая продолжает обрастать такими исключениями. К моменту переключения сравнивают уже не две реализации одной формулы, а два разных мира.
Вторая цена в том, что команда живёт с двумя версиями сразу. Старую версию нельзя выключить, пока новая не совпадёт на боевых случаях. Команда платит за обе. Расхождения всплывают поздно, если сверку отложили на «после запуска».
Поэтому выигрышный рисунок другой. Берут ограниченный участок с понятным результатом, например только расчёт суммы без писем и без прав. Старую реализацию изолируют за контрактом, чтобы соседи перестали звать её внутренности. Ответственность переносят маленькими кусками. Совместимость со старым ответом держат, пока потребители не перешли. Старый код удаляют, когда на него не осталось вызовов, а не когда «новая архитектура в целом готова».
Приём постепенного замещения, в литературе Strangler Fig, на уровне модуля выглядит так. Новый расчёт стоит рядом. Вызовы идут через развилку: часть запросов считает старый путь, часть — новый, ответы сравнивают. Расхождение пишут в журнал с идентификатором предложения, а не чинят на глаз в пятницу. Когда расхождения понятны и либо признаны дефектом, либо приняты как смена правила, развилку убирают. Где этот рисунок обязателен на уровне контуров ERP и где он не подходит, см. плейбук замещения. На одном модуле он не подходит, если нет способа сравнить результаты и нет пути отката развилки.
Для критичного расчёта полезны три предохранителя. Двойной просчёт без двойной записи: обе реализации считают, в базу пишет ещё старая, разница видна в наблюдении. Теневой прогон: новый путь исполняется на копии входа и не влияет на ответ пользователю. Переключатель включения: новый путь включается на долю трафика и выключается без выкладки, если ошибки поползли. План отката называют до включения, а не во время разбора.
Сервис появляется после границы
Хорошая декомпозиция часто заканчивается модульным монолитом. Приложение одно, процесс один, выкладка одна. Внутри модули с публичными контрактами, запрет на импорт чужих внутренностей и свои таблицы, которые снаружи не читают напрямую. Это и промежуточная остановка по дороге к сервисам, и самостоятельная архитектура, если у команды нет разных циклов выпуска. Почему монолит раздувается, когда границ нет, разобрано в статье про монолиты ERP. Как вообще эволюционируют слои веб-приложения, см. историю архитектуры приложений.
Сервис выделяют, когда совпало несколько условий. Жизненный цикл и масштаб правда другие: расчёт предложений должен переживать пик витрины, а письма могут подождать. Требования к отказам другие: падение почты не должно ронять кассу. Команда выпускает этот контур отдельно и готова держать контракт. Сеть, повторные запросы, таймауты и частичный отказ становятся частью работы, а не сюрпризом после разрезания.
Цена распределённой схемы конкретная. Сеть теряет пакеты и добавляет задержку. Транзакция, которая раньше была одним коммитом базы, разъезжается на два хранилища и вопрос согласованности. Контракт надо версионировать, потому что соседа нельзя обновить в ту же секунду. Наблюдение, журналы и разбор инцидентов стоят дороже, чем стек одного процесса.
Порядок решений поэтому короткий. Сначала логическая граница и контракт внутри одного процесса. Потом замер: больно ли жить с общим выпуском. Только потом сеть. Разрезать по папкам «потому что так делают» значит вынести ту же кашу на два репозитория и добавить таймауты.
Сквозной пример: модуль коммерческого предложения
Условный модуль делает всё сразу. Принимает тело запроса. Проверяет право quote:create. Читает клиента из базы. Считает сумму строк. Применяет скидку золотого уровня и промокод. Не даёт сумме уйти ниже нуля. Пишет предложение. Шлёт письмо. Отдаёт строку с двумя знаками после запятой.
Ниже сжатый исходный вид на TypeScript. Он нарочно плоский: так выглядит шов до работы.
export async function createQuote(req, db, mailer) {
const user = req.user;
if (!user?.permissions.includes('quote:create')) {
throw new Error('forbidden');
}
const customer = await db.query(
'select id, tier, email from customers where id = $1',
[req.body.customerId],
);
if (!customer.rows[0]) throw new Error('customer');
let total = 0;
for (const line of req.body.lines) {
total += line.qty * line.unitPrice;
}
if (customer.rows[0].tier === 'gold' && total > 1000) total *= 0.9;
if (req.body.promo === 'SPRING' && total > 500) total *= 0.95;
if (total < 0) total = 0;
await db.query('insert into quotes(customer_id, total) values ($1, $2)', [
customer.rows[0].id,
total,
]);
await mailer.send(customer.rows[0].email, `Quote ${total}`);
return { status: 200, body: { total: total.toFixed(2) } };
}
Шаг 1. Назвать роли
Здесь пять ролей: транспорт и права, сценарий «создать предложение», правила цены, запись в базу, уведомление. Пока они в одной функции, любое изменение скидки рискует задеть письмо и SQL.
Шаг 2. Зафиксировать поведение
Характеризующие случаи, которые стоит записать до переноса формул: клиент без права; неизвестный клиент; сумма строк без скидки; золотой уровень выше порога; промокод выше своего порога; оба правила сразу; ноль и отрицательная промежуточная сумма, если она вообще возможна на входе. Ожидаемые числа снимают с текущего кода, а не с нового «более правильного» округления.
Шаг 3. Вынуть правила, не меняя формул
Чистая функция не знает про запрос и почту. Пороги и коэффициенты пока те же, что в старом коде. Имена говорят правило.
const GOLD_MIN = 1000;
const GOLD_RATE = 0.9;
const PROMO_MIN = 500;
const PROMO_RATE = 0.95;
export function priceQuote(input: {
linesTotal: number;
tier: string;
promo?: string;
}): number {
let total = input.linesTotal;
if (input.tier === 'gold' && total > GOLD_MIN) total *= GOLD_RATE;
if (input.promo === 'SPRING' && total > PROMO_MIN) total *= PROMO_RATE;
if (total < 0) total = 0;
return total;
}
Формулы не улучшают в этом коммите. Если золотая скидка и промокод не должны перемножаться, это смена поведения и отдельная задача со сверкой отчётов.
Шаг 4. Изолировать внешнее
Хранилище и уведомление становятся контрактами. В проверке их подменяют списками. В приложении за ними старые SQL и почта.
export interface QuoteStore {
findCustomer(id: string): Promise<{ id: string; tier: string; email: string } | null>;
insertQuote(customerId: string, total: number): Promise<void>;
}
export interface QuoteNotifier {
quoteReady(email: string, total: number): Promise<void>;
}
Шаг 5. Оставить снаружи один сценарий
Прикладная функция оркеструет роли. Транспорт только достаёт пользователя и тело, проверяет право и переводит ошибки в коды ответа. Соседи больше не видят порог 1000.
export async function createQuote(
input: { customerId: string; linesTotal: number; promo?: string },
store: QuoteStore,
notifier: QuoteNotifier,
): Promise<number> {
const customer = await store.findCustomer(input.customerId);
if (!customer) throw new Error('customer');
const total = priceQuote({
linesTotal: input.linesTotal,
tier: customer.tier,
promo: input.promo,
});
await store.insertQuote(customer.id, total);
await notifier.quoteReady(customer.email, total);
return total;
}
Шаг 6. Переносить вызовы и сверять
Старый createQuote на время становится тонкой оболочкой над новым сценарием. Ответы на характеризующих случаях сравнивают до удаления оболочки. Если где-то в системе функцию звали ради побочного письма без записи, это всплывёт здесь, а не после выкладки.
Шаг 7. Оценить, стало ли дешевле менять
Смотрите не на число строк. Смотрите, сколько файлов открывает правка порога золотой скидки (ожидание: один модуль правил). Насколько легко проверить правило без базы. Насколько узкий публичный вход: идентификатор, сумма строк, необязательный промокод, наружу число. Сколько времени занимает следующая похожая правка.
Если порог по-прежнему зашит в отчёте ночной сверки, декомпозиция модуля предложения не закончена. Правило ещё живёт второй жизнью. Это следующая задача того же цикла, а не повод завести микросервис.
Где переделка становится дороже кода

Большой кусок без промежуточных проверок. Откатить его уже нельзя: в диффе и переименования, и новая формула.
Абстракция под канал продаж, которого нет в бэклоге этого года. Каркас обгоняет задачу и сам становится кодом, который страшно менять.
Интерфейс на каждый класс. Навигация длиннее, поведение то же, шва для подмены нет.
Общий метод для правил с разным смыслом. Флаги внутри «универсальной» функции возвращают ту сложность, от которой уходили, только в худшем виде: она не видна по имени.
Слишком мелкие функции и файлы. Чтение сценария превращается в поход по двадцати переходам. Шов должен совпадать с правилом, а не с каждой строкой.
Попытка внедрить Clean Architecture или предметно-ориентированное проектирование сразу на всю систему. Слои появляются там, где ещё нет ни одного стабильного правила. Книжные имена не заменяют карту одного сценария.
Структура и поведение в одном коммите. Ревью пропускает сдвиг округления за переносом файла.
Рефакторинг ради цифры анализатора. Порог сложности зелёный, править скидку по-прежнему больно.
Работа без знания вызовов. Удалённый «мёртвый» аргумент оказывается частью внешнего контракта ночного задания.
Агент двигает текст, смысл остаётся на человеке

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

Архитектура здесь не чертёж, который утверждают один раз. Это способность поменять правило скидки в пятницу и не разбудить в понедельник чужой договор. Рефакторинг оставляет плиту работать, пока двигают шкаф. Декомпозиция подписывает полки так, чтобы следующий человек открыл одну дверцу. Маленькие шаги со сверкой дают больше контроля, чем субботник, после которого никто не может объяснить, куда делась старая кастрюля.
На этой неделе выберите одно горячее правило. Запишите его текущий результат. Выньте его в функцию, не меняя чисел. Посмотрите, сколько файлов откроет следующая правка. Если по-прежнему больше одного, граница ещё впереди, и это нормальный следующий шаг, а не повод сносить модуль.



Комментарии