Refactorización paso a paso
Vamos a tomar el siguiente código con múltiples code smells y refactorizarlo paso a paso aplicando los principios y técnicas que hemos visto.
4.1. Código Original (JavaScript)
function processOrder(orderId, items, userId, couponCode) {
console.log("Processing order: " + orderId);
// Get user
let user = null;
for (let i = 0; i < users.length; i++) {
if (users[i].id === userId) {
user = users[i];
break;
}
}
if (user === null) {
console.log("User not found");
return -1;
}
// Calculate total
let total = 0;
for (let i = 0; i < items.length; i++) {
total += items[i].price * items[i].quantity;
}
// Apply discount
let discount = 0;
if (couponCode !== null && couponCode !== "") {
if (couponCode === "SAVE10") {
discount = total * 0.1;
} else if (couponCode === "SAVE20") {
discount = total * 0.2;
} else {
console.log("Invalid coupon");
}
}
// Check if user has credit
let credit = 0;
if (user.hasCredit) {
credit = user.creditAmount;
if (credit > total) {
credit = total;
}
}
// Calculate final amount
let finalAmount = total - discount - credit;
// Process payment
let paymentResult;
if (user.paymentMethod === "CREDIT_CARD") {
paymentResult = processCreditCardPayment(user.creditCardNumber, finalAmount);
} else if (user.paymentMethod === "PAYPAL") {
paymentResult = processPayPalPayment(user.paypalEmail, finalAmount);
} else {
console.log("Unsupported payment method");
return -2;
}
if (paymentResult !== 0) {
console.log("Payment failed");
return -3;
}
// Update inventory
for (let i = 0; i < items.length; i++) {
let item = items[i];
let product = null;
for (let j = 0; j < products.length; j++) {
if (products[j].id === item.productId) {
product = products[j];
break;
}
}
if (product !== null) {
product.stock -= item.quantity;
}
}
// Create order record
let order = {
id: orderId,
userId: userId,
items: items,
total: total,
discount: discount,
credit: credit,
finalAmount: finalAmount,
date: new Date()
};
orders.push(order);
// Send confirmation email
let subject = "Order Confirmation - Order #" + orderId;
let body = "Thank you for your order, " + user.name + "!\n\n";
body += "Order details:\n";
for (let i = 0; i < items.length; i++) {
body += "- " + items[i].name + " x" + items[i].quantity + ": $" + (items[i].price * items[i].quantity) + "\n";
}
body += "\nTotal: $" + total;
if (discount > 0) {
body += "\nDiscount: -$" + discount;
}
if (credit > 0) {
body += "\nCredit Applied: -$" + credit;
}
body += "\nFinal Amount: $" + finalAmount;
sendEmail(user.email, subject, body);
console.log("Order processed successfully");
return 0;
}
4.2. Identificación de Code Smells
Antes de refactorizar, identificamos los code smells:
- Método Largo: La función hace demasiadas cosas
- Método con Muchos Parámetros: 4 parámetros
- Múltiples Niveles de Abstracción: Mezcla lógica de negocio, acceso a datos y presentación
- Duplicación de Código: Búsqueda de usuario y productos con el mismo patrón
- Códigos de Error Mágicos: Retorno de -1, -2, -3
- Acoplamiento Global: Dependencia de variables globales (users, products, orders)
- Switch Statements: Condicionales basados en el método de pago
- Responsabilidades Mezcladas: Cálculo, procesamiento de pago, inventario, emails
4.3. Plan de Refactorización
- Extraer clases con responsabilidades únicas
- Introducir objetos de parámetros
- Reemplazar códigos de error con excepciones/resultados
- Aplicar inyección de dependencias
- Reemplazar condicionales con polimorfismo
4.4. Refactorización Paso a Paso
Paso 1: Extraer Repositorios
class UserRepository {
findById(userId) {
return users.find(user => user.id === userId);
}
}
class ProductRepository {
findById(productId) {
return products.find(product => product.id === productId);
}
updateStock(productId, quantity) {
const product = this.findById(productId);
if (product) {
product.stock -= quantity;
}
}
}
class OrderRepository {
save(order) {
orders.push(order);
return order;
}
}
Paso 2: Crear Servicios Específicos
class DiscountService {
applyCoupon(total, couponCode) {
if (!couponCode) return 0;
switch (couponCode) {
case "SAVE10": return total * 0.1;
case "SAVE20": return total * 0.2;
default: return 0;
}
}
}
class PaymentService {
processPayment(user, amount) {
switch (user.paymentMethod) {
case "CREDIT_CARD":
return this.processCreditCardPayment(user.creditCardNumber, amount);
case "PAYPAL":
return this.processPayPalPayment(user.paypalEmail, amount);
default:
throw new Error("Unsupported payment method");
}
}
processCreditCardPayment(cardNumber, amount) {
// Implementación
}
processPayPalPayment(email, amount) {
// Implementación
}
}
class NotificationService {
sendOrderConfirmation(user, order) {
let subject = `Order Confirmation - Order #${order.id}`;
let body = `Thank you for your order, ${user.name}!\n\n`;
body += "Order details:\n";
for (const item of order.items) {
body += `- ${item.name} x${item.quantity}: $${item.price * item.quantity}\n`;
}
body += `\nTotal: $${order.total}`;
if (order.discount > 0) {
body += `\nDiscount: -$${order.discount}`;
}
if (order.credit > 0) {
body += `\nCredit Applied: -$${order.credit}`;
}
body += `\nFinal Amount: $${order.finalAmount}`;
return sendEmail(user.email, subject, body);
}
}
Paso 3: Crear Modelos de Dominio
class Order {
constructor(id, userId, items) {
this.id = id;
this.userId = userId;
this.items = items;
this.total = 0;
this.discount = 0;
this.credit = 0;
this.finalAmount = 0;
this.date = new Date();
}
calculateTotal() {
this.total = this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
return this.total;
}
applyDiscount(discount) {
this.discount = discount;
this.recalculateFinalAmount();
}
applyCredit(credit) {
this.credit = Math.min(credit, this.total - this.discount);
this.recalculateFinalAmount();
}
recalculateFinalAmount() {
this.finalAmount = this.total - this.discount - this.credit;
}
}
Paso 4: Introducir Result Objects
class Result {
static success(data) {
return new Result(true, data, null);
}
static failure(error) {
return new Result(false, null, error);
}
constructor(success, data, error) {
this.success = success;
this.data = data;
this.error = error;
}
}
Paso 5: Refactorizar la Función Principal
class OrderService {
constructor(
userRepository,
productRepository,
orderRepository,
discountService,
paymentService,
notificationService
) {
this.userRepository = userRepository;
this.productRepository = productRepository;
this.orderRepository = orderRepository;
this.discountService = discountService;
this.paymentService = paymentService;
this.notificationService = notificationService;
}
processOrder(orderRequest) {
try {
// 1. Buscar usuario
const user = this.userRepository.findById(orderRequest.userId);
if (!user) {
return Result.failure("User not found");
}
// 2. Crear objeto de orden
const order = new Order(orderRequest.orderId, user.id, orderRequest.items);
// 3. Calcular totales
order.calculateTotal();
// 4. Aplicar descuento si hay cupón
const discount = this.discountService.applyCoupon(
order.total,
orderRequest.couponCode
);
order.applyDiscount(discount);
// 5. Aplicar crédito del usuario si tiene
if (user.hasCredit) {
order.applyCredit(user.creditAmount);
}
// 6. Procesar pago
try {
this.paymentService.processPayment(user, order.finalAmount);
} catch (error) {
return Result.failure(`Payment failed: ${error.message}`);
}
// 7. Actualizar inventario
for (const item of order.items) {
this.productRepository.updateStock(item.productId, item.quantity);
}
// 8. Guardar orden
this.orderRepository.save(order);
// 9. Enviar notificación
this.notificationService.sendOrderConfirmation(user, order);
return Result.success(order);
} catch (error) {
return Result.failure(`Error processing order: ${error.message}`);
}
}
}
Paso 6: Implementación de Payment Strategy Pattern
// Interfaces para pagos
class PaymentStrategy {
process(amount) {
throw new Error("Method not implemented");
}
}
class CreditCardPayment extends PaymentStrategy {
constructor(cardNumber) {
super();
this.cardNumber = cardNumber;
}
process(amount) {
console.log(`Processing ${amount} via Credit Card ${this.cardNumber}`);
// Implementación real aquí
return true;
}
}
class PayPalPayment extends PaymentStrategy {
constructor(email) {
super();
this.email = email;
}
process(amount) {
console.log(`Processing ${amount} via PayPal to ${this.email}`);
// Implementación real aquí
return true;
}
}
// Factory para crear la estrategia adecuada
class PaymentStrategyFactory {
create(user) {
switch (user.paymentMethod) {
case "CREDIT_CARD":
return new CreditCardPayment(user.creditCardNumber);
case "PAYPAL":
return new PayPalPayment(user.paypalEmail);
default:
throw new Error(`Unsupported payment method: ${user.paymentMethod}`);
}
}
}
// Servicio de pago refactorizado
class PaymentService {
constructor(paymentStrategyFactory) {
this.paymentStrategyFactory = paymentStrategyFactory;
}
processPayment(user, amount) {
const paymentStrategy = this.paymentStrategyFactory.create(user);
return paymentStrategy.process(amount);
}
}
4.5. Uso del Código Refactorizado
// Configuración de las dependencias
const userRepository = new UserRepository();
const productRepository = new ProductRepository();
const orderRepository = new OrderRepository();
const discountService = new DiscountService();
const paymentStrategyFactory = new PaymentStrategyFactory();
const paymentService = new PaymentService(paymentStrategyFactory);
const notificationService = new NotificationService();
// Creación del servicio de orden
const orderService = new OrderService(
userRepository,
productRepository,
orderRepository,
discountService,
paymentService,
notificationService
);
// Uso del servicio
const orderRequest = {
orderId: "ORD-12345",
userId: "USR-789",
items: [
{ productId: "PROD-001", name: "Laptop", price: 1200, quantity: 1 },
{ productId: "PROD-002", name: "Mouse", price: 25, quantity: 2 }
],
couponCode: "SAVE10"
};
const result = orderService.processOrder(orderRequest);
if (result.success) {
console.log("Order processed successfully:", result.data);
} else {
console.error("Order processing failed:", result.error);
}
4.6. Análisis de la Refactorización
Comparemos el código original con el refactorizado:
Longitud
- Un solo método de ~100 líneas
- Múltiples clases y métodos pequeños
Responsabilidades
- Mezcladas en una función
- Claramente separadas por clase
Manejo de Errores
- Códigos de retorno
- Objetos Result con información detallada
Dependencias
- Variables globales
- Inyección de dependencias
Extensibilidad
- Difícil de extender
- Fácil agregar nuevos métodos de pago, descuentos, etc.
Testabilidad
- Muy difícil de probar
- Cada componente puede probarse aisladamente