Refatoração de código legado em Kotlin: estratégia e exemplos práticos
Trabalhar com código legado é uma parte inevitável da vida de qualquer desenvolvedor. Muitas vezes, esse código é escrito em Java, mas migrado para Kotlin sem uma profunda reavaliação, ou representa um "espaguete" de funções utilitárias e campos anuláveis. A refatoração desse código não só melhora a legibilidade, mas também aumenta o desempenho e a segurança da aplicação. Neste artigo, vamos detalhar uma estratégia passo a passo para refatorar código legado em Kotlin com exemplos reais.
1. Preparação e teste: o alicerce da segurança
Antes de alterar o código, é necessário garantir seu funcionamento correto após as mudanças. Sem testes, a refatoração se torna um jogo de adivinhação.
- Escreva Testes de Caracterização (Characterization Tests): Execute o código existente com diferentes dados de entrada e registre os valores de saída. Esses testes serão sua "rede de segurança".
- Use a anotação @Deprecated: Se você não puder remover a API pública antiga imediatamente, marque-a como
@Deprecatedindicando a alternativa. - Trabalhe em pequenos passos: Nunca refatore mais de um bloco lógico por vez. Cada commit deve passar em todos os testes.
Exemplo de um teste de caracterização para uma função antiga:
// Código legadofun calculateTotal(items: List<Int>?): Int { if (items == null) return 0 var sum = 0 for (item in items) { sum += item } return sum}
// Teste@Testfun `calculateTotal returns sum or zero`() { assertEquals(0, calculateTotal(null)) assertEquals(6, calculateTotal(listOf(1, 2, 3))) assertEquals(0, calculateTotal(emptyList()))}2. Eliminação da segurança contra nulos e uso de funções de escopo (Scope-functions)
O código legado frequentemente sofre com verificações excessivas de nulo. O Kotlin fornece ferramentas elegantes para trabalhar com tipos anuláveis.
O que fazemos: Substituímos if (x != null) aninhados por let, run ou also.
// Antes da refatoraçãofun processUser(user: User?) { if (user != null) { if (user.name != null) { println("User: ${user.name}") } }}
// Após a refatoraçãofun processUser(user: User?) { user?.let { u -> u.name?.let { name -> println("User: $name") } }}
// Ou ainda mais curto com runfun processUserShort(user: User?) { user?.run { name?.let { println("User: $it") } }}Essa abordagem torna o código mais declarativo e reduz a probabilidade de esquecer uma verificação.
3. Substituição de classes utilitárias por funções de extensão (Extension-functions)
Em projetos Java legados, é comum encontrar classes utilitárias estáticas (StringUtils, DateUtils). Em Kotlin, é melhor substituí-las por funções de extensão.
Por que isso é importante: As funções de extensão tornam o código mais legível e permitem que a IDE as ofereça como métodos da própria classe.
// Estilo Java legadoobject 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")
// Refatoração para 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()Também vale a pena considerar a substituição de java.util.Date por java.time (LocalDate, Instant) — isso aumentará a segurança no trabalho com tempo.