Introdução: por que código legado não é uma sentença
Todo desenvolvedor, mais cedo ou mais tarde, enfrenta código legado em JavaScript. Podem ser scripts monolíticos em Node.js, escritos "de qualquer jeito" há 5 anos, ou funções callback confusas que até desenvolvedores seniores têm medo de tocar. No entanto, a refatoração de código legado não é simplesmente "reescrever tudo do zero", mas um trabalho sistemático para melhorar a arquitetura sem alterar o comportamento externo.
Neste artigo, vamos detalhar um plano passo a passo de refatoração em JavaScript (Node.js): desde a análise e testes até a implementação de padrões modernos. Você aprenderá como transformar com segurança um "pântano" em código sustentável, usando exemplos reais.
1. Análise e mapeamento do código legado
Antes de alterar o código, é preciso entender seu estado atual. Comece criando um mapa de dependências e identificando "pontos críticos".
1.1. Ferramentas para análise
Para JavaScript/Node.js, utilize:
- ESLint com as regras
complexityemax-lines— para identificar funções complexas. - Plato ou CodeClimate — para visualizar a complexidade ciclomática.
- Madge — para construir o grafo de dependências dos módulos.
1.2. Identificação de "code smells"
Problemas típicos de JS legado:
- Funções com mais de 100 linhas.
- Variáveis globais e mutação de estado.
- Callback hell (chamadas assíncronas aninhadas).
- Falta de modularidade (um arquivo com mais de 2000 linhas).
// Exemplo de "code smell": função faz tudo de uma vez
function processUserData(userId) {
// 100 linhas: validação, consulta ao BD, formatação, envio de email
// ... difícil de testar e manter
}
2. Preparação da "rede de segurança": testes
Sem testes, refatoração é "roleta-russa". Crie Characterization Tests (testes que fixam o comportamento atual).
2.1. Adicionando testes unitários
Use Jest ou Mocha. Comece pelos caminhos críticos:
// Testando a função existente sem alterá-la
const { processUserData } = require('./legacy');
describe('processUserData (legacy)', () => {
it('should process user with ID 1', () => {
const result = processUserData(1);
expect(result).toHaveProperty('email');
expect(result.status).toBe('processed');
});
});
2.2. Testes de integração para Node.js
Se o código trabalha com BD ou API, use supertest para requisições HTTP:
const request = require('supertest');
const app = require('../app');
describe('POST /api/users', () => {
it('should create user (legacy behavior)', async () => {
const res = await request(app)
.post('/api/users')
.send({ name: 'Test' });
expect(res.status).toBe(200);
expect(res.body.id).toBeDefined();
});
});
3. Refatoração passo a passo: do monolito aos módulos
Vamos dividir o processo em 3 etapas: isolamento, decomposição, otimização.
3.1. Isolamento e wrappers
Não mexa no interior — envolva a função legada em uma nova interface:
// Função legada
function oldFormatDate(date) {
// ... 50 linhas com mutações
return formattedDate;
}
// Novo wrapper
function formatDateSafe(date) {
if (!(date instanceof Date)) {
throw new Error('Invalid date');
}
return oldFormatDate(date);
}
3.2. Decomposição com Extract Function
Separe blocos lógicos em funções individuais:
// Antes: uma função de 80 linhas
function handleOrder(order) {
// 1. Validação
if (!order.id || !order.total) { ... }
// 2. Cálculo de desconto
```