12. Comparación entre código sin SOLID y código con SOLID

Para apreciar el valor de los principios SOLID es útil analizar un caso realista antes y después de aplicar una refactorización. Este capítulo muestra una comparativa en Java, destacando cómo cambian la estructura, la mantenibilidad y la capacidad de prueba.

Partimos de un módulo que gestiona órdenes de compra con lógica mezclada y dependencias rígidas. Luego aplicamos SOLID para separar responsabilidades, invertir dependencias y preparar el código para crecer.

12.1 Código original sin SOLID

El siguiente fragmento concentra validación, persistencia y notificaciones dentro de una única clase.

class OrdenService {
    private final Connection connection;

    OrdenService(Connection connection) {
        this.connection = connection;
    }

    void procesar(Orden orden) throws SQLException {
        if (orden.total() <= 0) {
            throw new IllegalArgumentException("Total inválido");
        }

        PreparedStatement ps = connection.prepareStatement(
            "INSERT INTO ordenes (cliente, total) VALUES (?, ?)");
        ps.setString(1, orden.cliente());
        ps.setBigDecimal(2, orden.total());
        ps.executeUpdate();

        if (orden.total().compareTo(new BigDecimal("10000")) > 0) {
            System.out.println("Enviar mail VIP a " + orden.cliente());
        } else {
            System.out.println("Enviar mail estándar a " + orden.cliente());
        }
    }
}

Problemas detectados:

  • SRP violado: la clase se ocupa de validar, persistir y notificar.
  • DIP ausente: depende de JDBC directo.
  • OCP inexistente: agregar un nuevo canal de notificación implica modificar la clase.
  • Testabilidad baja: las pruebas necesitan una base de datos real.

12.2 Refactorización aplicada con SOLID

Vamos a introducir interfaces y clases enfocadas que colaboren entre sí.

interface ValidadorOrdenes {
    void validar(Orden orden);
}

interface RepositorioOrdenes {
    void guardar(Orden orden);
}

interface NotificadorOrdenes {
    void notificar(Orden orden);
}

class OrdenService {
    private final ValidadorOrdenes validador;
    private final RepositorioOrdenes repositorio;
    private final NotificadorOrdenes notificador;

    OrdenService(ValidadorOrdenes validador,
                 RepositorioOrdenes repositorio,
                 NotificadorOrdenes notificador) {
        this.validador = validador;
        this.repositorio = repositorio;
        this.notificador = notificador;
    }

    void procesar(Orden orden) {
        validador.validar(orden);
        repositorio.guardar(orden);
        notificador.notificar(orden);
    }
}

12.3 Implementaciones concretas

class ValidadorOrdenesBasico implements ValidadorOrdenes {
    public void validar(Orden orden) {
        if (orden.total() <= 0) {
            throw new IllegalArgumentException("Total inválido");
        }
    }
}

class RepositorioOrdenesJdbc implements RepositorioOrdenes {
    private final DataSource dataSource;

    RepositorioOrdenesJdbc(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void guardar(Orden orden) {
        try (Connection connection = dataSource.getConnection();
             PreparedStatement ps = connection.prepareStatement(
                 "INSERT INTO ordenes (cliente, total) VALUES (?, ?)")) {
            ps.setString(1, orden.cliente());
            ps.setBigDecimal(2, orden.total());
            ps.executeUpdate();
        } catch (SQLException e) {
            throw new IllegalStateException("No se pudo guardar la orden", e);
        }
    }
}

class NotificadorOrdenesMail implements NotificadorOrdenes {
    public void notificar(Orden orden) {
        if (orden.total().compareTo(new BigDecimal("10000")) > 0) {
            System.out.println("Enviar mail VIP a " + orden.cliente());
        } else {
            System.out.println("Enviar mail estándar a " + orden.cliente());
        }
    }
}

12.4 Ventajas observadas

  • SRP cumplido: cada clase tiene una responsabilidad única.
  • DIP: el servicio depende de contratos, no de implementaciones concretas.
  • OCP: podemos agregar NotificadorOrdenesSms sin tocar el servicio central.
  • Testabilidad: se pueden usar dobles de prueba que implementen las interfaces.

12.5 Métricas comparativas

Aspecto Antes Después
Complejidad ciclomatica 12 4
Métodos por clase principal 1 método de +50 líneas 1 método orquestador de 6 líneas
Cobertura de pruebas 20 % 75 % (gracias a mocks)
Tiempo de incorporación de un nuevo canal 4 horas (modificar OrdenService) 1 hora (nueva implementación de NotificadorOrdenes)

12.6 Checklist para refactorizar hacia SOLID

  • Detectar responsabilidades mezcladas: identificar métodos largos o clases “dios”.
  • Crear interfaces con intención: basadas en necesidades reales del negocio.
  • Inyectar dependencias: utilizar constructores o contenedores IoC.
  • Agregar pruebas de regresión: garantizar que la funcionalidad se mantiene.
  • Medir resultados: comparar complejidad, cobertura y tiempos antes y después.

Esta comparación demuestra que SOLID no es un fin en sí mismo, sino un medio para conseguir software adaptable, testeable y alineado con las necesidades del negocio. En el próximo tema veremos cómo abordar una refactorización completa para llevar sistemas heredados hacia estos principios.