Введение: почему рефакторинг legacy кода на Kotlin — это не просто переписывание
Работа с унаследованным кодом (legacy code) на Kotlin — это не просто механическая замена Java-конструкций на синтаксис языка. Это стратегический процесс, направленный на повышение читаемости, безопасности и производительности кода без изменения его внешнего поведения. В этой статье мы рассмотрим продвинутые техники рефакторинга, которые помогут вам превратить хаотичный "зоопарк" классов в стройную, поддерживаемую систему. Мы не будем касаться базовых вещей вроде var vs val — сосредоточимся на том, что действительно меняет архитектуру.
1. Миграция с Java: замена геттеров, сеттеров и утилитарных классов
Самая распространенная проблема legacy кода — обилие Java-классов с явными геттерами и сеттерами, а также статических утилитарных классов. Kotlin предлагает элегантные решения.
1.1. Property-based доступ вместо Java-бобов
Вместо того чтобы писать user.getName() и user.setName("Alice"), используйте свойства Kotlin. Это не просто синтаксический сахар: вы получаете единый интерфейс доступа, который можно переопределить.
// Java legacy
public class User {
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
// Kotlin refactored
data class User(val name: String) // Или с var, если нужна мутабельность
Если логика геттера/сеттера сложнее (например, валидация), используйте пользовательские аксессоры:
class User {
var name: String = ""
set(value) {
field = value.trim() // Автоматическая обрезка пробелов
}
}
1.2. Extension functions вместо статических утилит
Legacy код часто содержит классы вроде StringUtils или FileUtils. В Kotlin их заменяют extension functions. Это делает код более читаемым и приближает к стилю "метод объекта".
// Java: StringUtils.isNullOrEmpty(str)
// Kotlin: str.isNullOrEmpty() (уже есть в stdlib)
// Пример своей extension
fun String.isValidEmail(): Boolean {
return this.contains("@") && this.contains(".")
}
// Использование
val email = "user@example.com"
if (email.isValidEmail()) { ... }
2. Работа с null safety: замена Optional и проверок на null
Legacy Java-код часто страдает от NullPointerException. Kotlin решает эту проблему на уровне типов, но при рефакторинге нужно аккуратно заменять Optional и цепочки проверок на null.
2.1. Замена Optional на nullable типы
Вместо Optional<String> используйте String?. Это снижает накладные расходы на создание объекта и делает код более лаконичным.
// Java
public Optional<String> findName() { ... }
// Kotlin
fun findName(): String? { ... }
Для цепочек вызовов используйте оператор ?. и элвис-оператор ?::
// Вместо if (user != null && user.address != null) ...
val city = user?.address?.city ?: "Unknown"
2.2. Использование require() и check() для инвариантов
Вместо ручных проверок в начале функций используйте встроенные функции:
fun processUser(user: User?) {
requireNotNull(user) { "User must not be null" }
// или require(user != null) { ... }
// Дальнейшая работа с user (smart cast)
}
3. Рефакторинг условной логики: sealed классы и when
Огромные блоки if-else или switch — признак legacy кода. В Kotlin их можно заменить на sealed class и исчерпывающий when.
3.1. Замена enum с полями на sealed class
Если у вас есть enum, который хранит разные типы данных для каждого значения, лучше использовать sealed class. Это обеспечивает типобезопасность и исключает ветки else.
// Legacy: enum с разными полями
enum class Action { SEND, RECEIVE, DELETE }
// Kotlin refactored
sealed class Action {
data class Send(val message: String) : Action()
data class Receive(val senderId: Long) : Action()
object Delete : Action()
}
fun handleAction(action: Action) = when (action) {
is Action.Send -> println("Sending: ${action.message}")
is Action.Receive -> println("Receiving from: ${action.senderId}")
Action.Delete -> println("Deleting...")
// Компилятор не требует else, так как все ветки покрыты
}
3.2. Pattern matching в when
Используйте when не только с константами, но и с проверками типов и условиями:
fun describe(obj: Any) = when (obj) {
is String -> "String of length ${obj.length}"
is Int -> "Integer"
in 1..10 -> "Small number"
else -> "Unknown"
}
4. Внедрение корутин: замена колбэков и RxJava
Асинхронный код в legacy проектах часто реализован через громоздкие колбэки или RxJava. Корутины Kotlin делают его линейным и простым для отладки.
4.1. Замена callback hell на suspend функции
Вместо вложенных колбэков используйте suspend функции и withContext:
// Legacy callback
fun fetchData(callback: (Result<String>) -> Unit) {
// ...
}
// Kotlin coroutine
suspend fun fetchData(): String = withContext(Dispatchers.IO) {
// выполнение в IO потоке
"data"
}
// Вызов в корутине
lifecycleScope.launch {
val result = fetchData()
updateUI(result)
}
4.2. Замена RxJava цепочек на Flow
Если в проекте используется RxJava, Flow из Kotlin Coroutines станет более легковесной и нативной альтернативой:
// RxJava
Observable.just(1, 2, 3)
.map { it * 2 }
.subscribe { println(it) }
// Kotlin Flow
flowOf(1, 2, 3)
.map { it