Безопасно удалить копипаст‑код с помощью общих утилит
Удаляйте копипаст-код безопасно: пошаговый план рефактора для дедупликации AI‑сгенерированных функций, сохранения поведения и проверки каждого изменения.

Что со временем ломает «копипаст-код»
Копипаст-код — это когда одна и та же логика появляется в нескольких местах с мелкими правками: переименована переменная, другое значение по умолчанию, чуть другой текст ошибки. Сначала кажется быстрее, чем выносить переиспользуемую функцию. Через месяцы это превращается в ловушку.
В реальных проектах это часто выглядит как один и тот же хелпер для запросов в трёх файлах, пять версий «форматировать дату» или две почти идентичные проверки аутентификации, которые по‑разному определяют, что значит «вошёл в систему». Код работает, пока одну из копий не починят, а другие — нет.
AI‑сгенерированные приложения склонны дублировать логику, потому что модель каждый раз решает задачу, стоящую перед ней. Попросите новую страницу — и вместо повторного использования существующих хелперов она может воссоздать их заново. Инструменты, которые генерируют код всплесками (страница за страницей, фича за фичей), делают повторения ещё более вероятными. В итоге кодовая база полна почти‑клонов, которые выглядят согласованно, но таковыми не являются.
Дублирование увеличивает количество багов и тормозит изменения просто: каждое изменение превращается в охоту по нескольким файлам. Пропустите одну копию — и выпустите несогласованное поведение. Отличия легко пропустить при ревью, потому что они часто маленькие.
Когда говорят «поведение идентично», обычно имеют в виду «пользователи не должны заметить изменений». Это включает не только очевидный вывод, но и детали, о которых забывают:
- Одинаковые входы дают одинаковые выходы, включая краевые случаи.
- Одинаковые ошибки происходят в тех же ситуациях, с теми же сообщениями или кодами.
- Одинаковые побочные эффекты сохраняются (логирование, кэширование, ретраи, записи в БД).
- Чувствительное ко времени поведение остаётся в ожидаемых пределах.
Если вы удаляете копипаст-код безопасно, реальная цель — не «чище код». Это «никаких сюрпризов в проде».
Выберите хороший первый объект для дедупликации
Начните с небольшой области, которую легко наблюдать и сложно неправильно понять. Хорошие ранние цели — паттерны хелперов вроде валидации входа, форматирования дат или денег, повторяющихся проверок auth или общего обёртки для API‑вызовов, используемой на множестве экранов.
Выбирайте то, где код в основном выполняет одну задачу и вы можете описать её в одном предложении. Если нужно три абзаца, чтобы объяснить, что он делает — это не первый кандидат.
Прежде чем что‑то менять, определите границы. Запишите, что входит (входы), что выходит (выходы) и что ещё затрагивается (побочные эффекты: логирование, кэш, чтение env‑переменных или изменение общего объекта). AI‑сгенерированные функции часто скрывают побочные эффекты в небольших местах, вроде «полезных» значений по умолчанию или тихого подавления ошибок.
Солидный кандидат обычно:
- Встречается в 3+ местах с преимущественно одинаковыми строками
- Имеет чёткие входы и выходы (мало глобального чтения)
- Падает видимым образом (ошибка, код статуса, сообщение)
- Не лежит на самом горячем, хрупком пути (платежи, ядро auth, миграции)
- Имеет краевые случаи, которые вы можете перечислить без догадок
Дальше инвентаризируйте дубликаты. Не полагайтесь только на поиск. Откройте каждый файл и подтвердите, что это действительно одна и та же задача, а не просто похожий код.
Наконец, решите, что нельзя менять. Будьте конкретны: точные типы ошибок, точные сообщения (если на них завязаны пользователи или тесты), как обрабатываются пустые значения и любые «странные» краевые поведения. Здесь прячутся сюрпризы: одна копия обрезает пробелы, другая — нет, и вдруг вход в систему ломается у небольшой группы.
Зафиксируйте текущее поведение перед изменениями
Вам нужно доказательство того, что код делает сейчас, включая странности, которые вы хотели бы убрать. Без этой базы «простая чистка» может тихо изменить выводы, тексты ошибок или краевые случаи, на которые полагаются вызовы.
Начните с захвата реальных примеров входов и выходов. Возьмите несколько из логов, тикетов поддержки или своих ручных прогонов. Если логов нет — запустите приложение и запишите несколько реальных запросов или пользовательских сценариев (что вводили, что увидели и что система вернула).
Запишите ожидаемые падения тоже. Многие AI‑сгенерированные хелперы отличаются главным образом тем, как они падают: одна возвращает пустой объект, другая бросает исключение, третья возвращает 200 с текстом ошибки. Эти различия важны, если другие части приложения построены вокруг них.
Создайте небольшой набор «золотых» сценариев, которые сможете прогонять после каждого небольшого изменения. Держите их короткими и реалистичными:
- Обычный ввод, который должен пройти успешно
- Граничный ввод (пустая строка, очень длинная строка, ноль, отсутствующее поле)
- Известно плохой ввод, который должен вызвать конкретную ошибку
- Сценарий, где опционные флаги или заголовки меняют поведение
- Производительный кейс (большая нагрузка, крупный payload или повторный цикл)
Также отметьте скрытые зависимости, которые могут делать поведение непредсказуемым между прогонками: env‑переменные, feature‑флаги, текущее время и часовые пояса, случайные ID, глобальные кэши и сетевые запросы.
Пример: если три дублированных хелпера запросов все добавляют заголовок авторизации, проверьте, отличаются ли они при отсутствии токена (бросают исключение против возвращают null) и откуда читают токен — из глобала, localStorage или env. Это поведение нужно сохранить при дедупликации.
Сопоставьте дубликаты и их мелкие различия
Копипаст‑код редко совпадает идеально. Прежде чем что‑то сливать, составьте чёткую карту того, что общее, а что тихо отличается между версиями.
Положите дублирующиеся функции рядом и сравните их построчно. Не просматривайте галопом. AI‑сгенерированный код часто меняет одну мелочь (значение по умолчанию, имя заголовка, пропущенная проверка на null), которая проявляется только в проде.
Большинство различий попадает в несколько категорий:
- Имена и форма (имена параметров, возвращаемые поля, тексты ошибок)
- Значения по умолчанию (таймауты, ретраи, пустая строка vs null, запасные значения)
- Обработка краевых случаев (отсутствующие поля, 204 ответы, неопределённые env)
- Побочные эффекты (логирование, метрики, кэширование, запись в хранилище)
- Формат входа/выхода (триминг, кодирование, парсинг дат)
Как только перечислите отличия, выберите одну версию как базовую. «Базовая» не значит «лучше написанная». Это означает «на которую больше всего полагается остальная часть приложения», часто та, что используется в большем числе мест или соответствует тому, что пользователи видят сейчас.
Затем решите, какие различия намеренные, а какие случайные. Если различие документировано, покрыто тестом или явно требуется конкретным вызывающим — считайте его намеренным. Если выглядит как случайная вариация (другой таймаут без причины, непоследовательное мэппирование ошибок, чуть другой регистр заголовка), по умолчанию считайте его случайным, пока не доказано обратное.
Конкретный пример: два хелпера запросов оба добавляют Authorization, но только один ещё отправляет куки по умолчанию. Это однострочное отличие может изменить, кто «входит» в систему в проде.
Спроектируйте общую утилиту простой и маленькой
Общая утилита должна казаться почти скучной. Версия 1 не про «лучший дизайн». Это про одинаковое поведение, но в одном месте.
Держите поверхность минимальной
Начните с той самой наименьшей единицы, что повторяется. Если пять функций нормализуют одни и те же входы или собирают одинаковый payload, вытяните лишь эту часть. Оставьте правила валидации, ретраи и особые случаи там, где они есть, пока не докажете, что они действительно общие.
Маленькая утилита проще для ревью, потому что меньше способов случайно поменять поведение. Признаки того, что вы оставили её маленькой:
- Имя описывает одно действие.
- Она принимает явные параметры и возвращает значение.
- Она не знает о маршрутах, экранах или таблицах базы данных.
- Избегает скрытых значений по умолчанию, которые «угадывают», что вы хотели.
Предпочитайте явные параметры вместо глобалей
AI‑сгенерированный код часто лезет в глобалы, env‑переменные или одиночки. Это делает дедупликацию рискованной, потому что каждый вызывающий может полагаться на немного разный скрытый стейт.
Вместо этого передавайте всё, что нужно утилите, через параметры. Например, передавайте baseUrl, headers или timezone. Это может выглядеть повторяющимся сначала, но делает различия видимыми и держит общую логику честной.
Держите побочные эффекты в вызывающем коде
Если дублирующийся код логирует, пишет в БД или триггерит аналитику, по возможности оставьте эти побочные эффекты в вызывающем месте. Стремитесь к «дан набор входов — верни выход». Вызывающий решает, логировать, хранить или подавлять ошибку.
Пример: если три эндпойнта собирают сообщение об ошибке и одновременно логируют его, извлеките только buildErrorMessage(details). Каждый эндпойнт сохранит своё логирование, чтобы вы случайно не поменяли объём или момент логов.
Пошагово: небольшие, проверяемые ходы рефактора
Относитесь к этому как к серии небольших замен, а не к одному большому переписыванию. Вы хотите текущее поведение, просто перемещенное в одно место.
-
Выберите одну «источниковую» дублирующуюся реализацию.
-
Скопируйте её точную реализацию в новый общий файл. Ещё не чистите: никаких переименований, форматирования или «пока здесь» правок.
-
Мигрируйте маленькими шагами:
- Создайте общую утилиту, скопировав одну существующую функцию как есть, оставив старые дубликаты на месте.
- Обновите один вызов, чтобы использовать новую утилиту.
- Прогоните золотые сценарии и сравните выводы, логи и побочные эффекты.
- Повторяйте: переносите ещё один вызов, снова прогоняйте проверки.
- После того как все вызовы используют утилиту, удалите старые копии и неиспользуемые импорты.
Между каждым шагом держите чистый, удобный для ревью коммит. Если что‑то сломалось, вы откатываете один небольшой шаг, а не ищете баг в гигантском диффе.
Если у вызовов слегка разные формы аргументов, используйте временную обёртку‑совместимость. Держите грязную конвертацию у края (рядом с вызывающим), а не внутри общей утилиты.
Как проверить, что поведение осталось одинаковым
Самый безопасный способ доказать, что вы не изменили поведение — ненадолго держать оба пути и сравнивать их на одних и тех же входах.
Сохраните 10–20 реальных входов для повторного прогона (фикстуры из логов — идеал). Прогоните их через старую функцию и новую утилиту в одном и том же порядке и сравните результаты, включая форму и типы, а не только значения.
Не останавливайтесь на счастливом пути. Многие поломки проявляются только при ошибках. Сравните:
- Тексты ошибок и типы ошибок (включая формулировку, если UI её показывает)
- Коды статусов для API‑ответов (200 vs 204 vs 404 имеют значение)
- Обработку пустых значений, null и отсутствия полей ("" vs null vs undefined)
- Порядок элементов (особенно если вы сортируете или мапите ключи)
- Побочные эффекты: логирование, ретраи, кэширование
Если различия сложно заметить, добавьте временный debug‑переключатель, который запускает оба пути и печатает компактный diff при несовпадении. После миграции переключатель удалите.
Наконец, следите за производительностью в обычных путях. Засеките несколько типичных вызовов до и после и убедитесь, что вы не добавили лишних запросов к БД, повторного парсинга JSON или ненужных сетевых вызовов.
Частые ловушки, которые меняют поведение без заметных признаков
Большинство рефакторов терпят неудачу по скучной причине: вы поменяли две вещи одновременно. Держите цель «тот же вывод для того же ввода», а не «красивее код».
Эти ловушки встречаются часто:
- Изменение значений по умолчанию без замечания (
timeout = 0vstimeout = 30, илиundefinedобрабатывается иначе, чемnull) - Потеря неявного преобразования типов (строка "0" стала числом
0, отсутствующее поле стало пустой строкой) - «Приведение в порядок» обработки ошибок, которое скрывает падения (замена броска ошибки на лог‑ворнинг)
- Создание одной общей утилиты, которая делает всё (она растёт флагами, особенными случаями и скрытым состоянием)
- Перенос 9 вызовов и забывчивость о 10‑м (обычно это фоновая задача, админский экран или редко используемый эндпойнт)
Пример: два хелпера запросов оба делают ретраи для 500, но только один ретраит для 429 rate limit. Если вы слили их без сохранения этого исключения, процесс оформления заказа может замедлиться или начать падать при пиках трафика.
Быстрый чек‑лист перед мерджем рефактора
Перед мерджем вы хотите убрать дублирование, не изменив поведение приложения.
Дизайн утилиты — проверки
- Новая хелпер‑функция выполняет одну понятную задачу. Если делает две — разделите, пока ещё маленькая.
- Входы и выходы предсказуемы. Избегайте магических значений по умолчанию, зависящих от глобального состояния.
- Имя функции и параметры совпадают с терминологией, которую уже используют люди в проекте.
- Она не читает env, файлы, контекст запроса или сессию пользователя внутренне. Передавайте значения в параметры.
- Ошибки обрабатываются так же, как раньше (тип, форма сообщения, код статуса при необходимости).
Если не уверены, сравнивайте с тем, что генерировал старый код, а не с тем, чего вы хотите.
Готовность к мерджу — проверки
- Все места вызова мигрированы. Нет полумиграций, где файлы всё ещё используют старую копию.
- Старые копии удалены (или явно помечены для немедленного удаления), чтобы они не могли разойтись.
- Золотые сценарии проходят, включая по крайней мере один «несчастный» путь (плохой ввод, отсутствующее поле, таймаут).
- Логи, метрики и сообщения об ошибках не стали громче или тише так, что это запутает дебаг.
- Быстрый просмотр diff показывает, что секреты или токены случайно не переместились в общую утилиту.
Пример: дедупликация AI‑сгенерированных хелперов запросов
Вы унаследовали приложение, где AI‑инструмент сгенерировал три похожих хелпера: getJson(), postJson() и requestWithRetry(). Все «работают», но каждый эндпойнт вызывает чуть разный, и баги проявляются только в проде.
Выровняв их вы заметили значимые отличия: заголовки (Bearer vs API key, регистр), таймауты (5 seconds vs 5000 ms), правила ретраев (только по сети vs также 503), и маппинг ошибок ({ ok: false } vs throw vs обёртка в message).
Вместо того чтобы навязывать «лучшую» версию, создайте строитель запросов с явными входами, например makeRequest({ method, url, headers, timeoutMs, retryPolicy, mapError }). Главное — значения по умолчанию должны соответствовать поведению первой мигрируемой точки, а не тому, как вы бы хотели.
Мигрируйте первый эндпойнт — предпочтительно простой, который всё ещё проходит через auth и обработку ошибок. Ваши золотые сценарии поймают тонкие изменения быстро. Пример: эндпойнт, который возвращал 204 No Content, раньше возвращал null, а новая общая утилита пытается вызвать json() и падает. Сценарий сразу упадёт, и вы исправите это, явно обрабатывая 204.
Следующие шаги, если кодовая база слишком спутана
Иногда безопасно дедуплировать нельзя, потому что дубликаты скрывают более глубокие проблемы. Если каждая «похожая» функция имеет разные побочные эффекты, разную обработку ошибок или тихие правки данных, общая утилита может сломать реальные пользовательские потоки.
Пауза помогает, но это не обязательно полный рефактор. Цель — короткая стабилизация, которая снова делает возможными пошаговые рефакторы.
Сигналы, что во время рефактора нужно усилить безопасность
Если вы трогаете общий код, считайте эти проверки обязательными:
- Аутентификация несогласована (некоторые маршруты проверяют, другие забывают)
- Секреты в открытом виде (API‑ключи в коде, логах или клиентских бандлах)
- Пользовательский ввод попадает в SQL или запросы без строгой валидации (риск инъекций)
- «Админская» логика зависит от фронтенд‑флага или слабой проверки ролей
- Сообщения об ошибках сливают внутренности (stack traces, имена таблиц)
Исправление дубликатов без учёта этого может распространить риск в вашу новую утилиту.
Как сохранить темп, не переписывая всё приложение
Намеривайтесь на тонкий стабилизирующий слой: небольшой набор общих хелперов с жёсткими правилами и тестами или снимками вокруг самых загруженных потоков. Затем убирайте дубли по кластерам (все хелперы запросов, потом все проверки auth). Если модуль слишком грязный, изолируйте его за простым интерфейсом и отложите внутреннюю чистку, пока остальная часть приложения не станет стабильной.
Если вы унаследовали сломанный AI‑сгенерированный прототип от инструментов вроде Lovable, Bolt, v0, Cursor или Replit, внешний аудит может сэкономить дни догадок. FixMyMess (fixmymess.ai) начинает с бесплатного аудита кода, чтобы выявить дубликаты, скрытые различия поведения и проблемы безопасности, а затем помогает превратить прототип в готовый к продакшен софт, не меняя то, на что уже полагаются пользователи.
Часто задаваемые вопросы
Что именно считать «копипаст-кодом»?
Копипаст-код — это одинаковая логика, повторяющаяся в нескольких файлах с небольшими, легко пропускаемыми отличиями. Сначала он обычно работает, но со временем исправления попадают в одну копию, а в другие — нет, и поведение начинает расходиться.
Почему в AI-сгенерированных приложениях так много дубликатов?
Потому что модель склонна решать задачу, которая стоит перед ней «прямо сейчас», она часто воссоздаёт хелперы вместо использования существующих. Когда фичи генерируются страница за страницей, получаются почти клоны, которые выглядят одинаково, но по‑разному обрабатывают краевые случаи, значения по умолчанию или ошибки.
С чего лучше всего начать удалять дубликаты?
Начните с чего‑то маленького, видимого и легко описываемого в одну фразу: валидация входа, форматирование, helper для API-запросов или повторяющаяся проверка аутентификации. Избегайте самых хрупких критических путей, чтобы сначала отработать сам паттерн рефактора с низким риском.
Что нужно задокументировать перед изменением дублирующегося кода?
Зафиксируйте входы, выходы и побочные эффекты для каждой копии до любых изменений. Включите детали, которые обычно забывают: точные тексты ошибок, коды статусов, логирование, кэширование и то, как обрабатываются пустые значения.
Что такое «золотые сценарии» и сколько их нужно?
Это короткий набор реальных сценариев, которые вы можете прогнать после каждого небольшого изменения, чтобы доказать, что поведение осталось прежним. Держите их практичными: обычный успешный случай, краевой ввод, известная ошибка с конкретной ответной ошибкой и случай, где поведение меняют флаги/заголовки/окружение.
Как выбрать «базовую» копию для слияния в общую утилиту?
Выберите версию, на которую полагается остальная часть приложения сильнее всего, а не ту, которая кажется аккуратнее. Если один хелпер используется чаще или соответствует тому, что пользователи видят в проде, сохраните его поведение первым.
Как безопасно переносить вызовы на новую утилиту?
Двигайтесь очень маленькими шагами: скопируйте базовую функцию в общий файл без очистки, мигрируйте один вызывающий модуль, прогоните ваши золотые сценарии. Маленькие, откатываемые коммиты — главный щит против незаметных изменений поведения.
Почему рефакторы часто ломают обработку ошибок, даже если вывод «выглядит тем же»?
Потому что рефакторы часто меняют поведение при ошибках без видимого изменения вывода, особенно при различии между выбрасыванием ошибок и возвращением объектов, а также при обработке 204/пустых ответов. Новая общая утилита должна сохранять точные типы ошибок, сообщения и поведение для пустых значений, от которых зависят вызывающие.
Где должны жить логирование, ретраи и другие побочные эффекты после дедупликации?
Сохраняйте побочные эффекты (логирование, метрики, записи в хранилище, аналитика) в вызывающем коде, когда это возможно. Так общая утилита остаётся предсказуемой и не меняет объём логов или порядок ретраев неожиданно.
Когда следует прекратить рефакторинг и привлекать внешнюю проверку или помощь?
Если дубликаты скрывают проблемы безопасности — несогласованные проверки auth, открытые секреты или небезопасную обработку входа — дедупликация может распространить проблему в одну общую функцию. В таких случаях лучше привлечь аудит или помощь извне. FixMyMess может выполнить бесплатный аудит кода, найти дубликаты, различия в поведении и риски, а затем быстро починить или перестроить AI-сгенерированный код, обычно за 48–72 часа.