Trabalhando com legado: estratégias de manutenção
1. Diagnóstico inicial: entendendo o legado antes de tocar no código
Antes de qualquer intervenção em um sistema legado, é fundamental realizar um diagnóstico preciso. O primeiro passo é o mapeamento de dependências e dívida técnica. Ferramentas como ferramentas de análise estática e visualizadores de dependências ajudam a identificar o grau de acoplamento entre módulos. Métricas como acoplamento aferente (Ca) e eferente (Ce), profundidade de herança (DIT) e falta de coesão (LCOM) são indicadores objetivos.
A análise de cobertura de testes é outro pilar. Código legado frequentemente possui zonas protegidas por testes e "terras de ninguém" onde qualquer alteração pode quebrar funcionalidades críticas. Ferramentas de cobertura mostram exatamente onde os testes faltam. Por exemplo, um relatório pode indicar:
Cobertura de linhas: 34%
Cobertura de branches: 21%
Módulos críticos sem cobertura: módulo-de-pagamento, validador-de-regras-fiscais
Identificar padrões comuns em código legado é igualmente essencial. God classes (classes que fazem tudo), copy-paste generalizado e acoplamento excessivo são sinais de alerta. Um exemplo de god class pode ser identificado por métricas como número de métodos (>30) e linhas por classe (>500).
2. Estratégias de intervenção cirúrgica: o mínimo de risco, o máximo de valor
A técnica do "Strangler Fig" (figueira estranguladora) é uma das mais eficazes para substituir funcionalidades gradualmente. Em vez de reescrever todo o sistema, cria-se uma nova implementação ao lado da antiga, redirecionando chamadas aos poucos. Um exemplo prático de roteamento:
# Configuração de proxy reverso (exemplo conceitual)
if (funcionalidade_antiga && !migrada) {
encaminhar para servico_legado;
} else {
encaminhar para novo_servico;
}
Refatoração em passos de bebê significa fazer mudanças atômicas e reversíveis. Cada commit deve representar uma transformação mínima que não quebre a funcionalidade. Por exemplo, renomear uma variável ou extrair um método são passos seguros. O uso de feature flags isola alterações em produção, permitindo ativar ou desativar funcionalidades sem deploy:
if (feature_flags.esta_ativa("nova_regra_tributaria")) {
aplicar_nova_regra();
} else {
aplicar_regra_legado();
}
3. Construindo uma rede de segurança: testes como aliados
Testes de caracterização são essenciais para congelar o comportamento existente quando não há documentação. Eles capturam a saída atual do sistema para entradas conhecidas, sem julgar se o comportamento está "certo". O processo é simples:
- Executar o sistema com entradas reais
- Registrar a saída
- Criar um teste que compare futuras execuções com essa saída
Para adicionar testes em código não testável, a inversão de dependências e o uso de seams (costuras) são cruciais. Um seam é um ponto onde você pode alterar o comportamento sem modificar o código diretamente. Por exemplo, substituir uma chamada a um banco de dados por um mock:
// Código original (não testável)
function calcularTotal(pedido) {
return pedido.itens * banco.buscarTaxa(pedido.categoria);
}
// Com seam (injeção de dependência)
function calcularTotal(pedido, obterTaxa) {
return pedido.itens * obterTaxa(pedido.categoria);
}
Integração contínua com legado exige cuidado para evitar quebras silenciosas. Pipelines devem executar testes de caracterização e integração em ambientes isolados, com notificações claras para falhas.
4. Documentação viva: registrando o que o código não conta
Decisões de negócio enterradas em código legado precisam ser extraídas e documentadas. Regras como "se o valor for maior que 1000, aplicar desconto de 5%" muitas vezes têm origens obscuras. Entrevistas com stakeholders e análise de logs podem revelar o "porquê".
ADRs (Architecture Decision Records) são uma ferramenta poderosa para registrar decisões arquiteturais durante a manutenção de legado. Um ADR típico inclui contexto, decisão, consequências e alternativas consideradas. Exemplo:
# ADR-023: Substituir módulo de relatórios legado
## Contexto
O módulo de relatórios atual usa uma biblioteca descontinuada.
## Decisão
Migrar para a biblioteca X, mantendo compatibilidade via adaptador.
## Consequências
+ Manutenibilidade futura
- Esforço inicial de 2 semanas
Comentários que ajudam explicam o "porquê" de escolhas não óbvias. Comentários que poluem repetem o que o código já diz ou estão desatualizados. A regra é: documente a intenção, não a implementação.
5. Gestão de mudanças em times: comunicação e processo
Negociar prioridades entre features novas e manutenção de legado requer dados objetivos. Apresente métricas como tempo médio para corrigir bugs no legado versus em código novo, ou o custo de oportunidade de não refatorar. Use a "regra do escoteiro": deixe o código mais limpo do que encontrou, mesmo em pequenas alterações.
Code review para código legado deve focar em segurança e legibilidade. Pergunte: "Essa mudança quebra algo existente?" e "O código ficou mais claro?". Evite discussões estéticas; priorize a consistência com o estilo já existente.
Onboarding de novos membros em sistemas legados exige um roteiro claro: comece por áreas com boa cobertura de testes, forneça um mapa das dependências e incentive perguntas sobre decisões históricas.
6. Ferramentas e automatizações que salvam o dia
Linters e formatters como ESLint, Prettier ou ferramentas específicas de linguagem impõem consistência mesmo em código antigo. Configure-os para rodar automaticamente em commits ou pipelines.
Análise estática com ferramentas como SonarQube detecta code smells específicos de legado: complexidade ciclomática alta, duplicação de código, métodos muito longos. Crie um perfil de qualidade com regras prioritárias para o sistema legado.
Scripts de migração automatizada transformam repetição em automação segura. Por exemplo, um script para substituir chamadas de API antigas por novas:
# Script de migração (exemplo conceitual)
para cada arquivo em src/:
substituir("api_antiga.buscarCliente", "api_nova.obterCliente")
substituir("api_antiga.salvarPedido", "api_nova.registrarPedido")
executar testes de caracterização
7. Quando o legado precisa morrer: critérios para reescrita ou aposentadoria
O custo de manutenção versus custo de reescrita deve ser analisado objetivamente. Calcule o custo acumulado de manutenção nos últimos 12 meses (horas de desenvolvedor, bugs críticos, downtime) e compare com o custo estimado de reescrita (incluindo migração de dados, testes, treinamento). Use a fórmula:
Custo_manutencao_anual > (Custo_reescrita * 2) → considerar reescrita
Estratégias de descomissionamento (sunsetting) devem respeitar o usuário. Comunique com antecedência, ofereça alternativas, migre dados gradualmente e mantenha um período de coexistência.
O mito da reescrita total (big bang) é perigoso. Reescrever tudo de uma vez raramente funciona, pois o conhecimento de negócio embutido no legado é perdido. Prefira substituições incrementais, mesmo que levem mais tempo. A convivência controlada com o legado é quase sempre melhor que uma reescrita arriscada.
Referências
- Working Effectively with Legacy Code (Michael C. Feathers) — Livro clássico sobre estratégias para lidar com código legado, incluindo testes de caracterização e seams.
- Martin Fowler: Strangler Fig Application — Artigo original sobre a técnica de substituição gradual de sistemas legados.
- SonarQube Documentation: Analyzing Legacy Code — Guia oficial sobre como usar análise estática para detectar code smells em sistemas legados.
- ADR (Architecture Decision Records) by Michael Nygard — Artigo seminal sobre o uso de ADRs para documentar decisões arquiteturais.
- Feature Flags: A Practical Guide (LaunchDarkly) — Guia prático sobre implementação e gerenciamento de feature flags para isolar mudanças em produção.
- Refactoring Guru: Code Smells — Catálogo detalhado de code smells com exemplos e sugestões de refatoração, útil para diagnóstico de legado.
- Continuous Integration for Legacy Systems (ThoughtWorks) — Artigo técnico sobre como adaptar pipelines de CI para sistemas legados sem introduzir riscos.