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.
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.
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.
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.
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);
}
}
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());
}
}
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).
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.
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.