Logotipo do ConstructiCat
CodeBust.
Explorar a seção ▾

Evitação de Refatoração.

Assistentes de IA raramente reestruturam o código existente: eles penduram novos ramos, parâmetros de flag e camadas de wrapper sobre o que já existe, então funções e condicionais não param de crescer enquanto a refatoração — o retrabalho que mantém uma base de código saudável — desaparece em silêncio.

##Sinais e sintomas

O revisor vê um diff composto quase inteiramente de adições a código que já existe. O modelo resolveu o prompt empilhando mais um ramo, uma flag ou uma camada sobre a função indicada, em vez de remodelar essa função para que a mudança se encaixe. Sinais reveladores:

  • Diffs só de adições: nenhuma linha movida, renomeada ou apagada; a função tocada só fica mais longa.
  • Parâmetros de flag: isWholesale, skipValidation, legacy = false acrescentados a uma assinatura existente, cada um mudando o comportamento lá no fundo do corpo.
  • Proliferação de ramos: cada novo requisito vira mais um else if ou case na mesma função em vez de uma tabela de consulta, uma estratégia ou uma chamada polimórfica.
  • Um wrapper por cima: processOrderV2 chama processOrder e remenda o resultado em vez de alterá-lo; o caminho antigo e o novo convivem lado a lado.
  • Métricas subindo no mesmo lugar: complexidade cognitiva, tamanho e número de parâmetros das mesmas funções sobem commit após commit — um hotspot que cresce.
  • Flags mortas nunca removidas: parâmetros que todos os chamadores agora passam do mesmo jeito, mantidos "por via das dúvidas".
// Cada pedido acrescentou um parâmetro e um ramo; nada jamais foi reestruturado.
function priceOrder(
  items: Item[],
  discount?: number,
  isWholesale = false,
  applyVat = true,
  couponCode?: string,
) {
  let total = items.reduce((s, i) => s + i.price * i.qty, 0);
  if (discount) total = total * (1 - discount);
  if (isWholesale) {
    if (total > 1000) total = total * 0.9;
    else if (total > 500) total = total * 0.95;
  }
  if (couponCode === "SPRING") total = total - 10;
  else if (couponCode === "VIP") total = total * 0.85;
  if (applyVat) total = total * 1.2;
  return total;
}

Cada pedido acrescentou um parâmetro e um ramo, e cada diff mexeu só no final da função. Nada está duplicado, então um detector de clones fica em silêncio; o smell aparece como crescimento de complexidade e de tamanho.

##Causas do problema

Por que os modelos produzem isso

  • Viés aditivo da predição do próximo token. Um LLM completa o prompt que tem à frente. Acrescentar um ramo ou um parâmetro no ponto da mudança é a continuação localmente mais provável e de menor risco; remodelar a função exige manter no contexto de trabalho o módulo inteiro e seus chamadores e prever uma edição globalmente consistente, o que é mais difícil e não é o que o prompt imediato recompensa.
  • Contexto escasso do repositório. Assistentes de código raramente carregam todos os chamadores e testes. Sem ver quem depende de uma função, o modelo não tem como saber se é seguro reestruturá-la, então deixa a estrutura como está e adiciona um if onde consegue ver. A OX Security descreve a IA como "um exército de desenvolvedores juniores talentosos, mas inexperientes" — produtivos localmente, sem olhar para a arquitetura.
  • Refatorar é arriscado e não é recompensado. Reestruturar mexe em código que o modelo não foi solicitado a alterar e pode quebrar chamadores e testes. Um assistente bajulador, do tipo "só faça funcionar", minimiza o raio de impacto não mexendo no código que funciona — exatamente o comportamento que a OX mediu como "evitação de refatorações" (Avoidance of Refactors) em 80–90% do código gerado por IA: a IA "se destaca gerando código novo, mas nunca volta para melhorar a arquitetura".
  • Gerar é barato, apagar dá medo. Produzir 40 linhas novas não custa nada ao modelo; convencê-lo a apagar e remodelar 40 linhas existentes vai contra seu instinto de preservar.

Por que isso prejudica

  • A refatoração some do histórico. A análise de 2025 da GitClear sobre 211 milhões de linhas alteradas mostrou que as linhas associadas a refatoração ("movidas") caíram de 25% das linhas alteradas em 2021 para menos de 10% em 2024, enquanto as copiadas e coladas subiram de 8,3% para 12,3% e, em 2024, superaram as movidas pela primeira vez.
  • A complexidade se acumula. Cada flag pendurada dobra as combinações que a função precisa tratar e que os testes precisam cobrir; cada else if extra torna a próxima mudança mais arriscada — e o próximo prompt também adiciona em vez de reestruturar.
  • Acúmulo de dívida técnica. A refatoração é o retrabalho que mantém a entropia baixa; suprimi-la significa criar dívida que nunca é paga. A base de código cresce mais rápido do que melhora.
  • Carga de revisão. Os revisores recebem diffs que só acrescentam linhas a funções que já são longas; avaliar como um novo ramo interage com a dúzia acima dele é mais difícil do que revisar uma função remodelada para absorver a mudança.

##Tratamento

Táticas de prompt / revisão

  • Primeiro torne a mudança fácil. O conselho de Kent Beck — "para cada mudança desejada, torne a mudança fácil (aviso: isso pode ser difícil), depois faça a mudança fácil" — vira diretamente um prompt: "Antes de adicionar a regra do cupom, reestruture priceOrder para que a nova regra seja uma adição de uma linha; mantenha o comportamento idêntico e rode os testes. Depois adicione a funcionalidade."
  • Proíba novos parâmetros de flag. "Não adicione parâmetros booleanos ou de modo a funções existentes; se o comportamento precisar variar, proponha primeiro uma reestruturação."
  • Orce a complexidade, não só a correção. Exija que o assistente rode as regras complexity, max-params e max-lines-per-function do ESLint e a regra de complexidade cognitiva do Sonar, e informe os valores antes e depois para cada função que tocou; qualquer aumento precisa de justificativa.
  • Peça a refatoração como uma etapa separada. Gerar e reestruturar são tarefas diferentes: primeiro "faça funcionar", depois uma mudança separada que preserve o comportamento — "agora reestruture esta função para que os ramos virem dados ou funções separadas" —, com os testes verdes antes e depois.
  • Vigie os hotspots. Acompanhe frequência de mudanças × complexidade (Code Maat ou o próprio histórico do git) dos arquivos que crescem a cada commit escrito por IA e programe a reestruturação deles antes que a próxima funcionalidade chegue ali.

A refatoração em si — Decompor Condicional e Extrair Método dão nome a cada ramo; Substituir Parâmetro por Métodos Explícitos ou Introduzir Objeto de Parâmetro substituem os parâmetros de flag; e Substituir Condicional por Polimorfismo (ou uma tabela de consulta) assume quando os ramos escolhem o comportamento por tipo ou código.

// Depois: cada regra é uma função pequena; priceOrder só as compõe.
type PriceRule = (total: number) => number;

const subtotalOf = (items: Item[]) =>
  items.reduce((s, i) => s + i.price * i.qty, 0);

const VAT_RATE = 1.2;
const discount = (rate: number): PriceRule => (t) => t * (1 - rate);
const wholesale: PriceRule = (t) => (t > 1000 ? t * 0.9 : t > 500 ? t * 0.95 : t);
const COUPONS: Record<string, PriceRule> = {
  SPRING: (t) => t - 10,
  VIP: (t) => t * 0.85,
};
const vat: PriceRule = (t) => t * VAT_RATE;

function priceOrder(items: Item[], rules: PriceRule[]): number {
  return rules.reduce((total, rule) => rule(total), subtotalOf(items));
}

// Quem chama informa quais regras se aplicam, em ordem:
// priceOrder(items, [discount(0.1), wholesale, COUPONS.SPRING, vat]);

Uma nova regra de preço agora é mais uma entrada, não mais um parâmetro e um ramo, e cada regra pode ser testada isoladamente.

Não confunda com Duplicação em Vez de Extração: lá o modelo copia lógica existente para um novo bloco quase idêntico, e os detectores de clones o encontram. Aqui nada é copiado — o modelo continua fazendo a mesma função crescer com ramos, flags e camadas, e são as métricas de complexidade e tamanho que revelam isso.

##Detectado por

  • SonarQube / SonarSource S3776 — A complexidade cognitiva das funções não deve ser alta demais — sinaliza funções que continuam absorvendo novos ramos e flags
  • ESLint complexity — Limite de complexidade ciclomática por função (opção max)
  • ESLint max-lines-per-function — Limite de tamanho de função — pega funções que só crescem
  • ESLint max-params — Limite de número de parâmetros — pega flags e opções penduradas em uma assinatura existente
  • ESLint max-depth — Limite de profundidade de aninhamento — pega escadas de if/else empilhadas dentro de ramos antigos
  • Code Maat entity-churn — Frequência de mudanças por arquivo a partir do histórico do git — combinada com a complexidade, revela hotspots que crescem commit após commit