Refactorización Inteligente con IA
Mejora la calidad de tu código con inteligencia artificial
¿Qué es la Refactorización?
Refactorizar es mejorar la estructura interna del código sin cambiar su comportamiento externo. La IA puede identificar code smells, sugerir patrones de diseño y aplicar principios SOLID automáticamente.
Principios que la IA Puede Aplicar
SOLID
- Single Responsibility - Una clase, una responsabilidad
- Open/Closed - Abierto para extensión, cerrado para modificación
- Liskov Substitution - Subtipos reemplazables
- Interface Segregation - Interfaces específicas
- Dependency Inversion - Depender de abstracciones
Prompts para Refactorización
Refactorización general
"Refactoriza este código siguiendo los principios SOLID y clean code:
[código]
Mantén la misma funcionalidad pero mejora:
- Legibilidad
- Mantenibilidad
- Testabilidad
- Separación de responsabilidades
Explica cada cambio que hagas."
Extraer funciones
"Esta función es demasiado larga (150 líneas). Extráela en funciones más pequeñas con nombres descriptivos:
[código]
Cada función debe tener una sola responsabilidad y máximo 20 líneas."
Aplicar patrones de diseño
"Este código tiene mucho if/else anidado. ¿Qué patrón de diseño puedo aplicar para mejorarlo?
[código]
Implementa el patrón sugerido y explica por qué es mejor."
Ejemplos Prácticos
Antes: Código acoplado
class UserService {
async register(user) {
// Validar
if (!user.email) throw new Error('Email required');
if (!user.password) throw new Error('Password required');
// Guardar en DB
const db = new Database();
await db.save(user);
// Enviar email
const mailer = new Mailer();
await mailer.send(user.email, 'Welcome!');
// Log
console.log(`User ${user.email} registered`);
}
}
Después: Refactorizado con IA
class UserValidator {
validate(user) {
if (!user.email) throw new Error('Email required');
if (!user.password) throw new Error('Password required');
}
}
class UserService {
constructor(validator, repository, notifier, logger) {
this.validator = validator;
this.repository = repository;
this.notifier = notifier;
this.logger = logger;
}
async register(user) {
this.validator.validate(user);
await this.repository.save(user);
await this.notifier.sendWelcome(user.email);
this.logger.log(`User ${user.email} registered`);
}
}
Code Smells que la IA Detecta
- Funciones largas: Más de 30 líneas
- Duplicación: Código repetido
- Nombres poco claros: Variables como x, data, temp
- Demasiados parámetros: Más de 3-4 argumentos
- Clases Dios: Una clase que hace todo
- Acoplamiento: Dependencias hardcodeadas
- Magic numbers: Números sin contexto
Workflow de Refactorización
- Asegúrate de tener tests que pasen
- Pide a la IA que analice el código
- Revisa las sugerencias una por una
- Aplica cambios en commits pequeños
- Ejecuta tests después de cada cambio
- Commit con mensaje descriptivo
Patrones de Diseño Comunes
Los patrones de diseño son soluciones probadas a problemas recurrentes en el desarrollo de software. La IA puede identificar cuándo tu código se beneficiaría de aplicar un patrón específico y generar la implementación correcta. Aquí te mostramos los más útiles en el desarrollo moderno.
Strategy Pattern
El patrón Strategy es ideal cuando tienes múltiples algoritmos intercambiables y quieres evitar cadenas de if/else o switch enormes. Permite seleccionar el algoritmo en tiempo de ejecución sin modificar el código cliente.
// Antes: if/else chain difícil de mantener
function calculateShipping(method, weight) {
if (method === 'standard') return weight * 0.5;
if (method === 'express') return weight * 0.5 + 15;
if (method === 'overnight') return weight * 0.5 + 30;
if (method === 'drone') return weight * 0.3 + 50;
throw new Error('Unknown method');
}
// Después: Strategy Pattern
const shippingStrategies = {
standard: (weight) => weight * 0.5,
express: (weight) => weight * 0.5 + 15,
overnight: (weight) => weight * 0.5 + 30,
drone: (weight) => weight * 0.3 + 50,
};
function calculateShipping(method, weight) {
const strategy = shippingStrategies[method];
if (!strategy) throw new Error(`Unknown method: ${method}`);
return strategy(weight);
}
// Prompt para la IA:
"Este código tiene un switch con 12 casos. Refactorízalo usando
el patrón Strategy. Cada estrategia debe ser una clase separada
que implemente una interfaz común."
Observer Pattern
El patrón Observer permite que un objeto notifique a otros cuando su estado cambia, sin que los observadores necesiten conocer los detalles internos del sujeto. Es la base de los sistemas de eventos en JavaScript.
// Prompt para la IA:
"Tengo un sistema donde cuando un usuario se registra, necesito:
1. Enviar email de bienvenida
2. Crear perfil en analytics
3. Notificar al equipo de ventas por Slack
4. Añadir a la lista de newsletter
Actualmente todo está en una función gigante. Refactorízalo
usando el patrón Observer/EventEmitter para que cada acción
sea independiente y se pueda añadir/quitar sin modificar
el código de registro."
Factory Pattern
El patrón Factory centraliza la creación de objetos, permitiendo que el código cliente no necesite conocer las clases concretas que se instancian. Es especialmente útil cuando la creación de objetos es compleja o depende de condiciones externas.
// Prompt para la IA:
"Tengo este código que crea diferentes tipos de reportes
según el formato solicitado:
[código con new PdfReport(), new ExcelReport(), new CsvReport()]
Refactorízalo usando el patrón Factory. La factory debe:
- Aceptar el tipo de reporte como string
- Retornar la instancia correcta
- Soportar registro de nuevos tipos en runtime
- Lanzar un error descriptivo si el tipo no existe
- Incluir una interfaz Report que todos implementen"
Métricas de Calidad de Código
Las métricas de código te proporcionan datos objetivos sobre la calidad de tu software. En lugar de depender solo de la intuición, puedes usar estas métricas para identificar qué partes del código necesitan refactorización prioritaria y medir el progreso de tus mejoras.
Complejidad Ciclomática
La complejidad ciclomática mide el número de caminos independientes a través de tu código. Cada if, else, switch case, for, while o catch añade un camino. Una complejidad alta indica que la función es difícil de testear y mantener.
// Complejidad ciclomática: 8 (demasiado alta)
function processOrder(order) {
if (!order) return null; // +1
if (order.status === 'cancelled') return; // +1
for (const item of order.items) { // +1
if (item.stock < item.qty) { // +1
if (item.backorder) { // +1
notifyBackorder(item);
} else {
throw new Error('Out of stock');
}
}
if (item.discount) { // +1
applyDiscount(item);
}
}
if (order.total > 1000) { // +1
flagForReview(order);
}
return finalize(order); // +1 (base)
}
// Prompt para la IA:
"Esta función tiene complejidad ciclomática de 8.
Redúcela a máximo 3 extrayendo funciones.
Cada función extraída debe tener complejidad máxima de 2."
Acoplamiento y Cohesión
El acoplamiento mide cuánto depende un módulo de otros módulos. La cohesión mide cuán relacionadas están las responsabilidades dentro de un mismo módulo. El objetivo es bajo acoplamiento y alta cohesión: cada módulo hace una cosa bien y depende lo mínimo posible de otros.
// Alto acoplamiento (malo): UserService depende directamente de
// Database, Mailer, Logger y PaymentGateway
class UserService {
async register(user) {
const db = new Database(); // acoplamiento directo
const mailer = new Mailer(); // acoplamiento directo
await db.save(user);
await mailer.send(user.email);
}
}
// Bajo acoplamiento (bueno): UserService depende de interfaces
class UserService {
constructor(userRepo, notifier) { // inyección de dependencias
this.userRepo = userRepo;
this.notifier = notifier;
}
async register(user) {
await this.userRepo.save(user);
await this.notifier.notifyWelcome(user);
}
}
// Prompt para la IA:
"Analiza el acoplamiento de estas clases y sugiere cómo
reducirlo usando inyección de dependencias e interfaces:
[código]"
Deuda Técnica
La deuda técnica es el coste implícito de las decisiones rápidas que tomamos durante el desarrollo. Cada atajo, cada hack, cada TODO sin resolver se acumula y hace que el código sea más difícil de modificar en el futuro. La IA puede ayudarte a cuantificar y priorizar esta deuda.
Prompt para evaluar deuda técnica:
"Analiza este módulo y clasifica los problemas en:
- CRÍTICO: Bugs potenciales o problemas de seguridad
- ALTO: Código difícil de mantener que bloquea nuevas features
- MEDIO: Code smells que reducen legibilidad
- BAJO: Mejoras estéticas o de estilo
Para cada problema, estima el tiempo de refactorización
y el impacto en la mantenibilidad. Prioriza por ROI
(impacto / esfuerzo).
[código del módulo completo]"
Ejercicios Prácticos de Refactorización
La mejor forma de mejorar tus habilidades de refactorización es practicar. Estos ejercicios están diseñados para que los resuelvas primero por tu cuenta y luego uses la IA para comparar tu solución con una versión optimizada.
Ejercicio 1: Eliminar duplicación
Identifica el código duplicado en el siguiente ejemplo y refactorízalo para seguir el principio DRY (Don't Repeat Yourself). Luego pide a la IA que revise tu solución.
// Código con duplicación - refactoriza esto:
function createUser(data) {
const errors = [];
if (!data.name || data.name.length < 2) errors.push('Name too short');
if (!data.email || !data.email.includes('@')) errors.push('Invalid email');
if (errors.length) return { success: false, errors };
return db.users.insert({ ...data, createdAt: new Date() });
}
function createProduct(data) {
const errors = [];
if (!data.name || data.name.length < 2) errors.push('Name too short');
if (!data.price || data.price <= 0) errors.push('Invalid price');
if (errors.length) return { success: false, errors };
return db.products.insert({ ...data, createdAt: new Date() });
}
function createOrder(data) {
const errors = [];
if (!data.userId) errors.push('User required');
if (!data.items || !data.items.length) errors.push('Items required');
if (errors.length) return { success: false, errors };
return db.orders.insert({ ...data, createdAt: new Date() });
}
// Prompt: "Refactoriza estas 3 funciones eliminando la duplicación.
// Usa una factory function o un patrón que permita reutilizar
// la lógica de validación e inserción."
Ejercicio 2: Simplificar condicionales
Los condicionales anidados son uno de los mayores contribuidores a la complejidad del código. Practica transformar lógica condicional compleja en código limpio y legible.
// Simplifica estos condicionales:
function getUserDashboard(user) {
if (user) {
if (user.role === 'admin') {
if (user.verified) {
return { view: 'admin-full', features: allFeatures };
} else {
return { view: 'admin-limited', features: basicFeatures };
}
} else if (user.role === 'editor') {
if (user.verified) {
return { view: 'editor-full', features: editorFeatures };
} else {
return { view: 'editor-limited', features: basicFeatures };
}
} else {
return { view: 'viewer', features: viewerFeatures };
}
} else {
return { view: 'login', features: [] };
}
}
// Prompt: "Refactoriza esta función eliminando el anidamiento.
// Usa early returns, tablas de lookup o el patrón que consideres
// más apropiado. El resultado debe ser fácil de extender con
// nuevos roles en el futuro."
Preguntas Frecuentes
¿Cuándo debo refactorizar y cuándo dejar el código como está?
Refactoriza cuando el código dificulta la adición de nuevas funcionalidades, cuando los bugs son frecuentes en un módulo específico, o cuando nuevos desarrolladores tardan demasiado en entender el código. No refactorices si el código funciona correctamente, no va a cambiar pronto y tiene buena cobertura de tests. La regla de oro de Martin Fowler aplica: "Refactoriza cuando añadir una nueva feature sea más difícil de lo que debería ser". Nunca refactorices sin tests que garanticen que el comportamiento no cambia.
¿La IA puede romper mi código al refactorizar?
Sí, es posible. La IA puede introducir bugs sutiles al refactorizar, especialmente en código con efectos secundarios, lógica de concurrencia o dependencias implícitas. Por eso es esencial tener tests automatizados que verifiquen el comportamiento antes y después de la refactorización. Ejecuta siempre tu suite de tests completa después de aplicar cualquier cambio sugerido por la IA. Si no tienes tests, genera los tests primero (usando la IA) y refactoriza después.
¿Qué es mejor: refactorizar todo de golpe o poco a poco?
Siempre es mejor refactorizar incrementalmente. Los cambios grandes son difíciles de revisar, tienen mayor riesgo de introducir bugs y son más complicados de revertir si algo sale mal. La técnica recomendada es el "Boy Scout Rule": deja el código un poco mejor de como lo encontraste cada vez que lo tocas. Haz commits pequeños y atómicos, cada uno con un solo tipo de cambio (renombrar variables, extraer una función, eliminar duplicación). Esto facilita la revisión por pares y el rollback si es necesario.
¿Cómo convenzo a mi equipo de que refactorizar vale la pena?
Los argumentos más efectivos son los cuantificables. Muestra métricas como el tiempo que tarda el equipo en implementar nuevas features (si el código es malo, cada feature tarda más), la frecuencia de bugs en módulos específicos, o el tiempo de onboarding de nuevos desarrolladores. Usa la IA para generar un análisis de deuda técnica con estimaciones de tiempo e impacto. Propón dedicar un 20% del sprint a refactorización continua en vez de parar todo para un sprint de refactorización, que suele ser difícil de justificar ante stakeholders.