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.
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.
"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.
// 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.
# 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.
// 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
- 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.
- 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.
- 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
- livro Refactoring: Improving the Design of Existing Code — Martin Fowler (2ª ed., 2018).
- livro Working Effectively with Legacy Code — Michael Feathers (2004).
- artigo Workflows for AI-Assisted Refactoring — Martin Fowler (2024).
- artigo ast-grep: structural search and replace — ast-grep.github.io.
- artigo Codemod: Automated large-scale codebase refactoring — Facebook Engineering.
- vídeo Refactoring with AI Coding Assistants — NDC Oslo (2024).
- livro A Philosophy of Software Design — John Ousterhout (2018).
- artigo On the Naturalness of Software — Hindle et al. (2012).
- artigo The Hidden Costs of Premature Abstraction — Dan Luu (2023).
- docs Roslyn Analyzers and Code Fixes — Microsoft.
- artigo Evaluating LLMs on Code Refactoring Tasks — Liu et al. (2024).
- livro Clean Code — Robert C. Martin (2008).