Многие из нас начинали с книг Роберта Мартина и свято верили: чистый код — это святое. Мёртвый код нужно безжалостно удалять, рефакторинг проводить каждый спринт, а технический долг — гасить немедленно. Эта догма впитывается с первых месяцев работы.
Но если вы когда-нибудь работали в проекте, который живёт больше пяти лет, в 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 не зря летает уже полвека. Иногда лучший рефакторинг — это тот, который вы не сделали.
Автор: Niiikkto
