MÓDULO 13 · CONCEITO 07 DE 15

Refactoring Assistido por IA

Onde o ganho é real, onde o risco é alto, e como preservar comportamento enquanto o código melhora

Tempo de leitura ~20 min Pré-requisito 06 · Code review com IA Próximo 08 · Documentação gerada com IA

Refactoring tem uma definição precisa que importa para entender onde LLMs são úteis nele: é a atividade de alterar a estrutura interna do código sem alterar seu comportamento externo observável. Martin Fowler, em Refactoring: Improving the Design of Existing Code (1999, segunda edição 2018), cataloga centenas de transformações específicas — Extract Method, Rename Variable, Move Class, Replace Conditional with Polymorphism — cada uma com passos mecânicos definidos e verificação por testes de que o comportamento não mudou.

Essa definição revela imediatamente por que LLMs são bons em algumas formas de refactoring e ruins em outras. Refactoring mecânico — Extract Method, Rename, Inline Variable, Move — é transformação de código que segue regras claras e verificáveis. O LLM aplica a regra, os testes verificam que o comportamento não mudou, e o processo está completo. Refactoring arquitetural — mudar a fronteira entre módulos, redesenhar contratos entre componentes, migrar para um padrão diferente de estruturação — requer julgamento sobre o design do sistema como um todo, conhecimento de como os componentes interagem, e decisão sobre qual estrutura serve melhor os requisitos futuros. LLMs não têm esse julgamento; podem propor reestruturações que parecem melhores localmente mas pioram o sistema globalmente.

A linha entre os dois tipos não é sempre clara, mas a heurística é: se o refactoring pode ser descrito completamente em termos de transformações no código atual sem referência a decisões sobre o que o código deveria ser, é provavelmente mecânico. Se requer articular "o código deveria ser estruturado de forma diferente por causa de X", é arquitetural — e requer o engenheiro como tomador de decisão, com o LLM como executor das transformações decididas.

Refactoring mecânico — onde o ganho é real e confiável

Rename em larga escala. Renomear um símbolo que aparece em dezenas de arquivos é tedioso e sujeito a erro humano — é fácil perder uma ocorrência, especialmente em strings ou comentários que não são capturados por ferramentas de rename automático. LLMs fazem isso com velocidade alta e, quando instruídos a listar cada ocorrência antes de renomear, produzem transparência sobre o que está mudando. A verificação é simples: buscar o nome antigo no repositório depois da mudança e confirmar que não aparece onde não deveria.

Extract Method e Extract Function. Identificar um bloco de código com uma responsabilidade coesa, extraí-lo em método ou função própria com nome descritivo, e substituir todas as chamadas — transformação que IDEs fazem automaticamente para casos simples, mas que LLMs fazem melhor para casos onde a extração requer ajuste de parâmetros, decisão sobre o nível de abstração, ou renomeação de variáveis locais para fazer sentido fora do contexto original.

Migração de API ou biblioteca. Atualizar código que usa a versão antiga de uma biblioteca para a API nova: trocar HttpClient.GetAsync pelo padrão com IHttpClientFactory, migrar de Newtonsoft.Json para System.Text.Json, atualizar chamadas de Polly v7 para Polly v8. Esse tipo de migração segue regras de correspondência que o LLM pode aprender do changelog da biblioteca e aplicar sistematicamente em toda a base. O risco está em sutilezas de comportamento entre versões — que precisam ser verificadas pelos testes, não assumidas.

Eliminação de duplicação. Identificar código duplicado (não só cópia-e-cola literal, mas duplicação de lógica com variações superficiais), extrair o padrão comum em abstração compartilhada, e substituir as instâncias. LLMs identificam duplicação de lógica bem quando os padrões são similares o suficiente para reconhecimento textual; duplicação semântica mais sutil (dois algoritmos diferentes que implementam a mesma lógica de negócio de formas completamente diferentes) requer o olho humano.

Aplicação consistente de padrão. "Todos os handlers devem seguir o padrão X, mas metade ainda usa o padrão antigo Y" — migrar de Y para X em todos os handlers. Dado um exemplo de handler migrado como referência, o LLM pode aplicar a mesma transformação nos handlers restantes sistematicamente. O risco é diferenças sutis entre os handlers que tornam a transformação não-trivial em alguns casos — que precisam ser identificadas na revisão do diff antes de aplicar.

princípio orientador

A garantia de que refactoring não alterou comportamento não vem da inspeção do código — vem dos testes. Se o refactoring for aplicado por LLM e os testes passarem, o comportamento foi preservado para os casos cobertos pelos testes. Se os testes têm lacunas (módulo 05 mostrou como mutation testing revela isso), o refactoring pode ter introduzido regressão em comportamento não-testado. A qualidade da suíte de testes é o pré-requisito do refactoring seguro, com ou sem IA.

Refactoring arquitetural — onde o risco é alto

Refactoring arquitetural é onde LLMs podem produzir dano real se usados sem supervisão cuidadosa. A diferença para o mecânico não é de tamanho — é de tipo. Um refactoring arquitetural envolve decisões sobre o design do sistema que não estão contidas no texto do código atual.

Reestruturação de módulos e namespaces. Mover classes entre módulos parece mecânico mas raramente é: a fronteira entre módulos reflete uma decisão arquitetural sobre quais responsabilidades pertencem juntas. Mover classes sem entender por que a fronteira existe como está pode violar a coesão que o design original tinha. O LLM não sabe por que os módulos estão estruturados como estão — sabe só como estão.

Mudança de contrato entre componentes. Mudar a assinatura de uma interface que múltiplos componentes implementam, mudar o schema de eventos em um sistema orientado a eventos, mudar o formato de serialização de dados persistidos — essas mudanças têm impacto que vai além do que o LLM vê nos arquivos que está editando. Consumidores externos, dados persistidos em banco, eventos em fila, contratos com outros times — todos esses dependem do contrato e precisam ser considerados explicitamente.

Introdução de nova abstração. "Extraia uma abstração para esse comportamento duplicado" é instrução perigosa para um LLM sem supervisão próxima. O LLM vai extrair a abstração que parece correta para o padrão que observa nos arquivos que leu — mas a abstração certa para o sistema pode ser diferente, dependendo de como o comportamento vai evoluir, quais outros componentes vão precisar implementá-la, e qual nível de generalidade é útil versus prematuro. Abstrações erradas são mais caras de desfazer do que código duplicado.

armadilha clássica

"Refatore esse código para ficar mais limpo" é a instrução mais perigosa que se pode dar a um LLM em modo de refactoring. Sem definição do que "mais limpo" significa no contexto do sistema, o LLM vai aplicar suas próprias heurísticas — que podem estar corretas para o código local mas erradas para o sistema. O resultado é código que parece melhor mas diverge das convenções e decisões arquiteturais do projeto. Instruções de refactoring precisam especificar a transformação desejada, não o objetivo abstrato.

O protocolo: LLM executa, humano verifica

O protocolo correto para refactoring assistido por LLM tem etapas bem definidas que preservam a segurança sem perder a velocidade.

1. Humano decide o refactoring. O engenheiro decide o que vai ser refatorado e como — não o LLM. "Vou extrair a lógica de cálculo de desconto em uma classe separada porque ela está duplicada em três places e vai precisar de extensão no próximo trimestre" é a decisão do engenheiro. O LLM executa, não decide.

2. Confirmar que os testes existentes cobrem o comportamento a ser refatorado. Antes de instruir o LLM, rodar os testes e verificar que passam. Se a cobertura for baixa nas áreas que serão refatoradas, adicionar testes antes de refatorar — com ou sem LLM ajudando a escrevê-los.

3. Instruir o LLM com transformação específica e restrição de comportamento. "Extraia a lógica de cálculo de desconto das linhas 45-78 do OrderService em uma classe DiscountCalculator. A classe deve implementar a interface IDiscountCalculator (crie a interface também). O comportamento deve ser idêntico — não otimize, não mude a lógica, só mova." A instrução explícita "não mude a lógica" é importante: sem ela, o LLM pode "melhorar" a lógica durante o refactoring, o que quebra a garantia de preservação de comportamento.

4. Rodar os testes após a mudança. Se todos passam, o comportamento foi preservado para os casos cobertos. Se algum falha, o LLM alterou comportamento não-intencionalmente — identificar onde e corrigir antes de continuar.

5. Revisar o diff com foco em mudanças de comportamento, não de estrutura. O diff vai mostrar muita movimentação de código. O que o revisor procura não é a movimentação (que é intencional) mas qualquer mudança sutil na lógica — uma condição que mudou, uma variável renomeada de forma que altera semântica, uma chamada de método que foi omitida.

C# — instrução de refactoring mecânico com restrição explícita
// Instrução ao LLM:
// "Refatore OrderService.cs aplicando as seguintes
//  transformações, nessa ordem, uma de cada vez:
//
// 1. Extract Method: linhas 45-78 (cálculo de desconto)
//    → novo método privado CalculateDiscount(order, customer)
//    Não altere a lógica — só extraia.
//
// 2. Extract Class: mova CalculateDiscount para nova classe
//    DiscountCalculator em Services/DiscountCalculator.cs
//    com interface IDiscountCalculator em Contracts/
//    Injete via construtor em OrderService.
//
// 3. Rename: 'disc' → 'discount' em todo o arquivo OrderService.cs
//    (não em outros arquivos ainda)
//
// Após cada transformação, confirme que nenhuma lógica
// foi alterada — só estrutura. Não 'melhore' a lógica.
// Os testes em OrderServiceTests.cs devem continuar passando."

// Por que uma de cada vez: refactorings encadeados
// num único passo são difíceis de revisar e de reverter
// se algo der errado. Cada passo tem um diff claro
// e verificação independente.

A instrução "não melhore a lógica" é a proteção mais importante. LLMs têm impulso de "melhorar" o código que estão tocando — o que viola a garantia de preservação de comportamento do refactoring.

Python — migração de API com preservação de comportamento
# Migração de requests para httpx (API diferente para async):
#
# Instrução ao LLM:
# "Migre o módulo api_client.py de requests para httpx.
#  A versão httpx deve ser assíncrona (httpx.AsyncClient).
#
# Mapeamento de API:
# - requests.get(url, **kwargs) → await client.get(url, **kwargs)
# - requests.post(url, json=...) → await client.post(url, json=...)
# - response.json() → response.json() (mesmo)
# - response.status_code → response.status_code (mesmo)
# - response.raise_for_status() → response.raise_for_status() (mesmo)
#
# Atenção:
# - Timeout em requests é timeout=(connect, read); em httpx é
#   httpx.Timeout(connect=N, read=N) — ajuste onde timeout
#   está configurado.
# - Session em requests → AsyncClient como context manager.
#   Garanta que o client seja fechado (use async with).
#
# Testes em test_api_client.py usam responses mock de requests.
# Migre os mocks para respx (equivalente para httpx).
# Comportamento dos testes não deve mudar — só a biblioteca
# de mock."

Fornecer o mapeamento explícito de API antiga para nova remove ambiguidade sobre como cada chamada deve ser traduzida. Sem isso, o LLM pode fazer escolhas de API que diferem da intenção, especialmente nas diferenças de timeout.

Go — eliminação de duplicação com extração de padrão comum
// Três handlers com lógica duplicada de validação de ownership:
//
// Instrução ao LLM:
// "Os três handlers abaixo (GetOrder, UpdateOrder, CancelOrder)
//  têm a mesma lógica de verificação de ownership (linhas
//  12-19 em cada um). Extraia em uma função helper:
//
//   func requireOwnership(ctx context.Context, resourceID string,
//       userID string, store OwnershipChecker) error
//
//  onde OwnershipChecker é uma interface com um único método:
//
//   type OwnershipChecker interface {
//     IsOwner(ctx context.Context, resourceID, userID string) (bool, error)
//   }
//
// Substitua as três implementações inline pela chamada à função.
// A interface deve ser satisfeita por OrderStore (que já tem
// o método, só renomeie para IsOwner se necessário).
//
// Restrição: não mude o comportamento de erro — o handler
// deve continuar retornando 403 Forbidden quando ownership
// falha, com a mesma mensagem atual."
//
// Por que definir a interface antes: sem interface explícita,
// o LLM pode criar abstração acoplada ao OrderStore específico,
// que não serve para futuros recursos que precisem do mesmo check.

Definir a interface antes de pedir a extração garante que a abstração resultante tem o nível correto de generalidade. O LLM executa a extração seguindo a interface definida pelo engenheiro.

Refactoring em larga escala: abordagem incremental

Refactorings grandes — migrar uma base de 50 arquivos de uma versão de API para outra, aplicar um novo padrão arquitetural em toda a codebase, eliminar uma abstração que se mostrou errada — são onde LLMs têm o maior potencial de acelerar trabalho genuinamente tedioso e também onde o maior risco de dano emerge.

A abordagem que funciona é incremental e verificável a cada passo. Em vez de instruir o LLM a migrar todos os 50 arquivos de uma vez, migrar um arquivo, verificar que os testes passam, fazer um commit, e então migrar o próximo. Esse ritmo de micro-commits tem três vantagens: cada passo é reversível independentemente se der errado, o diff de cada commit é pequeno e revisável, e o progresso é rastreável — você sabe exatamente em qual passo está e quantos faltam.

Para larga escala, o LLM pode ajudar a gerar um script de migração em vez de aplicar as mudanças diretamente. "Dado o padrão de migração que apliquei manualmente no arquivo X, gere um script Python/sed/ast-grep que aplica a mesma transformação em todos os arquivos que correspondem ao padrão Y". Transformação via script tem a vantagem de ser auditável (você lê o script antes de executar), repetível (pode ser re-executado se o estado mudar), e verificável (o script pode ser testado em subset antes de aplicar em toda a base).

Quando não refatorar com LLM

Há situações em que usar LLM para refactoring aumenta o risco sem benefício proporcional: código de segurança crítica (autenticação, autorização, criptografia) onde bugs sutis de comportamento têm consequências sérias e os testes podem não cobrir todos os casos relevantes; código sem testes onde não há rede de segurança para verificar preservação de comportamento; código em estado de modificação ativa por múltiplos membros do time onde o diff do refactoring vai conflitar com trabalho em andamento; e código onde o engenheiro não entende o comportamento atual — refatorar código que você não entende produz código diferente que você não entende.

Essa última categoria é especialmente importante: refactoring pressupõe que você sabe o que o código faz e quer melhorar como ele faz. Se você não sabe o que o código faz, a primeira etapa é entender — com ou sem ajuda do LLM — antes de qualquer transformação.

Como praticar

  1. Catalogar os refactorings mecânicos do projeto. Olhe para a base de código atual e identifique três a cinco oportunidades de refactoring mecânico: código duplicado, nome que não comunica a intenção, método longo com responsabilidade clara de extração, migração de padrão antigo para padrão atual. Para cada oportunidade, escreva a instrução de refactoring como você passaria para o LLM — específica, com transformação definida e restrição de comportamento. O exercício de escrever a instrução antes de executar revela onde a transformação desejada está bem definida e onde precisa de mais análise.
  2. Migração guiada de um módulo. Escolha um módulo que usa padrão antigo (library deprecada, estilo anterior ao adotado pelo time, abstração que ficou errada). Aplique o protocolo completo: confirmar testes, instruir LLM com transformação específica, rodar testes, revisar diff focando em mudanças de comportamento. Documente onde o LLM precisou de correção — quais aspectos da transformação ele errou ou "melhorou" sem querer.
  3. Comparar refactoring manual com assistido. Para a mesma oportunidade de refactoring, faça a transformação você mesmo num branch e com LLM em outro. Compare: qual produziu código melhor? Onde o LLM fez algo que você não teria feito? Onde você fez algo que o LLM não fez? Essa comparação calibra onde o LLM adiciona valor no seu contexto específico e onde o seu julgamento é insubstituível.

Referências para aprofundar

  1. livro Refactoring: Improving the Design of Existing Code — Martin Fowler (2ª ed., 2018). O catálogo canônico de refactorings com passos mecânicos definidos. A fonte primária para entender quais transformações são mecânicas (onde LLM ajuda) e quais requerem julgamento arquitetural.
  2. livro Working Effectively with Legacy Code — Michael Feathers (2004). Técnicas para refatorar código sem testes — o caso mais difícil. As "seams" de Feathers são o que torna possível adicionar testes antes de refatorar, que é o pré-requisito para refactoring seguro com LLM.
  3. artigo Workflows for AI-Assisted Refactoring — Martin Fowler (2024). martinfowler.com — Fowler explorando como agentes de IA se encaixam nos workflows de refactoring que ele definiu. Perspectiva do autor do catálogo de refactoring sobre como LLMs mudam a prática.
  4. artigo ast-grep: structural search and replace — ast-grep.github.io. Ferramenta de transformação de código baseada em AST — alternativa a substituição textual para migrações de API. Complementa LLMs para transformações em larga escala que requerem precisão sintática.
  5. artigo Codemod: Automated large-scale codebase refactoring — Facebook Engineering. A abordagem de Facebook para refactoring em larga escala via scripts automatizados. O modelo de "script de transformação auditável" descrito neste conceito segue essa tradição.
  6. vídeo Refactoring with AI Coding Assistants — NDC Oslo (2024). youtube.com — Demonstração ao vivo de refactoring com Claude Code e Cursor. Útil para ver o ciclo real em ação, incluindo onde o agente erra e como corrigir.
  7. livro A Philosophy of Software Design — John Ousterhout (2018). Capítulo sobre deep modules e a diferença entre complexidade acidental e essencial. O julgamento que orienta refactoring arquitetural — o que LLMs não têm e o engenheiro sênior precisa ter antes de instruir o agente.
  8. artigo On the Naturalness of Software — Hindle et al. (2012). O paper que mostrou que código é mais "natural" (previsível) do que texto humano geral — a base teórica de por que LLMs funcionam bem em transformações mecânicas de código mas menos bem em transformações que requerem criatividade.
  9. artigo The Hidden Costs of Premature Abstraction — Dan Luu (2023). danluu.com — Sobre o custo de abstrações criadas antes de entender completamente o domínio. Relevante para a seção sobre refactoring arquitetural: LLMs criam abstrações prematuras porque não têm o contexto para avaliar se a abstração é correta para o sistema.
  10. docs Roslyn Analyzers and Code Fixes — Microsoft. learn.microsoft.com — Para C#, Roslyn analyzers são a alternativa tipada a transformações textuais. LLMs podem ajudar a escrever analyzers que depois executam transformações com segurança de tipo que LLMs sozinhos não têm.
  11. artigo Evaluating LLMs on Code Refactoring Tasks — Liu et al. (2024). arxiv.org — Benchmark de LLMs em tarefas de refactoring: quais transformações eles aplicam corretamente, onde introduzem bugs, e como a presença de testes afeta a qualidade do output.
  12. livro Clean Code — Robert C. Martin (2008). O catálogo de code smells que motivam refactoring. Útil para ter vocabulário compartilhado ao instruir o LLM: "essa função tem feature envy, extraia a lógica de validação para onde os dados vivem" é mais preciso do que "melhore essa função".