Refactorización de código legacy en JavaScript: nivel avanzado
Trabajar con código heredado (legacy) es una realidad inevitable para cualquier desarrollador, especialmente en el ámbito de Node.js. A menudo, este código es un "pantano" de variables globales, callbacks anidados, funciones de cientos de líneas y una completa falta de estructura modular. Los métodos simples de refactorización no funcionan aquí: se requiere un enfoque sistemático y el conocimiento de técnicas avanzadas. En este artículo, analizaremos estrategias que ayudarán a transformar el caos en una arquitectura mantenible sin detener la producción.
1. Análisis y mapeo de dependencias
Antes de cambiar el código, hay que entenderlo. En los proyectos legacy, la documentación a menudo falta o está desactualizada. El primer paso es construir un mapa de dependencias.
Herramientas:
- Madge — utilidad para visualizar dependencias entre módulos. Mostrará dependencias cíclicas y archivos "muertos".
- ESLint con plugins — ayudará a identificar variables, funciones e imports no utilizados.
- Despliegues en un entorno aislado — ejecute las pruebas (si no las hay, escriba al menos pruebas smoke) y registre qué módulos se cargan.
Ejemplo: Supongamos que tenemos un archivo legacy.js que contiene 2000 líneas y utiliza una variable global config.
// legacy.js (antes de la refactorización)var config = { apiUrl: 'https://api.example.com', timeout: 5000};
function fetchData(callback) { // 50 líneas de lógica con if y try/catch anidados // utiliza la variable global config if (config.timeout > 0) { // ... } callback(null, data);}Acción: Con Madge vemos que legacy.js se importa en otros 15 archivos. Es un "objeto divino". Nuestra tarea es dividirlo en módulos atómicos.
2. Estrategia "Strangler Fig" (Higuera estranguladora)
Este patrón es ideal para la refactorización sin detener el sistema. La esencia: no reescribimos todo de inmediato, sino que reemplazamos gradualmente partes del sistema antiguo por nuevas, hasta que el antiguo muera por sí solo.
Implementación en Node.js:
- Crear un nuevo módulo con una arquitectura limpia.
- Escribir una capa proxy (o fachada) que redirija las llamadas al módulo antiguo o al nuevo.
- Migrar gradualmente a los consumidores al nuevo módulo.
Ejemplo: Reemplazamos el antiguo estilo de callbacks por Promise/async/await.
// API antigua (basada en callbacks)function getUser(id, callback) { // código legacy callback(null, { id: id, name: 'John' });}
// API nueva (basada en Promise)async function getUserAsync(id) { // nueva implementación con async/await const user = await database.findUser(id); return user;}
// Fachada 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); }); }); }}La bandera USE_NEW_IMPLEMENTATION se puede externalizar en la configuración o en un feature toggle. Esto permite activar el nuevo código para una parte de los usuarios (canary release) o revertir los cambios sin necesidad de un despliegue.
3. Modularización y aislamiento de efectos secundarios
El código legacy a menudo sufre de efectos secundarios ocultos: una función modifica el estado global, escribe en un archivo o llama a una API externa sin devolver un resultado. La refactorización de estas funciones consiste en extraer la lógica pura.
Técnica: "Extraer función" (Extract Function) seguida de inyección de dependencias (Dependency Injection).
Código problemático: