Refatoração de código legado em JavaScript: nível avançado
Trabalhar com código legado é uma realidade inevitável para qualquer desenvolvedor, especialmente na área de Node.js. Muitas vezes, esse código é um "pântano" de variáveis globais, callbacks aninhados, funções com centenas de linhas e total ausência de estrutura modular. Métodos simples de refatoração não funcionam aqui: é necessária uma abordagem sistemática e conhecimento de técnicas avançadas. Neste artigo, vamos abordar estratégias que ajudam a transformar o caos em uma arquitetura sustentável sem parar a produção.
1. Análise e mapeamento de dependências
Antes de alterar o código, é preciso entendê-lo. Em projetos legados, a documentação geralmente é inexistente ou desatualizada. O primeiro passo é construir um mapa de dependências.
Ferramentas:
- Madge — utilitário para visualizar dependências entre módulos. Ele mostra dependências cíclicas e arquivos "mortos".
- ESLint com plugins — ajuda a identificar variáveis, funções e imports não utilizados.
- Implantação em ambiente isolado — execute testes (se não houver, escreva pelo menos testes smoke) e registre quais módulos são carregados.
Exemplo: Suponha que temos um arquivo legacy.js com 2000 linhas que usa uma variável global config.
// legacy.js (antes da refatoração)var config = { apiUrl: 'https://api.example.com', timeout: 5000};
function fetchData(callback) { // 50 linhas de lógica com ifs aninhados e try/catch // usa o config global if (config.timeout > 0) { // ... } callback(null, data);}Ação: Com o Madge, vemos que legacy.js é importado em outros 15 arquivos. É um "objeto divino". Nossa tarefa é dividi-lo em módulos atômicos.
2. Estratégia "Strangler Fig" (Figo Estrangulador)
Esse padrão é ideal para refatoração sem parar o sistema. A ideia: não reescrevemos tudo de uma vez, mas substituímos gradualmente partes do sistema antigo por novas, até que o antigo morra por si só.
Implementação em Node.js:
- Crie um novo módulo com arquitetura limpa.
- Escreva uma camada de proxy (ou fachada) que redireciona chamadas para o módulo antigo ou novo.
- Gradualmente, migre os consumidores para o novo módulo.
Exemplo: Substituindo o estilo antigo de callback por Promise/async/await.
// API antiga (baseada em callback)function getUser(id, callback) { // código legado callback(null, { id: id, name: 'João' });}
// Nova API (baseada em Promise)async function getUserAsync(id) { // nova implementação com async/await const user = await database.findUser(id); return user;}
// Fachada de proxyasync function getUserFacade(id) { if (USE_NEW_IMPLEMENTATION) { return getUserAsync(id); } else { return new Promise((resolve, reject) => { getUser(id, (err, user) => { if (err) reject(err); else resolve(user); }); }); }}A flag USE_NEW_IMPLEMENTATION pode ser colocada na configuração ou em um feature toggle. Isso permite ativar o novo código para parte dos usuários (canary release) ou reverter alterações sem implantação.
3. Modularização e isolamento de efeitos colaterais
Código legado frequentemente sofre de efeitos colaterais ocultos: a função altera o estado global, escreve em um arquivo ou chama uma API externa sem retornar o resultado. A refatoração dessas funções envolve extrair a lógica pura.
Técnica: "Extrair Função" (Extract Function) seguida de injeção de dependências (Dependency Injection).
Código problemático: