Refactorización de código legacy en Kotlin: estrategia y ejemplos prácticos
Trabajar con código heredado (legacy) es una parte inevitable de la vida de cualquier desarrollador. A menudo, dicho código está escrito en Java, pero se migró a Kotlin sin una profunda reconsideración, o representa un "espagueti" de funciones utilitarias y campos nullable. La refactorización de dicho código no solo mejora la legibilidad, sino que también aumenta el rendimiento y la seguridad de la aplicación. En este artículo, analizaremos una estrategia paso a paso para la refactorización de código legacy en Kotlin con ejemplos reales.
1. Preparación y pruebas: el fundamento de la seguridad
Antes de cambiar el código, es necesario garantizar su correcto funcionamiento después de los cambios. Sin pruebas, la refactorización se convierte en una adivinanza.
- Escriba pruebas de caracterización (Characterization Tests): Ejecute el código existente con diferentes datos de entrada y registre los valores de salida. Estas pruebas serán su "red de seguridad".
- Use la anotación @Deprecated: Si no puede eliminar la API pública antigua de inmediato, márquela como
@Deprecatedindicando la alternativa. - Trabaje en pequeños pasos: Nunca refactorice más de un bloque lógico a la vez. Cada commit debe pasar todas las pruebas.
Ejemplo de una prueba de caracterización para una función antigua:
// Legacy códigofun calculateTotal(items: List<Int>?): Int { if (items == null) return 0 var sum = 0 for (item in items) { sum += item } return sum}
// Test@Testfun `calculateTotal returns sum or zero`() { assertEquals(0, calculateTotal(null)) assertEquals(6, calculateTotal(listOf(1, 2, 3))) assertEquals(0, calculateTotal(emptyList()))}2. Eliminación de la seguridad null y uso de funciones Scope
El código legacy a menudo sufre de comprobaciones excesivas de null. Kotlin proporciona herramientas elegantes para trabajar con tipos nullable.
Qué hacemos: Reemplazamos los if (x != null) anidados por let, run o also.
// Antes de la refactorizaciónfun processUser(user: User?) { if (user != null) { if (user.name != null) { println("User: ${user.name}") } }}
// Después de la refactorizaciónfun processUser(user: User?) { user?.let { u -> u.name?.let { name -> println("User: $name") } }}
// O aún más corto usando runfun processUserShort(user: User?) { user?.run { name?.let { println("User: $it") } }}Este enfoque hace que el código sea más declarativo y reduce la probabilidad de pasar por alto una comprobación.
3. Reemplazo de clases utilitarias por funciones Extension
En los proyectos Java legacy, a menudo se encuentran clases utilitarias estáticas (StringUtils, DateUtils). En Kotlin, es mejor reemplazarlas con funciones extension.
Por qué es importante: Las funciones extension hacen que el código sea más legible y permiten que el IDE las sugiera como métodos de la propia clase.
// Estilo Java legacyobject StringUtils { fun isPalindrome(input: String): Boolean { val cleaned = input.filter { it.isLetterOrDigit() }.lowercase() return cleaned == cleaned.reversed() }}
// Usoval result = StringUtils.isPalindrome("A man, a plan, a canal: Panama")
// Refactorización a Kotlinfun String.isPalindrome(): Boolean { val cleaned = this.filter { it.isLetterOrDigit() }.lowercase() return cleaned == cleaned.reversed()}
// Usoval result = "A man, a plan, a canal: Panama".isPalindrome()También vale la pena considerar el reemplazo de java.util.Date por java.time (LocalDate, Instant) — esto aumentará la seguridad al trabajar con el tiempo.