Técnicas de Refatoração de Código Que Funcionam na Prática

Resumo

Refatoração de código é essencial para manter uma base de código limpa e fácil de estender. Este guia cobre os padrões que realmente importam na prática: extração de métodos, composição, branch-by-abstraction e simplificação de condicionais. Você aprenderá quando refatorar vale a pena e quando deixar o código como está.

Developer em home office escuro com dois monitores mostrando processo de refatoração de código

Técnicas de Refatoração de Código Que Funcionam na Prática

A maioria dos devs sabe que refatoração importa. O que menos fica claro é qual técnica escolher primeiro, até onde ir e que sinais indicam que uma base de código precisa de limpeza em vez de reescrita. Aqui está o que aprendi em seis meses trabalhando em um sistema legado de recomendações, e o que faria diferente na próxima vez.

Developer at dark home office with two monitors showing code refactoring process

Os sinais que indicam quando refatoração é necessária

Nem toda função que parece bagunçada vale a pena mexer. Três sinais que merecem atenção:

Duplicação em múltiplos lugares. Se você escreve a mesma transformação de dados em três controllers diferentes, isso não é questão de estilo. É uma bomba de manutenção. Uma mudança upstream significa três lugares para atualizar, e você vai esquecer um deles.

Uma mudança quebrando features não relacionadas. Se consertar um bug no fluxo de pagamento de alguma forma afeta como perfis de usuário renderizam, o acoplamento é muito forte. A base de código está tratando preocupações separadas como uma só.

Novos engenheiros não conseguem se orientar em menos de 30 minutos. Esse é um sinal subestimado. Se você não consegue explicar o que um arquivo faz sem ler tudo, ele provavelmente faz muitas coisas. Na minha experiência anterior, começamos a cronometrar walkthroughs de onboarding. Arquivos que levavam mais de 15 minutos para explicar entravam no backlog de refatoração.

Assimetria de mudanças no código. Rode git log --stat e veja quais arquivos são tocados com mais frequência. Se os mesmos 3 arquivos representam 60% dos seus commits, essa concentração é um sinal. Alta frequência de mudanças combinada com complexidade é o hotspot de 80/20 que vale atacar primeiro.

Pule a refatoração se: o código funciona, ninguém mexe nele e você não tem razão imediata para estender. A regra do escoteiro (deixar o código mais limpo do que encontrou) é um bom princípio, mas não limpe um acampamento que você não está usando.

Extração de Métodos: a técnica que você vai usar 80% do tempo

Extração de métodos é puxar um bloco significativo de lógica de uma função longa e dar um nome a ela. Esse nome é a documentação.

Antes:

def process_order(order):
    # validar
    if not order.get('user_id'):
        raise ValueError('Falta user_id')
    if order.get('amount', 0) <= 0:
        raise ValueError('Quantidade inválida')
    # aplicar desconto
    if order.get('coupon') == 'LAUNCH20':
        order['amount'] = order['amount'] * 0.8
    # salvar
    db.save(order)

Depois:

def process_order(order):
    validate_order(order)
    apply_discount(order)
    db.save(order)

def validate_order(order):
    if not order.get('user_id'):
        raise ValueError('Falta user_id')
    if order.get('amount', 0) <= 0:
        raise ValueError('Quantidade inválida')

def apply_discount(order):
    if order.get('coupon') == 'LAUNCH20':
        order['amount'] = order['amount'] * 0.8

A versão "depois" é mais longa. Tudo bem. Agora cada função tem uma única razão para existir, e quando a lógica de desconto mudar (e vai mudar), você sabe exatamente onde ir.

Vale a pena se: a função original tem mais de 20-25 linhas, ou se você se vê adicionando comentários como # validar para explicar o que um bloco faz. Esse comentário é um nome de função esperando para ser criado. Quando você escreve # aplicar desconto acima de um bloco, você já nomeou o método; só não o extraiu ainda.

Regra prática: se você sente vontade de adicionar um comentário explicando o que as próximas cinco linhas fazem, esse é um candidato para extração.

Composição de Métodos: quando extração não é o bastante

Composição vai além. Em vez de extrair um método, você decompõe uma classe ou módulo inteiro em componentes menores e focados. Isso é o que você precisa quando um arquivo cresceu para 600 linhas e serve a cinco propósitos não relacionados.

O padrão: identifique as responsabilidades distintas, dê a cada uma sua própria classe ou módulo, conecte-as em um nível superior. Martin Fowler chama isso de transformar uma classe inchada em um aglomerado de colaboradores.

Na prática, a parte difícil não é a mecânica. É descobrir onde uma responsabilidade termina e a outra começa. Um truque útil: se você consegue descrever o que uma função ou classe faz sem usar a palavra "e", ela provavelmente tem uma responsabilidade. Se precisa do "e", divida.

Outra forma de encontrar as costuras: olhe os imports no topo do arquivo. Se um único módulo importa de seis domínios não relacionados (camada de banco, serviço de email, logging, cliente de pagamento, reportes), está fazendo demais. Cada grupo de imports é um candidato para seu próprio módulo.

Composição de métodos também melhora a testabilidade. Uma classe de 400 linhas é difícil de testar unitariamente. Quatro classes de 100 linhas com interfaces claras são muito mais fáceis de mockar e isolar.

Developer at whiteboard planning code architecture refactoring diagram

Branch-by-Abstraction: para refatorar mantendo a produção ativa

Essa é mais arquitetônica, mas é a técnica que torna refatoração em grande escala viável em ambientes de produção.

A ideia: em vez de fazer uma troca tudo-de-uma-vez de uma implementação para outra, você introduz uma camada de abstração (geralmente uma interface ou protocolo) ao redor do código que quer mudar. A implementação antiga e a nova satisfazem a interface. Você pode colocar a nova implementação atrás de um feature flag, rodar ambas em paralelo e aposentar a antiga quando a nova estiver estável.

Por que importa para projetos paralelos: se você está reconstruindo uma integração de pagamento ou migrando um ORM, branch-by-abstraction significa que consegue manter a versão atual funcionando em produção enquanto a nova matura. Sem o "branch de refatoração" que fica desincronizado do main por três semanas.

O custo: mais trabalho no início para definir uma interface limpa. Vale a pena quando a coisa que você está substituindo é arriscada (autenticação, cobrança, acesso a dados) e você não consegue arcar com uma janela de downtime completa.

Um exemplo concreto: tínhamos um módulo legado de rastreamento de eventos que escrevia direto em uma tabela PostgreSQL. Migrar para um pipeline baseado em Kafka significava tocar em 40+ call sites. Com branch-by-abstraction, introduzimos uma interface EventTracker, trocamos a implementação atrás de um flag, rodamos ambos os destinos em paralelo por duas semanas e depois deletamos o antigo. Zero downtime, sem deploys de emergência.

Simplificação de Condicionais: a limpeza mais subestimada

Lógica condicional é onde complexidade se esconde. Uma função com cinco if aninhados é mais difícil de testar, ler e estender do que cinco condições separadas expressas como guard clauses ou polimorfismo.

Substitua condicionais aninhados com guard clauses:

Antes:

function getDiscount(user) {
  if (user) {
    if (user.isActive) {
      if (user.plan === 'pro') {
        return 0.20;
      } else {
        return 0.05;
      }
    } else {
      return 0;
    }
  } else {
    return 0;
  }
}

Depois:

function getDiscount(user) {
  if (!user) return 0;
  if (!user.isActive) return 0;
  if (user.plan === 'pro') return 0.20;
  return 0.05;
}

Mesma lógica. Muito mais fácil de escanear. Os retornos antecipados eliminam a pirâmide de indentação e deixam você ler cada condição isoladamente.

Substitua condicionais com polimorfismo quando você tem a mesma verificação if type === X repetida em múltiplas funções. Defina uma classe base ou protocolo, uma implementação por tipo, e deixe o dispatch acontecer no call site. Pule se: você tem apenas dois tipos e adicionar um terceiro é improvável. O overhead de abstração não vale para uma escolha binária que nunca vai crescer.

Consolide condicionais duplicadas. Se você tem a mesma condição verificada em três lugares diferentes antes de fazer coisas ligeiramente diferentes, extraia a condição em uma função predicado nomeada. if (isEligibleForDiscount(user)) fica melhor do que repetir a verificação de três partes cada vez.

Two developers doing pair programming and code review session at shared desk

Movendo funcionalidades entre objetos: a técnica subutilizada

Às vezes um método está na classe errada. Não porque o código está errado, mas porque ele opera mais em dados de outra classe. Esse é um sinal para movê-lo.

O padrão de Mover Método é simples: identifique o método, veja qual classe ele realmente depende mais, mova para lá e atualize todos os chamadores. Seu IDE vai pegar as referências quebradas.

Onde isso aparece mais em projetos paralelos: classes utilitárias que acumulam tudo que não obviamente pertence em outro lugar. Arquivos helpers.js ou utils.py que crescem para 400 linhas. Em algum ponto, essas funções pertencem aos objetos de domínio que estão operando. Uma função que recebe um objeto User e faz cinco coisas com ele pertence em User, não em utils.

Pule se está no início: se o projeto tem duas semanas e ainda está encontrando sua forma, não sobre-engenharie a estrutura. Deixe o design emergir do uso real antes de investir em um modelo de domínio limpo. Mover coisas muito cedo significa movê-las de novo quando os requisitos mudarem.

O que fazer antes de refatorar: a linha de base de testes

Refatorar sem testes é só jogo de azar. Você acha que está limpando; talvez esteja quebrando.

A rede de segurança mínima viável: testes de caracterização. Escreva testes que capturem o comportamento atual do código que vai mudar. Não o que ele deveria fazer, mas o que ele realmente faz agora. Esses testes existem para pegar regressões, não para validar o design.

Uma vez com a linha de base, refatore em commits pequenos. Um padrão por commit. Se algo quebrar, você sabe exatamente qual mudança causou. Compare com a alternativa: uma sessão de refatoração de três dias commitada como um único diff gigante. Quando o CI falha às 2 da manhã, você não tem ideia de qual das 40 mudanças quebrou.

Checklist prático antes de qualquer passe de refatoração:

Restrição útil: se você não consegue explicar a refatoração em uma única frase na mensagem de commit, provavelmente está fazendo muito de uma vez.

Organizando dados: o padrão que melhora tudo mais

Muita lógica condicional bagunçada existe porque o modelo de dados está errado. Quando você passa primitivos brutos em volta em vez de objetos de domínio, acaba com condicionais de verificação de tipo em todo lugar.

Substituir um primitivo com um objeto (string email vira EmailAddress, int cents vira Money) te dá um lugar para a lógica de validação. Em vez de verificar if not re.match(EMAIL_REGEX, email) em 12 lugares, o construtor EmailAddress faz uma vez.

Esse é o padrão "Replace Type Code with Class" do livro Refactoring de Fowler. Não é glamoroso, mas reduz espalhamento de condicionais melhor que quase tudo mais no livro.

Mesmo princípio vale para números mágicos: if status == 3 é ilegível. if status == OrderStatus.SHIPPED não é. Substituir números mágicos com constantes nomeadas leva cinco minutos e se paga cada vez que alguém lê o código.

Quando NÃO refatorar: os casos que as pessoas pulam

Aqui é onde a maioria dos guias para. Voilà ce qui coince en pratique: nem toda base de código vale refatoração, e tratar limpeza como sempre-boa é simplesmente débito técnico de outro tipo.

Não refatore pouco antes de um deadline importante. Refatoração introduz mudança. Mudança introduz risco. Se você tem uma demo para cliente em 48 horas, não é hora.

Não refatore código que você vai deletar. Se uma feature está sendo cortada no próximo sprint, limpar sua implementação é trabalho perdido. Confirme deletação antes de investir em limpeza.

Não refatore só por estilo. Se o código funciona, tem testes e ninguém mexe nele, o fato de que você teria escrito diferente não é justificativa para mudar. O risco de regressão é real; o benefício é estético.

Não refatore sem um alvo claro. "Limpe isso" não é uma tarefa. "Extraia a lógica de validação de OrderProcessor em uma classe Validator dedicada" é uma tarefa. Escopo vago leva a scope creep, que leva a um branch que vive por três semanas e faz merge com conflitos.

Isso não é um conjunto perfeito de regras. É um conjunto viável, e em uma base de código de produção, viável supera perfeito.

O que seis meses de refatoração incremental realmente parece

Um time que refatora incrementalmente, um pouco a cada sprint, alinhado com as features que está shippando, acaba com uma base de código mais barata de trabalhar. Não mais rápida para escrever inicialmente, mas mais rápida para estender, debugar e onboardar novos contribuidores.

Para builders solo: o ganho é pessoal. Você é o novo engenheiro daqui a seis meses. O código que você limpou hoje é o código que você entenderá sem re-ler em janeiro.

Um quadro de seis meses do projeto de sistema de recomendações: começamos com um arquivo de serviço de 1.200 linhas sem testes. Extrai métodos durante cada sprint de feature, adicionei testes de caracterização conforme toquei cada seção e movi coisas para as classes certas conforme entendia melhor o domínio. Depois de seis meses, a mesma lógica vivia em nove arquivos com média de 130 linhas cada, todos testados, todos nomeados pelo que fazem. Frequência de deployment foi de semanal para diária. Tempo médio de recuperação de um bug caiu em aproximadamente 40%.

Esses números não são de um estudo; são de um time, um sistema. Mas a direção se sustenta. Unidades menores e mais limpas são mais rápidas de mudar. Essa é a aposta inteira.

Perguntas frequentes

O que é refatoração de código e por que importa?
Refatoração de código é o processo de reestruturar código existente sem mudar seu comportamento externo. Importa porque torna a base de código mais fácil de entender, estender e debugar, reduzindo diretamente o custo de desenvolvimento futuro e correção de bugs.
O que é a técnica Extract Method?
Extract Method puxa um bloco significativo de lógica de uma função longa e dá um nome a ele. O nome funciona como documentação inline. É a técnica de refatoração mais usada e frequentemente o primeiro movimento certo quando uma função ultrapassa 20-25 linhas.
Quando você NÃO deveria refatorar código?
Pule refatoração pouco antes de deadlines importantes, em código que está prestes a ser deletado e em código funcionando que ninguém mexe e não precisa ser estendido. Refatoração introduz mudança e risco, então só vale quando há um benefício claro e específico.
O que é branch-by-abstraction em refatoração?
Branch-by-abstraction introduz uma camada de interface ao redor do código que você quer substituir, permitindo implementações antiga e nova coexistirem. Você pode colocar a versão nova atrás de um feature flag e aposentar a velha quando estiver estável, evitando uma troca arriscada tudo-de-uma-vez.
Como guard clauses simplificam lógica condicional?
Guard clauses substituem blocos if-else aninhados com retornos antecipados no topo de uma função. Cada condição sai imediatamente se não for satisfeita, eliminando a pirâmide de indentação e tornando o caminho feliz fácil de ler à primeira vista.
Preciso de testes antes de refatorar?
Sim. Testes de caracterização capturam o comportamento atual do código que você vai mudar. Sem eles, refatoração é adivinhação: você pode quebrar algo e não saber até produção. Escreva os testes primeiro, depois refatore em commits pequenos e de um padrão por commit.
O que é a técnica Move Method?
Move Method relocate uma função para a classe que ela realmente depende mais. É usada quando um método usa mais dados de outra classe do que da sua, o que sinaliza que está no lugar errado. A maioria das IDEs lidam com a mudança mecânica e atualizações de referência automaticamente.