Introducción: por qué el código legacy no es una sentencia
Cada desarrollador, tarde o temprano, se encuentra con código legacy en JavaScript. Pueden ser scripts monolíticos en Node.js escritos "a las apuradas" hace 5 años, o funciones callback enredadas que incluso los desarrolladores senior temen tocar. Sin embargo, la refactorización de código legacy no es simplemente "reescribir todo desde cero", sino un trabajo sistemático para mejorar la arquitectura sin cambiar el comportamiento externo.
En este artículo, analizaremos un plan paso a paso para la refactorización en JavaScript (Node.js): desde el análisis y las pruebas hasta la implementación de patrones modernos. Aprenderá cómo transformar de forma segura un "pantano" en código mantenible, utilizando ejemplos reales.
1. Análisis y mapeo del código legacy
Antes de cambiar el código, debe comprender su estado actual. Comience creando un mapa de dependencias e identificando los "puntos críticos".
1.1. Herramientas para el análisis
Para JavaScript/Node.js, utilice:
- ESLint con las reglas
complexityymax-lines— para identificar funciones complejas. - Plato o CodeClimate — para visualizar la complejidad ciclomática.
- Madge — para construir el gráfico de dependencias de los módulos.
1.2. Identificación de "olores de código"
Problemas típicos del JS legacy:
- Funciones de más de 100 líneas.
- Variables globales y mutación de estado.
- Callback hell (llamadas asíncronas anidadas).
- Falta de modularidad (un solo archivo de más de 2000 líneas).
// Ejemplo de "olor": la función lo hace todo a la vezfunction processUserData(userId) { // 100 líneas: validación, consulta a BD, formateo, envío de email // ... difícil de probar y mantener}2. Preparación de la "red de seguridad": pruebas
Sin pruebas, la refactorización es una "ruleta rusa". Cree Pruebas de Caracterización (pruebas que fijan el comportamiento actual).
2.1. Adición de pruebas unitarias
Utilice Jest o Mocha. Comience con las rutas críticas:
// Probamos la función existente sin cambiarlaconst { 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. Pruebas de integración para Node.js
Si el código trabaja con BD o API, utilice supertest para peticiones 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. Refactorización paso a paso: del monolito a los módulos
Dividamos el proceso en 3 etapas: aislamiento, descomposición, optimización.
3.1. Aislamiento y envoltorios (Wrapper)
No toque el interior — envuelva la función legacy en una nueva interfaz:
// Función legacyfunction oldFormatDate(date) { // ... 50 líneas con mutaciones return formattedDate;}
// Nuevo envoltoriofunction formatDateSafe(date) { if (!(date instanceof Date)) { throw new Error('Invalid date'); } return oldFormatDate(date);}3.2. Descomposición con Extract Function
Extraiga los bloques lógicos en funciones separadas:
// Antes: una función de 80 líneasfunction handleOrder(order) { // 1. Validación if (!order.id || !order.total) { ... } // 2. Cálculo de descuento