Некоторое время назад я попал в геймдев, где столкнулся с проектами по 2 млн. строк кода, которые пишут десятки программистов. При таких масштабах кодобазы возникают проблемы неведомого мне ранее характера. Об одной и них я хочу вам сейчас рассказать.
Итак, представьте себе следующую ситуацию. Так уж случилось, что вам надо отрефакторить очень большой кусок кода, целую подсистему. Строк, эдак, на 200К. Причем рефакторинг явно выглядит очень крупным, затрагивающим базовые концепции, по которым построена ваша подсистема. Фактически надо переписать всю архитектуру, сохранив бизнес логику. Такое бывает, если, например, вы сделали один проект и у вас впереди новый, и вы хотите в нём исправить все ошибки прошлого. Допустим, по первым прикидкам, на рефакторинг надо месяца 2, не меньше. В процессе рефакторинга всё должно работать, в том числе нельзя мешать другим программистам добавлять новые фичи и чинить баги в подсистеме. Часто такой рефакторинг бывает насколько сложен, что совершенно невозможно замерджить старый код в новый, а также невозможно выкатить результат по частям. Фактически вам надо заменить двигатель самолёта на лету.
Примеры из практики, как моей, так и моих коллег:
- Переделать всю работу с базой данных с чистого JDBC на Hibernate.
- Переделать архитектуру сервиса с отсылки-приёмки сообщений на удалённый вызов процедур (RPC).
- Полностью переписать подсистему трансляции XML файлов в рантайм объекты.
Что делать? С какой стороны подойти к проблеме? Ниже представлен набор советов и практик, которые нам помогают справиться с этой проблемой. Сначала более общие слова, а потом конкретные методики. В общем-то ничего сверхъествественного, но кому-то может помочь.
Подготовка к рефакторингу
- Разбить рефакторинг на части, если это возможно. Если возможно, то нет проблем. Все остальные советы про то, что надо делать, это не удалось.
- Постарайтесь выбрать период, когда активность добавления новых фичей в подсистему будет минимальна. Например, удобно переделывать бэкенд, в то время как все усилия команды сосредоточены на фронтенде.
- Хорошо ознакомьтесь с кодом, зачем он вообще нужен. Какая архитектура заложена в основании подсистемы, какие стандартные подходы, которые там использовались. Чётко определить для себя, в чём заключается новая концепция, и что конкретно вы хотите поменять. Должна быть измеряемая финальная цель.
- Определить область кода, которую захватит рефакторинг. По возможности изолируйте её в отдельный модуль/в отдельную директорию. Это в будущем будет очень полезно.
- Рефакторинг без тестов делать очень опасно. У вас должны быть тесты. Как без них жить я не знаю.
- Запустите тесты с подсчётом покрытия, это даст много информации для размышления.
- Почините сломанные тесты, которые относятся к искомой подсистеме.
- Анализируя информацию о покрытии методов можно найти и удалить неиспользуемый код. Как ни странно, такого часто бывает до 10-15%. Чем больше кода удастся удалить, тем меньше рефакторить. Профит!
- По покрытию определите, какие части кода не покрыты. Надо дописать недостающие тесты. Если юнит тесты писать долго и утомительно, напишите, хотя бы, высокоуровневые smke тесты.
- Постарайтесь довести покрытие до 80-90% осмысленного кода. Не надо стараться покрыть все. Убьёте много времени с малой пользой. Покройте хотя бы основной путь выполнения.
Рефакторинг
- Оберните вашу подсистему интерфейсом. Переведите весь внешний код на использование этого интерфейса. Это не только форсирует применение хороших практик программирования, но и упростит рефакторинг.
- Убедитесь, что ваши тесты тестируют интерфейс, а не реализацию.
- Сделайте возможность при старте указывать, какую реализацию этого интерфейса использовать. Это возможность нужно поддержать и в тестах и в продакшне.
- Записать ревизию системы контроля версий, на которой началось написание новой имплементации. С этой секунды каждый коммит в вашу старую подсистему – ваш враг.
- Напишите новую реализацию интерфейса вашей подсистемы. Иногда можно начинать с нуля, иногда можно применить излюбленый метод — copy&paste. Писать её надо в отдельном модуле. Периодически гоняйте тесты на новой реализации. Ваша цель — сделать так, чтобы все тесты проходили успешно на новой и старой реализации.
- Не надо ждать, когда вы всё полностью напишите. Заливайте код в репозиторий как можно чаще, оставляя включённой старую реализацию. Если долго держать код в загашнике, можно огрести проблем с тем, что другие люди рефакторят модули, которые вы используете. Простое переименование метода может доставить вам много проблем.
- Не забудьте написать тесты, специфичные для новой имплементации, если такие имеются.
- После того как всё напишите, посмотреть историю SVN в папке со старым кодом, чтобы найти, что там менялось за время вашего рефакторинга. Надо перенести эти изменения в новый код. По идее, если вы что-то забыли перенести, тесты должны это поймать.
- После этого, все тесты должны проходить как со старой, так и с новой подсистемой.
- Сразу после того, как вы убедились в стабильности новой версии вашего модуля, переключайте на него тесты и продакшн. Блокируйте коммиты в старую подсистему. Всеми силами старайтесь минимизировать время существования двух подсистем в параллель.
- Подождите неделю или две, соберите всё багло, зачините его и смело удаляйте старую подсистему.
Дополнительные советы
- Все новые фичи, создаваемые параллельно с рефакторингом, должны покрываться тестами на 100%. Это нужно для того, чтобы при переключении на новую имплементацию, упали тесты и сигнализировали о том, что в новой имплементации не хватает кода из старой.
- Любой фикс бага надо делать по принципу — сначала пишем тест, который будет воспроизводить проблему и падать, потом его чиним. Причины те же самые.
- Если вы используете систему а-ля TeamCity, на время рефакторинг сделайте отдельный билд, где будут гоняться все тесты на новой подсистеме. Автоматический билд делает ваш новый, ещё не используемый, код «официальным». К нему начинают применяться все те же политики и правила, что и ко всему остальному.
- Часто бывает так, что вы не знаете, исправили ли вы всё, что хотели в старом коде на новую архитектуру. Например, вы не знаете, не использует ли ваш код где-то прямое JDBC подключение, вместо Hibernate. Или вдруг где-то проскользнуло сообщение, а не RPC вызов. Для обнаружения таких мест надо придумать способ сделать старую методу неработающей. Т.е. сломать её в тестах. Например, сломать систему доставки сообщений или подсунуть системе неработающий JDBC драйвер. Практика показывает, что таким образом обычно находится как минимум штук 5 забытых и не исправленных мест.
- Поговорите с другими программистами, держите их в курсе вашего прогресса. Если они будут знать, что вам осталась неделя, они иногда могут подвинуть свои задачи до времени выхода новой версии вашей подсистемы. Не надо будет сливать изменения.
Опыт подсказывает, что даже страшные и большие подсистемы можно отрефакторить относительно малой кровью. Главные ваши помощники, это тесты и систематичность.
Автор: Randll