Рефакторинг legacy кода на Kotlin: стратегия и практические примеры
Работа с унаследованным (legacy) кодом — неизбежная часть жизни любого разработчика. Часто такой код написан на Java, но мигрирован на Kotlin без глубокого переосмысления, или же представляет собой «спагетти» из утилитарных функций и nullable-полей. Рефакторинг такого кода не только улучшает читаемость, но и повышает производительность и безопасность приложения. В этой статье мы разберем пошаговую стратегию рефакторинга legacy кода на Kotlin с реальными примерами.
1. Подготовка и тестирование: фундамент безопасности
Прежде чем менять код, необходимо обеспечить его корректную работу после изменений. Без тестов рефакторинг превращается в гадание.
- Напишите характеристические тесты (Characterization Tests): Запустите существующий код с разными входными данными и зафиксируйте выходные значения. Эти тесты станут вашей «сеткой безопасности».
- Используйте @Deprecated аннотацию: Если вы не можете удалить старый публичный API сразу, пометьте его как
@Deprecatedс указанием альтернативы. - Работайте маленькими шагами: Никогда не рефакторьте более одного логического блока за раз. Каждый коммит должен проходить все тесты.
Пример характеристического теста для старой функции:
// Legacy код
fun calculateTotal(items: List<Int>?): Int {
if (items == null) return 0
var sum = 0
for (item in items) {
sum += item
}
return sum
}
// Тест
@Test
fun `calculateTotal returns sum or zero`() {
assertEquals(0, calculateTotal(null))
assertEquals(6, calculateTotal(listOf(1, 2, 3)))
assertEquals(0, calculateTotal(emptyList()))
}
2. Избавление от null-безопасности и использование Scope-функций
Legacy код часто страдает от избыточных проверок на null. Kotlin предоставляет элегантные инструменты для работы с nullable-типами.
Что делаем: Заменяем вложенные if (x != null) на let, run или also.
// До рефакторинга
fun processUser(user: User?) {
if (user != null) {
if (user.name != null) {
println("User: ${user.name}")
}
}
}
// После рефакторинга
fun processUser(user: User?) {
user?.let { u ->
u.name?.let { name ->
println("User: $name")
}
}
}
// Или еще короче с применением run
fun processUserShort(user: User?) {
user?.run {
name?.let { println("User: $it") }
}
}
Такой подход делает код более декларативным и снижает вероятность пропустить проверку.
3. Замена утилитарных классов на Extension-функции
В legacy Java-проектах часто встречаются статические утилитарные классы (StringUtils, DateUtils). В Kotlin их лучше заменить на extension-функции.
Почему это важно: Extension-функции делают код более читаемым и позволяют IDE предлагать их как методы самого класса.
// Legacy Java-стиль
object StringUtils {
fun isPalindrome(input: String): Boolean {
val cleaned = input.filter { it.isLetterOrDigit() }.lowercase()
return cleaned == cleaned.reversed()
}
}
// Использование
val result = StringUtils.isPalindrome("A man, a plan, a canal: Panama")
// Рефакторинг на Kotlin
fun String.isPalindrome(): Boolean {
val cleaned = this.filter { it.isLetterOrDigit() }.lowercase()
return cleaned == cleaned.reversed()
}
// Использование
val result = "A man, a plan, a canal: Panama".isPalindrome()
Также стоит обратить внимание на замену java.util.Date на java.time (LocalDate, Instant) — это повысит безопасность работы с временем.
4. Оптимизация коллекций и Stream API
Legacy код часто использует циклы for и if для фильтрации и трансформации данных. Kotlin предлагает богатый набор функций для работы с коллекциями.
Типичные преобразования:
filterвместоifвнутри циклаmapвместо создания нового списка и добавления элементовfirstOrNullвместо поиска с breakgroupByвместо ручной группировки
// Legacy код
fun getActiveUsernames(users: List<User>): List<String> {
val result = mutableListOf<String>()
for (user in users) {
if (user.isActive) {
result.add(user.name)
}
}
return result
}
// После рефакторинга
fun getActiveUsernames(users: List<User>): List<String> {
return users.filter { it.isActive }
.map { it.name }
}
// Или еще короче с использованием ссылок на функции
fun getActiveUsernamesShort(users: List<User>): List<String> =
users.filter(User::isActive).map(User::name)
Обратите внимание: цепочки вызовов filter и map ленивы только для Sequence. Для больших коллекций используйте asSequence() в начале цепочки.
5. Рефакторинг классов данных (Data Classes)
Если в legacy коде есть классы, которые хранят только данные, но не имеют логики — превратите их в data class. Это автоматически даст вам equals(), hashCode(), toString() и copy().
// До
class Person(val name: String, val age: Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Person
return name == other.name && age == other.age
}
override fun hashCode(): Int {
return 31 * name.hashCode() + age
}
}
// После
data class Person(val name: String, val age: Int)
Это не только сокращает код, но и делает его менее подверженным ошибкам при добавлении новых полей.
Заключение
Рефакторинг legacy кода на Kotlin — это не просто пере