Многие из нас начинали с книг Роберта Мартина и свято верили: чистый код — это святое. Мёртвый код нужно безжалостно удалять, рефакторинг проводить каждый спринт, а технический долг — гасить немедленно. Эта догма впитывается с первых месяцев работы.
Но если вы когда-нибудь работали в проекте, который живёт больше пяти лет, в high‑load системе или enterprise‑среде, вы наверняка сталкивались с парадоксом: самые стабильные части системы — это те, к которым никто не прикасается годами.
В этой статье я хочу поговорить о крамольной для многих идее: удаление кода — это не всегда благо, а рефакторинг в долгоживущих системах часто становится роскошью, которую бизнес не может себе позволить. Мы разберём, почему принцип «не трогай работающее» — это не лень, а зрелая инженерная стратегия, и как архитектурно подойти к сосуществованию старого и нового кода без тотальных переписываний.
1. История, которая всё меняет
В конце 1990‑х инженеры NASA работали над зондом Deep Space 1. На его борту летел код, написанный в 1970‑х для предыдущих миссий. Молодые инженеры предлагали переписать устаревшие модули на более современные языки, провести рефакторинг, улучшить архитектуру.
Руководитель программы отказал. Его аргумент звучал примерно так:
«Этот код работает 20 лет. Мы знаем каждое его поведение в космосе. Если мы его перепишем, мы не узнаем, что сломается, пока не станет слишком поздно. Оставьте его в покое».
Этот случай — классический пример принципа «Let well alone». В инженерной практике он означает: если компонент стабилен, решает свою задачу и не мешает развитию системы, его не трогают, даже если он выглядит «некрасиво» с точки зрения современных стандартов.
NASA в итоге не переписывало код, а оборачивало его в новые адаптеры и интерфейсы, добавляя функциональность снаружи, не затрагивая проверенное ядро.
2. Проблема: догма «удали мёртвый код»
Почему же мы так стремимся удалять и переписывать? Всё начинается с благих намерений:
Упрощение поддержки — меньше кода, меньше проблем.
Улучшение читаемости — избавление от «запахов».
Снижение технического долга — идейный долг нужно возвращать.
Но в реальных проектах, особенно с высокой нагрузкой и многолетней историей, удаление кода несёт риски, которые часто перевешивают преимущества.
2.1 Риск регрессов
Удаление даже, казалось бы, «мёртвого» кода может привести к падению в самых неожиданных местах. Почему?
Скрытые зависимости. Код может вызываться через reflection, динамическую загрузку классов, по имени в конфигах, через RPC.
Тестовое покрытие. В старых системах тесты часто покрывают только happy path. Удаление части кода может нарушить бизнес-логику, которая вообще не покрыта автотестами.
Особенности данных. В продакшене могут существовать данные, которые активируют ветки кода, не используемые в тестовых сценариях годами.
2.2 Потеря институциональной памяти
Когда уходит команда, которая писала модуль, а через год новый разработчик находит «странный» код и удаляет его, потому что «он нигде не вызывается» — это классическая история катастрофы. Оказывается, код обрабатывал миграцию данных, которая запускается раз в полгода, или содержал логику, критичную для compliance.
2.3 Смена вендоров и поставщиков
В enterprise-среде часто используются решения от вендоров, которые поставляют кастомизированные модули. Удаление «лишнего» кода может привести к тому, что вендор откажется поддерживать систему, так как «изменена базовая функциональность».
3. Архитектурный подход: Strangler Fig наоборот
В классической литературе рекомендуют паттерн Strangler Fig (фиговое дерево-удушитель) : постепенно заменять старые компоненты новыми, пока старое приложение не умрёт.
Но я предлагаю посмотреть на этот паттерн с другой стороны: мы не убиваем старое, мы строим новое рядом и переключаем трафик, оставляя старый код в живых как страховку.
3.1 Две стратегии сосуществования
Стратегия |
Что делаем |
Когда применять |
|---|---|---|
Удаление |
Убираем старый код, переписываем на новом стеке |
Когда функциональность простая, есть 100% покрытие тестами, нет внешних зависимостей |
Сосуществование |
Оставляем старый код, новый код работает параллельно, трафик переключается постепенно |
Когда функциональность критическая, тестов мало, риски высоки |
Сосуществование — это не трусость. Это управление рисками.
3.2 Feature Toggles как инструмент безопасности
Один из самых мощных инструментов для безопасного сосуществования кода — feature toggles (флаги функций) .
Вместо того чтобы удалять старый код, мы:
Пишем новую реализацию.
Оборачиваем вызов в проверку флага.
Включаем флаг для небольшого процента пользователей.
Если всё хорошо — увеличиваем процент.
Если что-то пошло не так — мгновенно откатываем флаг.
При этом старый код физически остаётся в репозитории и в билде ещё долгое время, иногда годами.
4. Пример кода: как оставить старый код без вреда для проекта
Покажу на примере Spring Boot (Java), но принцип применим к любому фреймворку.
4.1 Оборачиваем легаси в интерфейс
Допустим, у нас есть старый сервис расчёта скидок, написанный 5 лет назад. Мы не уверены, что новая реализация покроет все edge cases.
// Старый легаси-сервис (не трогаем!) @Service public class LegacyDiscountService { public double calculateDiscount(Order order) { // Сложная логика, написанная 5 лет назад // Никто не хочет в неё лезть return order.getTotal() * 0.1; } } // Новый сервис @Service public class NewDiscountService { public double calculateDiscount(Order order) { // Современная логика return applyComplexRules(order); } }
4.2 Фасад с флагом
Создаём фасад, который решает, какой сервис вызвать.
@Component public class DiscountServiceFacade { private final LegacyDiscountService legacyService; private final NewDiscountService newService; @Value("${feature.discount.new.enabled:false}") private boolean useNewService; public DiscountServiceFacade(LegacyDiscountService legacyService, NewDiscountService newService) { this.legacyService = legacyService; this.newService = newService; } public double calculateDiscount(Order order) { if (useNewService) { return newService.calculateDiscount(order); } return legacyService.calculateDiscount(order); } }
4.3 Изоляция через профили Spring
Для более сложных случаев можно использовать профили и условные бины.
@Configuration public class DiscountConfiguration { @Bean @ConditionalOnProperty(name = "feature.discount.new", havingValue = "false", matchIfMissing = true) public DiscountService legacyDiscountService() { return new LegacyDiscountService(); } @Bean @ConditionalOnProperty(name = "feature.discount.new", havingValue = "true") public DiscountService newDiscountService() { return new NewDiscountService(); } }
4.4 Тесты: игнорируем старый код
Чтобы тесты не падали из-за легаси, мы можем исключать его из покрытия или мокать.
@Test @DisabledIf("!${feature.discount.new.enabled}") void testNewDiscountLogic() { // Тестируем только новую логику, когда флаг включён }
5. Схема архитектуры
Ниже представлена схема постепенного переключения трафика с монолита на микросервис с сохранением старого кода в качестве fallback.

Пояснение к схеме:
API Gateway принимает запросы и на основе feature flags решает, куда направить трафик.
Монолит продолжает существовать и обрабатывает часть запросов. Код монолита не удаляется.
Микросервис постепенно начинает обрабатывать увеличивающийся процент трафика.
Feature Toggle Service позволяет в реальном времени менять процент трафика и мгновенно откатываться.
Старый код живёт в репозитории и в продакшене 2–3 года, пока новая система не наберёт достаточную статистику надёжности.
6. Когда всё-таки нужно удалять?
Я не призываю никогда не удалять код. Есть ситуации, когда удаление необходимо:
Комплаенс и безопасность. Если в старой версии есть уязвимости (например, использование устаревших библиотек с известными CVE).
Законодательные требования. GDPR, хранение персональных данных — иногда старые модули нарушают новые законы.
Смена технологического стека. Когда старая технология перестаёт поддерживаться (например, устаревшая версия Java без обновлений безопасности).
Высокая стоимость поддержки. Если старый модуль требует уникальных знаний, а специалистов на рынке больше нет.
Но даже в этих случаях удаление должно происходить постепенно, с сохранением возможности отката, а не за одну ночь.
7. Выводы
Зрелость инженера измеряется не количеством удалённых строк кода, а количеством часов uptime после деплоя.
В больших, долгоживущих системах принцип «Let well alone» — не проявление лени или консерватизма. Это стратегический подход к управлению рисками:
Стабильность важнее красоты. Система, которая работает 5 лет без падений, ценнее системы, которая переписана на современном стеке, но падает раз в месяц.
Удаление кода — это операция с высоким риском. Перед удалением нужно быть уверенным на 100%, что код действительно мёртвый, а это требует времени и инструментов (анализ покрытия, мониторинг вызовов в продакшене).
Сосуществование старого и нового — это норма. Feature toggles, паттерн Strangler Fig, адаптеры — это инструменты, позволяющие развивать систему без остановки.
Технический долг не всегда нужно гасить. Иногда дешевле оставить долг, если его обслуживание дешевле, чем риски рефакторинга.
NASA не зря летает уже полвека. Иногда лучший рефакторинг — это тот, который вы не сделали.
Комментарии (10)

swame
28.03.2026 12:50В диаграмме вы забыли указать, в какой момент надо уволиться, и оставить вот это все в поддержку новым бедолагам.

Dhwtj
28.03.2026 12:50Если что-то пошло не так — мгновенно откатываем флаг
Через пару релизов откатиться не сможет: зависимости сломаны. Потому, у нас есть правило: фичафлаги удаляем через 2 релиза. И вообще, это плохо показало себя в реальности
Версионирование явных контрактов / апи лучше, живёт дольше. Но не вечно. Если хочется археологии - ищи в гит
Технический долг не всегда нужно гасить. Иногда дешевле оставить долг, если его обслуживание дешевле, чем риски рефакторинга
Легаси не создаёт явный контракт с понятными намерениями. Даже если не менять старый код, а пытаться прибить новый код сбоку, то всё может сломаться. Но да, нужен баланс, конечно.
Легаси на критическом пути, новый код зависит от него - рефактори или оберни в явный контракт
Чтобы зафиксировать контракт легаси, нужно его понять. Чтобы понять - фактически нужно провести тот же анализ, что и для рефакторинга. А гарантировать полноту невозможно, потому что часть поведения — случайная, а потребители уже от неё зависят (закон Хайрама). Реалистичный компромисс - characterization tests (как у Физерса в "Working Effectively with Legacy Code"): не пытаешься понять намерения, а фиксируешь фактическое поведение как есть. Не идеально, но хотя бы ловит регрессии при изменениях рядом

Zoolander
28.03.2026 12:50Это здравый подход и выглядит так что за ним будущее.
Человеку тяжело искать и читать все версии фич, но последние две недели, когда я готовил новый дизайн под фича-флагом и попутно вносил новые правки, у нейронки Codex 5.4 ни разу не возникло проблем с правкой в двух версиях. Человек бы заплакал, сошел с ума, застрелился бы цианидом и потом уволился, но нейронка отпахала как боженька, даже без особых напоминаний
-- Поправила баг и в старой версии компонента, потому что он независим от дизайна.
Рефакторинг становится дешёвым, поддержка нескольких ветвей кода - не проблема, человеческий фактор чистоты кода уходит на второй план.

farafonoff
28.03.2026 12:50Или любит такое, делает фича флаги на ровном месте, раздувает код. Вот только в какой то момент у ии заканчивается контекст и он теряется в собственном коде

9lLLLepuLLa
28.03.2026 12:50Кажется «чистый код» это про то, как не доводить систему до такого состояния, когда вы вынуждены поддерживать несколько систем внутри одной и высчитывать что дороже, стоимость поддержки или рефакторинга. А не про то, о чем вы написали в статье, когда уже поздно пить Борджоми.

Tzimie
28.03.2026 12:50По поводу примера со скидкой. У нас делался не только feature toggele, но и вызов обоих веток для небольшого количества клиентов и сравнение, и логирование всех расхождений

Anarchist
28.03.2026 12:50И вас нисколько не напрягает то, что в системе есть черные ящики, и ни у кого нет знаний, как они работают? Какие edge cases покрывают? И, главное, какие не покрывают? Они так же могут упасть на чём-то неожиданном, но вот убытки могут быть на порядки выше, если никто не имеет опыта работы с ними.

Bobathegreat
28.03.2026 12:50Knight capital потерял 5 миллиардов за 30 минут, потому что старый код никто не удалил 10 лет.

xSVPx
28.03.2026 12:50Могли бы удалить и потерять 50 миллиардов за 10 минут.
Ретроспективно все дартаньяны. На практике неизвестно что хуже "работает не трожь" или "давайте отрефакторим всё". Точнее известно...
apevzner
Я видел исходный код драйвера WiFi от одного крупного производителя чипов, Название не скажу, с меня взали слово не разглашать.
Там было Фсё. С большой буквы “Ф”. Своя реализация точки доступа. Написанная независимо от hostapd. Своя реализация клиента, не основанная на wpa_supplicant. Ну и разумеется, аппаратные драйвера под несколько платформ.
Я насчитал 6 реализаций одного и того же. Совпадающих почти строка в строку, с мелкими отличиями. Всё это было в одном проекте, разделённое условной компиляцией. Даже по файлам не было разделено.
Возможно, там было больше копий, но мне попались на глаза только 6.
Понять, какой экземпляр используется в какой конфигурации, было невозможно. Даже собрать это чудо было подвигом.
У компании был принцип: при появлении новой конфигурации используется та же кодовая база, но сборки под уже выпущенные конфигурации трогать нельзя. Поэтому всё просто дублировалось, и изменения шли в новую копию. Очень похожие, но немного различающиеся копии постепенно накапливались в единой кодовой базе.
По-моему, этот код был образцовым воплощением принципов, изложенных в данной статье.
Это был код, про который говорят, что он вызывает рак глаз. Когда я взялся за этот проект, мне казалось, что получить NNNN баксов за исправление небольшой ошибки - удачная сделка. Потом пожалел 10 раз, но отказаться уже было нельзя, потому, что это - вопрос чести. Ошибку, кстати, поправил - вслепую, без возможности проверить, потому, что железку соответствующую мне не дали. Но больше - ни за что, никогда.