Введение: почему рефакторинг legacy кода на Kotlin — это необходимость
Работа с унаследованным (legacy) кодом — неизбежная реальность для большинства разработчиков. Даже на таком современном языке, как Kotlin, проекты со временем обрастают техническим долгом: устаревшие паттерны, отсутствие тестов, смешение стилей Java и Kotlin. Рефакторинг legacy кода на Kotlin — это не просто "причесывание" кода, а стратегическая инвестиция в будущее проекта.
В этой статье мы разберем полный цикл рефакторинга: от анализа текущего состояния до внедрения современных практик Kotlin. Вы узнаете, как безопасно трансформировать "спагетти-код" в чистую, поддерживаемую архитектуру, не ломая существующую функциональность.
1. Подготовка к рефакторингу: анализ и стратегия
Прежде чем менять код, необходимо понять его текущее состояние. Рефакторинг без плана — путь к катастрофе. Начните с аудита:
1.1 Оценка технического долга
Проведите анализ кодовой базы с помощью инструментов статического анализа (Detekt, Ktlint). Выявите проблемные зоны:
- Дублирование кода (Copy-Paste)
- Длинные функции (>50 строк)
- Классы с высокой связанностью
- Использование Java-стиля (например,
varвместоval)
1.2 Создание страховочной сети
Критически важно обеспечить тестовое покрытие. Напишите хотя бы интеграционные тесты для ключевых сценариев:
class LegacyServiceTest {
private val service = LegacyService()
@Test
fun `should process data correctly after refactoring`() {
val input = listOf("data1", "data2")
val result = service.processData(input)
assertEquals(2, result.size)
assertTrue(result.all { it.isProcessed })
}
}
2. Пошаговый рефакторинг: от Java-стиля к идиоматическому Kotlin
Начнем с самых распространенных проблем в legacy Kotlin-коде. Рассмотрим конкретные приемы.
2.1 Замена Java-коллекций на Kotlin-коллекции
Старый код часто использует изменяемые коллекции из Java:
// Плохо: Java-style
fun getActiveUsers(): MutableList<User> {
val users = mutableListOf<User>()
for (user in allUsers) {
if (user.isActive) {
users.add(user)
}
}
return users
}
// Хорошо: Kotlin-style
fun getActiveUsers(): List<User> = allUsers.filter { it.isActive }
2.2 Избавление от null-небезопасности
Одна из главных проблем legacy кода — повсеместное использование !!. Рефакторинг должен быть направлен на безопасную работу с nullable:
// Плохо: опасный код
fun getUserName(userId: String): String {
val user = userRepository.findById(userId)!!
return user.name!!
}
// Хорошо: безопасная альтернатива
fun getUserName(userId: String): String? {
return userRepository.findById(userId)?.name
}
2.3 Применение sealed классов для состояний
Legacy код часто использует цепочки if-else или enum для представления состояний. Замените их на sealed классы:
// Плохо: enum с when
enum class ResultType { SUCCESS, ERROR, LOADING }
// Хорошо: sealed class
sealed class NetworkResult<out T> {
data class Success<T>(val data: T) : NetworkResult<T>()
data class Error(val message: String, val throwable: Throwable? = null) : NetworkResult<Nothing>()
object Loading : NetworkResult<Nothing>()
}
// Использование
fun handleResult(result: NetworkResult<String>) {
when (result) {
is NetworkResult.Success -> println(result.data)
is NetworkResult.Error -> logError(result.message)
is NetworkResult.Loading -> showLoader()
}
}
3. Архитектурный рефакторинг: от монолита к модульности
Часто legacy код представляет собой "божественный объект" (God Object), который делает всё. Разделение ответственности — ключевой этап.
3.1 Выделение слоев абстракции
Разделите код на четкие слои с помощью интерфейсов:
// До: все в одном классе
class UserManager {
fun saveUser(user: User) { /* SQL + валидация + логика */ }
fun sendEmail(user: User) { /* SMTP + шаблоны */ }
fun validateUser(user: User): Boolean { /* 100 строк */ }
}
// После: разделение ответственности
interface UserRepository {
fun save(user: User)
}
interface EmailService {
fun sendWelcomeEmail(user: User)
}
interface UserValidator {
fun validate(user: User): ValidationResult
}
class UserService(
private val repository: UserRepository,
private val emailService: EmailService,
private val validator: UserValidator
) {
fun registerUser(user: User): Result<User> {
return validator.validate(user)
.map { repository.save(user) }
.map { emailService.sendWelcomeEmail(user); it }
}
}
3.2 Внедрение зависимостей (DI)
Замените ручное создание объектов на DI-фреймворк (Koin, Dagger/Hilt):
// Плохо: жесткая связность
class UserService {
private val repository = UserRepositoryImpl()
private val api = ApiClient()
}
// Хорошо: DI через конструктор
class UserService(
private val repository: UserRepository,
private val api: ApiClient
)
4. Тестирование и безопасность при рефакторинге
Рефакторинг без тестов — это не рефакторинг, а переписывание. Используйте технику "Золотые тесты" (Golden Tests):
4.1 Стратегия параллельного тестирования
Запускайте старые и новые версии кода параллельно, сравнивая результаты:
class RefactoringTest {
private val legacyService = LegacyService()
private val refactoredService = RefactoredService()
@Test
fun `both implementations should produce same results`() {
val testCases = listOf(
InputData("test1", 42),
InputData("test2", 100)
)
testCases.forEach { input ->
val legacyResult = legacyService.compute(input)
val refactoredResult = refactoredService.compute(input)
assertEquals(legacyResult, refactoredResult)
}
}
}