13. Cómo refactorizar una aplicación para cumplir SOLID

Refactorizar una aplicación existente hacia los principios SOLID requiere estrategia, disciplina y una visión clara del dominio. En este capítulo presentamos un plan de trabajo paso a paso, con ejemplos en Java, que podés adaptar a tus proyectos.

13.1 Diagnóstico inicial

  • Analizar métricas: revisar complejidad ciclomatica, tamaño de clases y cobertura de pruebas.
  • Identificar hotspots: localizar módulos con muchos defectos o cambios frecuentes.
  • Entrevistar al equipo: comprender qué áreas son difíciles de mantener y por qué.

El diagnóstico sirve para priorizar la refactorización. No es necesario aplicar SOLID a toda la base de código de una sola vez.

13.2 Plan de acción escalonado

  1. Seleccionar un módulo crítico pero acotado (por ejemplo, gestión de pedidos).
  2. Definir métricas objetivo: cobertura, tiempo de despliegue, número de incidencias.
  3. Establecer checkpoints y revisiones con el equipo para evaluar el progreso.

13.3 Ejemplo inicial: módulo de facturación acoplado

Partimos de un servicio que viola varios principios a la vez.

class FacturacionService {
    void facturar(Pedido pedido) {
        if (!pedido.esValido()) {
            throw new IllegalStateException("Pedido inválido");
        }
        // cálculo de impuestos
        BigDecimal impuestos = pedido.total().multiply(new BigDecimal("0.21"));
        // persistencia
        EntityManager em = Persistence.createEntityManagerFactory("app").createEntityManager();
        em.getTransaction().begin();
        em.persist(new Factura(pedido, impuestos));
        em.getTransaction().commit();
        // notificación
        System.out.println("Enviando correo a " + pedido.cliente());
    }
}

Aquí se mezclan cálculo, persistencia y notificación, sin pruebas automatizadas ni interfaces.

13.4 Paso 1: asegurar pruebas de regresión

Antes de refactorizar, crear tests que capturen el comportamiento actual. Si no es posible escribir pruebas unitarias, comenzar con pruebas de integración para proteger los flujos principales.

13.5 Paso 2: aplicar SRP

Dividimos la clase en colaboradores con responsabilidades claras.

interface CalculadoraImpuestos {
    BigDecimal calcular(Pedido pedido);
}

interface GeneradorFacturas {
    void generar(Pedido pedido, BigDecimal impuestos);
}

interface NotificadorFacturas {
    void notificar(Pedido pedido);
}

class FacturacionService {
    private final CalculadoraImpuestos calculadoraImpuestos;
    private final GeneradorFacturas generadorFacturas;
    private final NotificadorFacturas notificadorFacturas;

    FacturacionService(CalculadoraImpuestos calculadoraImpuestos,
                       GeneradorFacturas generadorFacturas,
                       NotificadorFacturas notificadorFacturas) {
        this.calculadoraImpuestos = calculadoraImpuestos;
        this.generadorFacturas = generadorFacturas;
        this.notificadorFacturas = notificadorFacturas;
    }

    void facturar(Pedido pedido) {
        if (!pedido.esValido()) {
            throw new IllegalStateException("Pedido inválido");
        }
        BigDecimal impuestos = calculadoraImpuestos.calcular(pedido);
        generadorFacturas.generar(pedido, impuestos);
        notificadorFacturas.notificar(pedido);
    }
}

13.6 Paso 3: invertir dependencias (DIP)

Las interfaces nos permiten desacoplar el servicio de las implementaciones concretas. A continuación, creamos implementaciones específicas.

class CalculadoraImpuestosBasica implements CalculadoraImpuestos {
    public BigDecimal calcular(Pedido pedido) {
        return pedido.total().multiply(new BigDecimal("0.21"));
    }
}

class GeneradorFacturasJpa implements GeneradorFacturas {
    private final EntityManager em;

    GeneradorFacturasJpa(EntityManager em) {
        this.em = em;
    }

    public void generar(Pedido pedido, BigDecimal impuestos) {
        em.getTransaction().begin();
        em.persist(new Factura(pedido, impuestos));
        em.getTransaction().commit();
    }
}

class NotificadorFacturasMail implements NotificadorFacturas {
    public void notificar(Pedido pedido) {
        System.out.println("Enviando correo a " + pedido.cliente());
    }
}

13.7 Paso 4: habilitar extensiones (OCP + ISP)

Podemos agregar nuevas variantes sin modificar el servicio central, simplemente implementando las interfaces.

class NotificadorFacturasSms implements NotificadorFacturas {
    public void notificar(Pedido pedido) {
        System.out.println("Enviando SMS a " + pedido.cliente());
    }
}

Las interfaces se mantienen delgadas, enfocadas en una única capacidad (ISP).

13.8 Paso 5: validar sustitución (LSP)

Crear una suite de pruebas polimórficas que ejecute los mismos casos contra todas las implementaciones asegura que cada variante respeta el contrato. Esto evita sorpresas en producción.

13.9 Automatizar la integración

  • Configurar pipelines: ejecutar pruebas unitarias, de integración y análisis estático.
  • Monitorear métricas: comparar resultados antes y después de la refactorización.
  • Publicar reportes: mantener al equipo informado sobre la evolución.

13.10 Checklist de refactorización SOLID

  • ¿Las responsabilidades están separadas (SRP)?
  • ¿Las extensiones se logran implementando interfaces, sin modificar código probado (OCP)?
  • ¿Las subclases o implementaciones respetan el contrato (LSP)?
  • ¿Las interfaces son específicas y no obligan a métodos innecesarios (ISP)?
  • ¿Los módulos de alto nivel dependen de abstracciones en lugar de detalles (DIP)?

Refactorizar con SOLID no se trata solo de cambiar código. Implica alinear al equipo, documentar decisiones y repetir el proceso en iteraciones. En el siguiente capítulo veremos casos prácticos en Java que muestran esta evolución paso a paso.