Рефакторинг legacy кода на Kotlin: стратегия и примеры

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Рефакторинг 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 — это не разовая акция,

Блоги

Рекомендаций по книгам