Muchos desarrolladores están convencidos de que lo dominan. Lo han leído, lo han escuchado en charlas, incluso lo repiten de memoria. Sin embargo, cuando miras el código real, es muy común ver que el SRP se aplica de forma incorrecta o, directamente, no se aplica en absoluto.
Si le preguntas a cualquier desarrollador qué es el Single Responsibility Principle, lo más probable es que te responda algo como:
“Significa que una función o un módulo debe hacer una sola cosa”.
Y aunque esa respuesta no es del todo falsa, sí es incompleta, y es justamente en ese matiz donde comienzan la mayoría de los problemas, porque al quedarse en una explicación superficial se pierde el verdadero sentido del principio; de hecho, el propio Robert C. Martin, más conocido como el tío Bob y creador de SOLID, ha señalado en varias ocasiones que esa definición tan repetida es, en realidad, incorrecta.
Sí, así tal cual.
El tío Bob explica que la idea de “hacer una sola cosa” tiene sentido cuando se utiliza como una guía para refactorizar funciones de bajo nivel, ya que ayuda a mejorar métodos largos, confusos y difíciles de leer; sin embargo, eso no es el SRP, porque el Principio de Responsabilidad Única va mucho más allá de contar líneas de código o de dividir funciones simplemente porque “se sienten grandes”, y tiene que ver con cómo y por qué cambia el software.
Históricamente, el SRP se definía así:
“Un módulo debe tener una, y solo una, razón para cambiar”.
Hasta aquí todo parece claro… hasta que aparece la pregunta incómoda:
¿qué es exactamente una “razón” para cambiar?
La respuesta es sorprendentemente simple: los sistemas no cambian por capricho técnico, cambian porque alguien lo pide. Cambian porque hay personas, equipos o áreas del negocio que necesitan que el software evolucione.
Por eso, con el tiempo, el propio tío Bob refinó la definición y la dejó en su forma más precisa y útil:
“Un módulo debe ser responsable ante un solo actor.”
Y ojo, porque un actor no tiene por qué ser una persona concreta. Normalmente es un rol, un área o un departamento dentro de la organización: Finanzas, Recursos Humanos, Marketing, Tecnología, Compliance, etc.
Cuando un módulo responde a las necesidades de más de un actor, estás mezclando responsabilidades. Y en ese punto, tarde o temprano, el código empieza a romperse… aunque “haga una sola cosa”.
Un ejemplo que viola el SRP
Imaginemos una tienda online muy básica. Queremos calcular el total de un pedido aplicando descuentos, impuestos y validaciones. Todo parece encajar naturalmente en una sola clase:
public class Pedido {
public double calcularTotal(double precioUnitario, int cantidad) {
double total = precioUnitario * cantidad;
if (cantidad >= 10) {
total *= 0.9;
}
return total;
}
public boolean esPedidoValido(int cantidad) {
return cantidad > 0;
}
public double aplicarImpuesto(double total) {
return total * 1.21;
}
}
El problema no está en el código, sino en los motivos de cambio
Aunque la clase funcione correctamente, concentra responsabilidades que pertenecen a actores distintos:
- Las reglas de descuentos responden al área de negocio
- El impuesto responde a finanzas
- Las validaciones responden a operaciones
Esto significa que cualquier cambio en una de esas áreas obliga a modificar la misma clase. Con el tiempo, ese archivo se convierte en un punto de fricción constante.
El SRP no busca que el código falle menos hoy, sino que sufra menos mañana.
Separar responsabilidades no es complicar el diseño
Aplicar el SRP no implica introducir frameworks ni patrones complejos. A veces, basta con separar conceptos que evolucionan de forma independiente.
El primer paso es dejar que la entidad represente solo los datos:
public class Pedido {
public double precioUnitario;
public int cantidad;
}
Luego, cada regla vive en su propio lugar.
La lógica de precios y descuentos queda aislada:
public class CalculadorDePrecio {
public double calcularSubtotal(Pedido pedido) {
double subtotal = pedido.precioUnitario * pedido.cantidad;
if (pedido.cantidad >= 10) {
subtotal *= 0.9;
}
return subtotal;
}
}
La lógica de impuestos se mantiene separada, lista para cambiar cuando finanzas lo decida:
public class CalculadorDeImpuestos {
public double aplicar(double subtotal) {
return subtotal * 1.21;
}
}
Y la validación del pedido vive en su propio contexto:
public class ValidadorDePedido {
public boolean esValido(Pedido pedido) {
return pedido.cantidad > 0 && pedido.precioUnitario > 0;
}
}
El patrón Facade: simplificar sin mezclar responsabilidades
Cuando se empieza a aplicar el SPR, suele aparecer una objeción muy común:
“Ahora tengo demasiadas clases para hacer algo sencillo”
Y es una reacción natural.
Separar responsabilidades implica dividir el sistema en piezas más pequeñas, y eso puede hacer que el uso del código se sienta más complejo de lo necesario.
Aquí es donde entra el patrón Facade.
Aplicar SRP suele llevar a este escenario:
- Una clase para precios
- Una clase para impuestos
- Una clase para validaciones
- Una clase para orquestar el flujo
Desde el punto de vista del diseño, esto es correcto.
Pero desde el punto de vista del cliente del código, no es cómodo.
El Facade permite simplificar el punto de entrada, sin volver a mezclar responsabilidades.
El Facade como punto de entrada estable
En nuestro ejemplo, la clase ProcesarPedido cumple exactamente ese rol.
public class ProcesarPedido {
private final CalculadorDePrecio calculadorDePrecio = new CalculadorDePrecio();
private final CalculadorDeImpuestos calculadorDeImpuestos = new CalculadorDeImpuestos();
private final ValidadorDePedido validador = new ValidadorDePedido();
public double ejecutar(Pedido pedido) {
if (!validador.esValido(pedido)) {
throw new IllegalArgumentException("Pedido inválido");
}
double subtotal = calculadorDePrecio.calcularSubtotal(pedido);
return calculadorDeImpuestos.aplicar(subtotal);
}
}
Desde afuera, el sistema se usa así:
double total = new ProcesarPedido().ejecutar(pedido);
El consumidor no necesita saber:
- cómo se calcula el descuento,
- qué impuestos se aplican,
- ni qué validaciones existen.
Eso es responsabilidad del Facade.
Un Facade no debería tomar decisiones importantes, solo coordinar:
- valida,
- calcula,
- delega,
- devuelve el resultado.
Cuando un Facade empieza a tener lógica propia, deja de ser un Facade y se convierte en una clase con múltiples responsabilidades.
Una buena regla mental es esta:
Si una regla cambia, el Facade no debería cambiar
Conclusión
El Principio de Responsabilidad Única no se trata de escribir menos código, sino de escribir código que envejezca bien.
Cuando cada clase tiene un solo motivo para cambiar:
- los bugs se aíslan,
- los tests se simplifican,
- y el sistema se vuelve más predecible.
No es un principio académico.
Es una herramienta práctica para construir software que resista el paso del tiempo.
Referencias
- Clean Architecture – Robert C. Martin
- Agile Software Development: Principles, Patterns, and Practices – Robert C. Martin
- Clean Code – Robert C. Martin
- Design Patterns – Erich Gamma et al.
- A Philosophy of Software Design – John Ousterhout