Рефакторинг легаси: как не сломать то, что работает

В прошлом году мне досталась задача, которую многие разработчики боятся больше, чем писать код с нуля: привести в порядок модуль оплаты, которому было пять лет, без тестов и с единственным человеком в компании, который хоть немного помнил, как он устроен — и этот человек уже уволился. Вот к каким выводам я пришёл по итогам этой работы.
Первое правило: сначала понять, потом менять
Соблазн сразу начать переписывать код, который выглядит плохо, очень велик. Но за уродливым на первый взгляд кодом почти всегда стоит история — часто это накопленные исправления реальных багов и обработка граничных случаев, которые не видны из чтения кода поверхностно. Прежде чем менять что-то, я потратил неделю просто на чтение: git blame по ключевым файлам, старые тикеты, связанные с этим кодом, комментарии в коммитах.
Одна строчка кода, которая выглядела как явная ошибка — округление суммы вниз вместо математического округления — оказалась намеренным решением из-за требований бухгалтерского учёта. Если бы я «исправил» её без понимания контекста, это привело бы к реальному финансовому расхождению.
Второе правило: тесты раньше рефакторинга, не после
Без единого теста трогать код оплаты страшно по делу — любое изменение можно случайно что-то сломать, и узнать об этом только после того, как деньги спишутся неправильно. Прежде чем менять хоть одну строчку, я написал характеризационные тесты (characterization tests) — тесты, которые фиксируют текущее поведение системы как есть, включая её странности, а не то, каким это поведение «должно быть».
Эти тесты не проверяют, что код правильный. Они проверяют, что после рефакторинга код делает то же самое, что и до него. Это принципиально разные вещи, и для легаси без документации второе куда важнее на первом этапе.
Третье правило: маленькие шаги, каждый — рабочая версия
Я разбил рефакторинг на цепочку маленьких изменений, каждое из которых можно было задеплоить отдельно и откатить без последствий, если что-то пойдёт не так. Ни одного «большого взрыва», где переписывается всё и сразу, а результат проверяется только в самом конце. Каждый шаг сопровождался прогоном характеризационных тестов и ручной проверкой на staging-окружении с копией продовых данных.
Что в итоге
Весь рефакторинг занял два с половиной месяца вместо изначально запланированных трёх недель — и это нормально: легаси-код почти всегда содержит больше скрытой сложности, чем кажется на старте. Зато за прошедший год после рефакторинга в модуле оплаты не было ни одного инцидента, связанного с расчётами, — раньше такие случались примерно раз в квартал.
Главный вывод
Рефакторинг легаси — это не про красоту кода ради самой красоты. Это про снижение риска и стоимости следующего изменения. Если после рефакторинга следующую фичу можно добавить за день вместо недели — рефакторинг оправдал себя, даже если сам код по-прежнему не идеален. Идеальный код — не цель; предсказуемый код, которому можно доверять, — цель.