Многие из нас начинали с книг Роберта Мартина и свято верили: чистый код — это святое. Мёртвый код нужно безжалостно удалять, рефакторинг проводить каждый спринт, а технический долг — гасить немедленно. Эта догма впитывается с первых месяцев работы.

Но если вы когда-нибудь работали в проекте, который живёт больше пяти лет, в 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 (флаги функций) .

Вместо того чтобы удалять старый код, мы:

  1. Пишем новую реализацию.

  2. Оборачиваем вызов в проверку флага.

  3. Включаем флаг для небольшого процента пользователей.

  4. Если всё хорошо — увеличиваем процент.

  5. Если что-то пошло не так — мгновенно откатываем флаг.

При этом старый код физически остаётся в репозитории и в билде ещё долгое время, иногда годами.

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.

схема постепенного переключения трафика
схема постепенного переключения трафика

Пояснение к схеме:

  1. API Gateway принимает запросы и на основе feature flags решает, куда направить трафик.

  2. Монолит продолжает существовать и обрабатывает часть запросов. Код монолита не удаляется.

  3. Микросервис постепенно начинает обрабатывать увеличивающийся процент трафика.

  4. Feature Toggle Service позволяет в реальном времени менять процент трафика и мгновенно откатываться.

  5. Старый код живёт в репозитории и в продакшене 2–3 года, пока новая система не наберёт достаточную статистику надёжности.

6. Когда всё-таки нужно удалять?

Я не призываю никогда не удалять код. Есть ситуации, когда удаление необходимо:

  • Комплаенс и безопасность. Если в старой версии есть уязвимости (например, использование устаревших библиотек с известными CVE).

  • Законодательные требования. GDPR, хранение персональных данных — иногда старые модули нарушают новые законы.

  • Смена технологического стека. Когда старая технология перестаёт поддерживаться (например, устаревшая версия Java без обновлений безопасности).

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

Но даже в этих случаях удаление должно происходить постепенно, с сохранением возможности отката, а не за одну ночь.

7. Выводы

Зрелость инженера измеряется не количеством удалённых строк кода, а количеством часов uptime после деплоя.

В больших, долгоживущих системах принцип «Let well alone» — не проявление лени или консерватизма. Это стратегический подход к управлению рисками:

  1. Стабильность важнее красоты. Система, которая работает 5 лет без падений, ценнее системы, которая переписана на современном стеке, но падает раз в месяц.

  2. Удаление кода — это операция с высоким риском. Перед удалением нужно быть уверенным на 100%, что код действительно мёртвый, а это требует времени и инструментов (анализ покрытия, мониторинг вызовов в продакшене).

  3. Сосуществование старого и нового — это норма. Feature toggles, паттерн Strangler Fig, адаптеры — это инструменты, позволяющие развивать систему без остановки.

  4. Технический долг не всегда нужно гасить. Иногда дешевле оставить долг, если его обслуживание дешевле, чем риски рефакторинга.

NASA не зря летает уже полвека. Иногда лучший рефакторинг — это тот, который вы не сделали.