Рефакторинг legacy кода на Kotlin: стратегия и примеры
Работа с устаревшим (legacy) кодом — неизбежная часть жизни любого разработчика. Часто такой код написан на Java, но даже проекты на ранних версиях Kotlin могут содержать «запахи» (code smells) и архитектурные ошибки. В этой статье мы разберем пошаговую стратегию рефакторинга legacy кода на Kotlin, приведем конкретные примеры преобразований и расскажем, как избежать типичных ошибок.
1. Подготовка к рефакторингу: безопасность прежде всего
Первый и самый важный этап — обеспечить безопасность изменений. Без хорошего тестового покрытия любой рефакторинг превращается в игру в русскую рулетку.
- Добавьте модульные тесты. Если тестов нет — напишите их для критических участков кода. Используйте фреймворки
JUnit 5иMockKдля Kotlin. - Зафиксируйте поведение. Пишите тесты, которые проверяют текущее поведение системы (characterization tests), даже если оно кажется неправильным. Это позволит не сломать логику при рефакторинге.
- Используйте систему контроля версий. Делайте коммиты после каждого логически завершенного шага, чтобы можно было откатиться.
Пример простого теста для legacy функции на Kotlin:
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class LegacyServiceTest {
@Test
fun `test calculateTotal`() {
val service = LegacyService()
val result = service.calculateTotal(listOf(10.0, 20.0, 30.0))
assertEquals(60.0, result)
}
}
2. Постепенная миграция с Java на Kotlin
Если ваш legacy код написан на Java, не пытайтесь переписать всё сразу. Используйте автоматические конвертеры (например, встроенный в IntelliJ IDEA: Code → Convert Java File to Kotlin File), а затем вручную улучшайте результат.
Основные моменты, на которые стоит обратить внимание после конвертации:
- Null safety. Замените
@Nullableи@NonNullна операторы?и!!(последний — только если уверены в отсутствии null). - Data классы. Вместо Java-классов с
equals()иhashCode()используйтеdata class. - Функции-расширения. Выносите утилитарные статические методы в extension functions.
Пример преобразования Java-класса в Kotlin:
// Java
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
}
// Kotlin (после рефакторинга)
data class User(val name: String, val age: Int)
3. Улучшение архитектуры: избавляемся от «спагетти» кода
Legacy код часто страдает от нарушения принципов SOLID. Вот три конкретных приема, которые помогут привести код в порядок:
3.1. Разделение ответственности (Single Responsibility)
Найдите классы, которые делают слишком много. Например, «Божественный объект» (God Object), который и данные хранит, и валидирует, и отправляет запросы. Разбейте его на несколько классов с четкой зоной ответственности.
3.2. Замена длинных цепочек if-else на when
Kotlin предоставляет мощный оператор when, который делает код читаемее:
// До рефакторинга
fun getStatusText(status: Int): String {
if (status == 0) return "Pending"
else if (status == 1) return "Active"
else if (status == 2) return "Blocked"
else return "Unknown"
}
// После рефакторинга
fun getStatusText(status: Int): String = when (status) {
0 -> "Pending"
1 -> "Active"
2 -> "Blocked"
else -> "Unknown"
}
3.3. Использование sealed классов для ограниченных иерархий
Если в коде часто встречаются проверки типа (instanceof) — замените их на sealed class. Это сделает код типобезопасным и избавит от необходимости писать ветку else в when.
sealed class NetworkResult {
data class Success(val data: String) : NetworkResult()
data class Error(val message: String) : NetworkResult()
object Loading : NetworkResult()
}
fun handleResult(result: NetworkResult) = when (result) {
is NetworkResult.Success -> println("Data: ${result.data}")
is NetworkResult.Error -> println("Error: ${result.message}")
NetworkResult.Loading -> println("Loading...")
// Ветка else не нужна — компилятор знает все варианты
}
4. Работа с устаревшими API и библиотеками
Часто legacy код использует устаревшие библиотеки или собственные велосипеды. Замените их на современные аналоги из Kotlin stdlib или проверенных библиотек:
- Вместо
StringUtils.isEmpty()используйте встроенныеisNullOrEmpty()илиisNullOrBlank(). - Вместо циклов for используйте функции высшего порядка:
map,filter,forEach,fold. - Вместо
Optionalиз Java 8 используйтеnullс оператором?.или?:(Elvis).
Пример замены цикла на функциональный стиль:
// До рефакторинга (Java-style)
fun getActiveUserNames(users: List<User>): List<String> {
val result = mutableListOf<String>()
for (user in users) {
if (user.isActive) {
result.add(user.name)
}
}
return result
}
// После рефакторинга (Kotlin-style)
fun getActiveUserNames(users: List<User>): List<String> =
users.filter { it.isActive }.map { it.name }
Заключение
Рефакторинг legacy кода на Kotlin — это не разовая акция,