Refatorar com IA sem quebrar o que funciona
Um fluxo seguro para refatorar com assistentes de IA: testes de caracterização, uma técnica por vez, checagem de comportamento e os vícios do código gerado.
Refatorar tem uma definição precisa: mudar a estrutura interna do código sem mudar o comportamento externo. Não é reescrever, não é adicionar funcionalidade, não é corrigir bug. É deixar a próxima mudança mais barata sem quebrar o que funciona hoje.
Assistentes de IA mudam essa disciplina de dois jeitos. Eles encontram code smells mais rápido e com mais constância do que uma leitura manual, e aplicam técnicas conhecidas (Extract Method, Introduce Parameter Object, Replace Conditional with Polymorphism) em segundos. O risco também é real: o modelo pode mudar o comportamento enquanto afirma que preservou. A disciplina está em saber usar com segurança.
O contrato: só estrutura
A frase mais útil que eu coloco num pedido de refatoração é esta:
Isto é uma refatoração estrutural. Não corrija bugs, não melhore algoritmos,
não adicione tratamento de erro e não mude comportamento, mesmo que veja
oportunidade. Se encontrar algo que pareça bug, liste separadamente no final.
Sem ela, o modelo tende a “aproveitar” a refatoração para consertar o que parece errado. Às vezes o “bug” é comportamento de que alguém depende, e a correção some no meio de um diff estrutural que ninguém consegue revisar direito. Bug encontrado vira outro ticket e outro commit.
O fluxo seguro
Refatorar sem testes não é refatorar, é apostar. A sequência que eu sigo:
- Travar o comportamento com testes. Se o código não tem testes, o primeiro passo é escrever testes de caracterização.
- Uma técnica por vez. Nada de “melhore este código”. Peça “aplique Extract Method nesta função”. Misturar várias refatorações num passo impede saber qual delas causou o problema.
- Rodar os testes depois de cada passo. Verde, commit. Vermelho, reverte e entende antes de seguir.
- Checar a afirmação de que nada mudou. Antes de aceitar, peça ao modelo para comparar as duas versões (o prompt está mais abaixo).
Testes de caracterização
Eles documentam o que o código faz, não o que deveria fazer. A ideia é do Michael Feathers, em Working Effectively with Legacy Code, e combina muito bem com IA, porque escrever esse tipo de teste é trabalho mecânico e extenso.
Escreva testes de caracterização para esta função. O objetivo NÃO é provar que
o comportamento está correto, e sim travar o comportamento atual para que
qualquer refatoração que mude a saída seja detectada.
- Nome de cada teste: O QUE a função devolve para QUAL entrada.
- Cubra entradas típicas, limites, vazio/nulo e condições de erro.
- Não corrija nada: teste como está, mesmo que pareça errado.
No final, liste comportamentos que parecem suspeitos, para eu decidir
separadamente se são bugs.
A lista de comportamentos suspeitos no final costuma ser a parte mais valiosa da sessão.
Inventário antes da mudança
Antes de aplicar qualquer técnica, peça um inventário priorizado:
Audite este código em busca de oportunidades de refatoração. Para cada smell:
nome (Long Method, Feature Envy, Data Clump...), trechos afetados, técnica que
resolve e impacto em legibilidade e facilidade de mudança (baixo/médio/alto).
Ordene por impacto. Não gere o código refatorado ainda.
O “não gere ainda” é proposital. Se o modelo já entrega o código, você pula a etapa de decidir se cada mudança vale o custo. A priorização é sua.
As técnicas que mais uso
Extract Method
A técnica mais frequente. O gatilho é um bloco dentro de uma função que poderia ser descrito por um nome diferente do nome da função. Comentários do tipo // busca o usuário e // calcula o resumo são dicas diretas: cada um é uma extração esperando nome.
// depois da extração: a função principal vira um índice do que acontece
function generateMonthlyReport(userId: string, month: number, year: number): MonthlyReport {
const user = findUserOrThrow(userId);
const period = monthRange(month, year);
const transactions = findTransactions(userId, period);
return toReport(user, { month, year }, summarize(transactions), transactions);
}
function summarize(transactions: Transaction[]): Summary {
const sum = (type: TransactionType) =>
transactions.filter(t => t.type === type).reduce((acc, t) => acc + t.amount, 0);
const totalIn = sum('credit');
const totalOut = sum('debit');
return { totalIn, totalOut, balance: totalIn - totalOut };
}
Peça que os nomes descrevam a intenção, não repitam o comentário.
Introduce Parameter Object
Quando os mesmos três ou quatro parâmetros aparecem juntos em várias assinaturas (origem, destino, partida, retorno), eles são um conceito sem nome. O objeto novo deve validar os próprios invariantes:
class FlightRoute {
constructor(
readonly origin: string,
readonly destination: string,
readonly departure: Date,
readonly returnDate?: Date,
) {
if (!origin || !destination) throw new Error('Origin and destination are required');
if (returnDate && returnDate < departure) throw new Error('Return must be after departure');
}
get isRoundTrip(): boolean {
return this.returnDate !== undefined;
}
}
// as assinaturas passam a receber o conceito, não os quatro valores soltos
declare function searchFlights(route: FlightRoute): Promise<Flight[]>;
declare function bookFlight(route: FlightRoute, passengerId: string): Promise<Booking>;
Repare que a validação no construtor é comportamento novo. Se as funções antigas não validavam, isso é uma mudança e merece commit próprio.
Replace Conditional with Polymorphism, sem quebrar a API
Quando o mesmo switch por tipo aparece em vários métodos, o tipo deveria carregar o comportamento.
type ShippingType = 'standard' | 'express' | 'overnight';
interface Order { shippingType: ShippingType; weight: number; }
// antes: o mesmo switch em três métodos
export class Shipping {
calculateCost(order: Order): number {
switch (order.shippingType) {
case 'standard': return order.weight * 2.5;
case 'express': return order.weight * 5.0 + 10;
case 'overnight': return order.weight * 8.0 + 25;
}
}
estimateDeliveryDays(order: Order): number {
switch (order.shippingType) {
case 'standard': return 7;
case 'express': return 3;
case 'overnight': return 1;
}
}
getCarrierCode(order: Order): string {
switch (order.shippingType) {
case 'standard': return 'STD-01';
case 'express': return 'EXP-02';
case 'overnight': return 'OVN-03';
}
}
}
Um erro comum, inclusive em respostas de IA, é aproveitar para trocar a API: os chamadores passam a usar Shipping.for(tipo).calculateCost(...). Isso deixou de ser refatoração; é uma mudança de interface que obriga a mexer em todos os chamadores. A versão que preserva o contrato mantém os três métodos públicos com as mesmas assinaturas e move só o conteúdo:
interface ShippingRule {
cost(order: Order): number;
deliveryDays: number;
carrierCode: string;
}
const RULES: Record<ShippingType, ShippingRule> = {
standard: { cost: o => o.weight * 2.5, deliveryDays: 7, carrierCode: 'STD-01' },
express: { cost: o => o.weight * 5.0 + 10, deliveryDays: 3, carrierCode: 'EXP-02' },
overnight: { cost: o => o.weight * 8.0 + 25, deliveryDays: 1, carrierCode: 'OVN-03' },
};
// depois: mesma classe, mesmos métodos, mesmas assinaturas
export class Shipping {
calculateCost(order: Order): number {
return RULES[order.shippingType].cost(order);
}
estimateDeliveryDays(order: Order): number {
return RULES[order.shippingType].deliveryDays;
}
getCarrierCode(order: Order): string {
return RULES[order.shippingType].carrierCode;
}
}
Os chamadores não mudam, os testes de caracterização continuam válidos, e um tipo novo de frete é uma entrada nova no mapa (o Record faz o compilador exigir a entrada quando o tipo for adicionado à união). Se depois fizer sentido expor ShippingRule diretamente, isso é um segundo passo, com a mudança de API explícita no commit.
Esse exemplo também mostra por que a checagem de comportamento importa. Se shippingType vier de um JSON externo com um valor inesperado, o switch original devolve undefined e a versão nova lança TypeError ao acessar RULES[...]. A diferença só aparece em tempo de execução, e só alguém procurando por ela vai encontrar.
Move Method e Extract Class
Um método que só usa dados de outra classe (feature envy) vai para a classe dona dos dados. Uma classe cujos campos e métodos formam dois grupos que não se falam (identidade e endereço num User, por exemplo) vira duas, com composição. São refatorações em que a IA acerta muito, porque o critério é visível no próprio código.
A checagem de comportamento
Depois de cada refatoração gerada, antes de aceitar:
Compare a versão original e a refatorada e liste cada caso em que podem se
comportar diferente:
1. Retornos: todos os caminhos são equivalentes?
2. Exceções: a nova lança onde a original não lançava, ou engole o que a
original propagava?
3. Efeitos colaterais: algo escrito, logado ou emitido de forma diferente?
4. Nulo e ausente: alguma trata valor ausente de outro jeito?
5. Curto-circuito: alguma extração muda quando a avaliação para?
6. Mutação: algum objeto passa a ser (ou deixa de ser) alterado no lugar?
Para cada diferença, diga se é intencional ou não.
Ela pega a maioria das mudanças silenciosas, mas não substitui os testes. É uma segunda leitura, não uma prova.
Os vícios do código que a IA escreve
Refatorar com IA também significa refatorar o que a IA gerou. Alguns padrões aparecem com tanta frequência que vale procurar ativamente:
- Função que faz tudo. Se o pedido descreve cinco responsabilidades, o modelo entrega uma função com cinco responsabilidades. Ele cumpriu o que foi pedido. Divida o pedido antes de enviar.
- Defesa em excesso. Checagens de nulo e de tipo em parâmetros internos que já chegaram validados, e
try/catchque troca uma exceção específica por uma genérica. Valide na fronteira do sistema e confie no contrato por dentro. - Números mágicos.
>= 3e* 0.95espalhados pelo código. Peça para extrair limites e taxas como constantes nomeadas. - Estilo destoante. O modelo não conhece as convenções do time. Cole um trecho existente no pedido (“siga este padrão”) e a saída melhora muito.
- Extração demais. Um bloco de três linhas vira função, dois passos viram duas funções, e entender o fluxo exige pular entre dez arquivos. Extraia quando o nome novo acrescenta informação, não só porque o bloco é separável.
- Abstração prematura. Strategy, factory e registry para um caso que tem duas variantes e não vai ter terceira. Esse merece uma seção própria.
Inversão de dependência sem exagero
O DIP diz que módulos de alto nível não devem depender de módulos de baixo nível; os dois dependem de abstrações. Na prática, um OrderService que faz new PostgresOrderRepository() no construtor amarra regra de negócio a banco: trocar o banco exige mexer na regra, e testar a regra exige banco. Injetar a dependência pelo construtor, tipada por uma interface que o próprio serviço define, resolve as duas coisas.
O exagero oposto é criar interface para tudo, e a IA faz muito isso quando você pede “siga SOLID”. Interface tem custo: mais um arquivo, mais uma indireção. Ela se paga quando existem duas implementações (a de produção e um fake de teste já contam) ou quando você quer proteger um módulo estável de um detalhe volátil, como um gateway de pagamento de terceiros. Uma interface com uma única implementação e nenhum teste usando outra é cerimônia.
O teste do padrão
Antes de aceitar um padrão de projeto sugerido pela IA, faça uma pergunta: que mudança concreta e provável este padrão torna mais fácil?
Se a resposta for “nenhuma que eu consiga imaginar hoje”, o padrão está adicionando indireção sem flexibilidade. Um Strategy com uma estratégia, um Observer entre dois métodos da mesma classe, uma Facade sobre duas chamadas internas: tudo isso deixa o código atual mais difícil de ler em troca de uma flexibilidade que talvez nunca seja usada. Se a mudança for provável (um terceiro meio de pagamento já está no roadmap), o padrão se paga. Três linhas parecidas são melhores do que a abstração errada.
Uma sequência para código legado
Para a classe de 500 linhas, sem teste e sem dono claro:
- Entender antes de mexer. Peça uma explicação seção por seção, com entradas, saídas, efeitos colaterais e dependências externas, sem sugestões.
- Testes de caracterização para o que você entendeu.
- Renomear primeiro. Nome bom não custa nada e clareia todo o resto.
- Extract Method nos blocos marcados por comentário.
- Extract Class quando os agrupamentos ficarem visíveis.
- Polimorfismo por último. É a mudança estrutural mais profunda e só fica clara quando o resto já está bem nomeado e extraído.
Há um efeito que se acumula a favor: código bem estruturado gera sugestões melhores. O modelo raciocina muito melhor sobre uma função curta e bem nomeada do que sobre um bloco de duzentas linhas emaranhado. Cada sessão de refatoração deixa o código mais fácil para a próxima, com ou sem IA.
O que a ferramenta não substitui é o julgamento sobre quando abstrair e quando deixar inline, quando a terceira repetição justifica uma função compartilhada e quando é coincidência, quando um nome está errado porque o conceito de negócio mudou. Use a IA para tirar o atrito mecânico (nomes, extrações, testes, consistência) e gaste o tempo economizado nessas decisões.