close

DEV Community

Cover image for Legacy code não envelhece como vinho: quanto mais espera, pior fica
Taina Costa
Taina Costa

Posted on

Legacy code não envelhece como vinho: quanto mais espera, pior fica

Semana passada eu passei três horas debugando um bug que deveria levar 20 minutos. O problema? Um módulo de validação escrito em 2019 que ninguém mexe "porque funciona". Spoiler: não funcionava mais, e quando finalmente abri o arquivo, encontrei um // TODO: refactor this datado de 2020.

Por que legacy vira bola de neve

A indústria trata código legado como se fosse dívida técnica opcional — algo que você paga "quando tiver tempo". Mas código legado se comporta mais como mofo: se espalha, contamina áreas adjacentes, e quanto mais você ignora, mais cara fica a limpeza.

O ciclo é previsível: você herda um projeto ou feature antiga, vê que está "meio bagunçado mas roda", adiciona sua feature com um if a mais, e segue em frente. Seis meses depois, outra pessoa faz o mesmo. Um ano depois, aquele arquivo tem 800 linhas, cinco níveis de if aninhados, e zero testes. Ninguém mais entende o fluxo completo, então cada mudança vira uma sessão de especulação: "se eu mexer aqui, quebra ali?"

O custo real de esperar

Esse código "que funciona" tem um custo oculto que aparece em três formas:

Velocidade de desenvolvimento despenca. Features que deveriam levar dois dias levam uma semana porque você passa mais tempo entendendo o contexto do que escrevendo código novo.

Bugs aumentam exponencialmente. Código sem testes e com lógica embolada é um gerador de regressões. Você corrige um edge case e quebra outro que nem sabia que existava.

Onboarding vira tortura. Novo dev no time? Boa sorte explicando por que aquele service tem três formas diferentes de fazer autenticação, ou por que a mesma validação está copiada em sete lugares.

Sinais de que você está sentado em cima de uma bomba

Nem todo código antigo é legacy tóxico. Aqui estão os red flags que indicam que você precisa agir agora:

// Red flag #1: comentários mentirosos ou inúteis
function processPayment(order) {
  // Process the payment
  const user = order.user;  // TODO: fix this later

  // HACK: don't touch this, breaks prod
  if (user.country === 'BR') {
    // ...
  }
}

// Red flag #2: lógica de negócio duplicada
// arquivo A
const validCPF = cpf => cpf.length === 11 && /^\d+$/.test(cpf);

// arquivo B (outro módulo)
const isCPFValid = doc => doc.replace(/\D/g, '').length === 11;

// arquivo C (mais outro)
function checkCPF(value) {
  return value.match(/\d{11}/) !== null;
}

// Red flag #3: dependências circulares e acoplamento absurdo
import { UserService } from './user';
import { OrderService } from './order';
// OrderService importa UserService
// UserService importa OrderService
// Ambos importam shared state global
Enter fullscreen mode Exit fullscreen mode

Se você leu esse código e pensou "isso parece familiar", você já está pagando juros.

Estratégia incremental: você não precisa reescrever tudo

A boa notícia: você não precisa parar o mundo para arrumar legacy. Reescritas completas falham 80% das vezes porque você gasta seis meses reconstruindo funcionalidade existente enquanto o negócio continua pedindo features novas.

A abordagem que funciona é refactoring incremental com testes de caracterização. A ideia é:

  1. Capture o comportamento atual (mesmo que estranho) em testes
  2. Refatore sem mudar comportamento
  3. Repita até o código ficar legível
  4. Só então mude comportamento, com testes reais

Exemplo prático. Você tem esse monstro:

function calculateTotal(items, user, coupon) {
  let total = 0;
  for (let i = 0; i < items.length; i++) {
    if (items[i].category === 'book') {
      total += items[i].price * 0.9;
    } else {
      total += items[i].price;
    }
  }
  if (user.isPremium) {
    total = total * 0.95;
  }
  if (coupon && coupon.valid) {
    total = total - coupon.value;
  }
  return total;
}
Enter fullscreen mode Exit fullscreen mode

Primeiro, você captura o comportamento atual com testes de caracterização:

describe('calculateTotal (caracterização)', () => {
  it('aplica 10% desconto em livros', () => {
    const items = [{ category: 'book', price: 100 }];
    expect(calculateTotal(items, {}, null)).toBe(90);
  });

  it('aplica desconto premium antes de coupon', () => {
    const items = [{ category: 'other', price: 100 }];
    const user = { isPremium: true };
    const coupon = { valid: true, value: 10 };
    // 100 * 0.95 - 10 = 85 (não 90 * 0.95)
    expect(calculateTotal(items, user, coupon)).toBe(85);
  });
});
Enter fullscreen mode Exit fullscreen mode

Esses testes documentam bugs e edge cases que você nem sabia que existiam (tipo a ordem de aplicação dos descontos). Agora você pode refatorar com segurança:

const applyBookDiscount = price => price * 0.9;
const applyPremiumDiscount = total => total * 0.95;
const applyCoupon = (total, coupon) => 
  coupon?.valid ? total - coupon.value : total;

function calculateTotal(items, user, coupon) {
  const subtotal = items.reduce((sum, item) => {
    const price = item.category === 'book' 
      ? applyBookDiscount(item.price)
      : item.price;
    return sum + price;
  }, 0);

  let total = user.isPremium 
    ? applyPremiumDiscount(subtotal)
    : subtotal;

  return applyCoupon(total, coupon);
}
Enter fullscreen mode Exit fullscreen mode

Os testes continuam passando, mas agora o código é legível e as funções são reutilizáveis. Você pode até descobrir bugs (tipo aquela ordem de desconto esquisita) e decidir se corrige agora ou documenta como comportamento legado intencional.

Caveats: quando NÃO mexer

Refatorar legacy tem armadilhas:

Não refatore sem testes. Se você não consegue escrever testes de caracterização porque o código é intratável (muitos side effects, dependências globais), primeiro isole a lógica. Extraia funções puras antes de tentar refatorar a orquestração.

Cuidado com "melhorias" que mudam comportamento. Aquele bug pode ser uma feature não documentada que algum cliente depende. Se não tem teste cobrindo, não mude.

Legacy que ninguém toca pode ficar quieto. Se é um módulo que não mudou em três anos e não causa problemas, deixa lá. Foque em código que você precisa mexer frequentemente.

TL;DR

  • Código legado piora exponencialmente: cada mudança sem refactor aumenta complexidade e risco
  • Red flags: comentários inúteis, lógica duplicada, dependências circulares, zero testes
  • Não reescreva tudo: use refactoring incremental com testes de caracterização
  • Capture comportamento atual primeiro, refatore sem mudar semântica, depois melhore
  • Não mexa em código que está quieto e não incomoda
  • Quanto mais espera, mais caro fica — escolha suas batalhas, mas não ignore todas

Top comments (0)