Введение: почему legacy код — это не приговор
Каждый разработчик сталкивался с legacy кодом — той самой «чёрной коробкой», которую боишься трогать, потому что «оно работает, и не надо лезть». Однако, когда кодовая база на Kotlin (или Java, которую постепенно мигрируют) начинает тормозить развитие, накапливать баги и пугать новых членов команды, приходит время рефакторинга. Рефакторинг legacy кода на Kotlin — это не просто переписывание, а системный процесс улучшения внутренней структуры без изменения внешнего поведения.
В этой статье мы разберём полный путь: от анализа текущего состояния до внедрения современных практик Kotlin. Вы узнаете, как безопасно преобразовать устаревший код, используя возможности языка (нулабельность, extension functions, sealed classes) и не сломать продакшен.
1. Диагностика: как оценить масштаб legacy кода
Прежде чем браться за рефакторинг, необходимо понять, с чем вы имеете дело. Legacy код на Kotlin часто приходит из проектов, которые начинали писать на Java, а затем частично мигрировали. Типичные признаки:
- God-объекты — классы, которые делают всё: от парсинга до бизнес-логики.
- Отсутствие тестов — код не покрыт unit-тестами, и любое изменение рискованно.
- Мутабельные данные — обилие
varвместоval, mutable списки, которые передаются между модулями. - Null-кошмар —
!!оператор, разбросанный по всему коду, и бесконечные проверкиif (x != null).
Для диагностики используйте инструменты: Detekt (статический анализ Kotlin), SonarQube (метрики качества) и IntelliJ IDEA Inspections. Создайте чек-лист проблемных мест и приоритизируйте их по риску и частоте использования.
1.1. Составление карты зависимостей
Возьмите лист бумаги или используйте PlantUML для визуализации связей между классами. Если граф зависимостей напоминает паутину — это звоночек. Особое внимание уделите циклическим зависимостям: они делают код хрупким и сложным для тестирования.
2. Стратегия рефакторинга: пошаговый план действий
Рефакторинг без плана — это путь к катастрофе. Используйте подход «Техника малых шагов» (baby steps). Алгоритм действий:
- Задокументируйте поведение — напишите характеристические тесты (characterization tests), которые фиксируют текущее поведение системы.
- Изолируйте изменения — работайте в отдельной ветке, используйте feature toggles для сложных участков.
- Рефакторинг по паттернам — применяйте проверенные техники (Extract Method, Replace Conditional with Polymorphism, Introduce Parameter Object).
- Постепенная миграция — не пытайтесь переписать всё сразу. Используйте
@Deprecatedаннотации для старых методов.
2.1. Пример: замена if-else на when и sealed classes
Типичный legacy код на Kotlin содержит цепочки if-else для обработки состояний:
// Плохо: мутабельный флаг и if-else
var status = "active"
if (status == "active") {
// ...
} else if (status == "inactive") {
// ...
} else {
throw Exception("Unknown status")
}
Рефакторинг с sealed классами:
// Хорошо: типобезопасная модель
sealed class Status {
object Active : Status()
object Inactive : Status()
data class Custom(val reason: String) : Status()
}
fun handleStatus(status: Status) = when (status) {
is Status.Active -> // обработка
is Status.Inactive -> // обработка
is Status.Custom -> // обработка с reason
}
Это устраняет необходимость в else и делает код расширяемым.
3. Безопасная работа с nullable и коллекциями
Legacy код часто полон null проверок и мутабельных коллекций. Kotlin предлагает элегантные решения:
- Замените
!!на?.letили?:— это предотвратит NPE в рантайме. - Используйте
List<T>вместоMutableList<T>— если коллекция не должна изменяться, сделайте её иммутабельной. - Примените scope-функции —
apply,with,runдля группировки операций.
3.1. Пример рефакторинга коллекций
// До: мутабельный список и цикл
val names = mutableListOf<String>()
for (user in users) {
if (user.name != null) {
names.add(user.name)
}
}
// После: функциональный стиль
val names = users
.mapNotNull { it.name }
.toList() // иммутабельный список
Такой код короче, безопаснее и легче читается.
4. Тестирование и интеграция: как не сломать продакшен
Рефакторинг без тестов — это игра в русскую рулетку. Вот обязательные шаги:
- Покрытие тестами критических путей — используйте JUnit 5 и MockK (аналог Mockito для Kotlin).
- Золотые тесты (snapshot testing) — для участков, где сложно написать assert, сохраняйте результат и сравнивайте с эталоном.
- CI/CD пайплайн — добавьте линтер (Detekt) и запуск тестов перед мержем.
4.1. Пример характеристического теста
@Test
fun `legacy method should return correct format`() {
val legacy = LegacyService()
val result = legacy.oldMethod("test")
// Фиксируем текущее поведение
assertEquals("TEST_2023", result)
}
После рефакторинга тест должен проходить, если поведение не изменилось.