SRP: Principio de Responsabilidad Única
SRP: Principio de Responsabilidad Única
El Principio de Responsabilidad Única (Single Responsibility Principle, SRP) es la S de SOLID y, paradójicamente, el más famoso y el más malentendido de los cinco. La culpa la tiene su nombre: “responsabilidad única” suena como “una clase debe hacer una sola cosa”, y esa lectura —tan intuitiva como equivocada— lleva a diseños fragmentados hasta el absurdo. Este capítulo te va a dar la interpretación correcta, la que el propio Uncle Bob aclaró años después de acuñarlo.
Definición formal y “en cristiano”
La formulación clásica de Robert C. Martin es:
Una clase debe tener una, y solo una, razón para cambiar.
Con los años, viendo cómo la gente confundía “una razón para cambiar” con “una tarea”, Uncle Bob reformuló el principio de una manera mucho más precisa y útil:
Un módulo debe ser responsable ante uno, y solo uno, actor.
En cristiano: una clase no debería tener que responder a dos jefes distintos. Un “actor” es un grupo de personas (un rol, un departamento, un stakeholder) que puede pedir cambios en el software por sus propios motivos. Si tu clase mezcla lógica que le importa a Finanzas con lógica que le importa a la base de datos y con lógica que le importa a Recursos Humanos, entonces tres actores distintos pueden pedir cambios sobre la misma clase, por razones que no tienen nada que ver entre sí. Eso es exactamente lo que SRP prohíbe.
La palabra clave no es “cosa” ni “tarea”: es razón para cambiar, y cada razón viene de un actor diferente.
flowchart LR
F["Actor: Finanzas<br/>cambia reglas de cálculo"] --> C["¿Una sola clase?"]
D["Actor: DBA<br/>cambia la persistencia"] --> C
R["Actor: RRHH<br/>cambia el formato del reporte"] --> C
C -->|"SRP dice: NO"| X["Tres razones para cambiar<br/>= tres responsabilidades"]
El code smell que ataca
SRP ataca la baja cohesión que vimos en el capítulo 1: clases que agrupan responsabilidades no relacionadas. Estas clases reciben nombres cariñosos y despectivos a la vez —God class, objeto Dios, clase manager— y se reconocen porque:
- Tienen muchos métodos que operan sobre subconjuntos distintos de los atributos.
- Importan de todo: lógica de negocio, acceso a base de datos, librerías de formato, clientes HTTP.
- Cambian constantemente, en cada release, por razones que no tienen relación.
- Generan colisiones de merge permanentes: dos personas trabajando en features distintas tocan el mismo archivo y chocan.
Ese último punto es la señal más reveladora en un equipo. Cuando varias personas, trabajando en tareas que no tienen nada que ver, terminan modificando el mismo archivo una y otra vez, ese archivo casi siempre viola SRP.
Ejemplo MALO: la clase Empleado que hace de todo
Veamos el caso de manual. Una clase Empleado que calcula el salario, se guarda a sí misma en la base de datos y se imprime como reporte:
// ❌ Viola SRP: tres actores metidos en una sola clase
class Empleado {
constructor(
public readonly id: string,
public readonly nombre: string,
public readonly salarioBase: number,
public readonly horasExtra: number,
public readonly cargo: "junior" | "senior" | "gerente",
) {}
// Responsabilidad 1: lógica de negocio — le importa a FINANZAS
calcularSalarioNeto(): number {
const tarifaExtra = this.salarioBase / 160;
const pagoExtra = this.horasExtra * tarifaExtra * 1.5;
const bono =
this.cargo === "gerente" ? this.salarioBase * 0.2 :
this.cargo === "senior" ? this.salarioBase * 0.1 : 0;
const bruto = this.salarioBase + pagoExtra + bono;
return bruto - bruto * 0.18; // descuento de impuestos
}
// Responsabilidad 2: persistencia — le importa al DBA
guardar(): void {
const conexion = crearConexionMySQL("db-produccion");
conexion.ejecutar(
"INSERT INTO empleados (id, nombre, salario) VALUES (?, ?, ?)",
[this.id, this.nombre, this.calcularSalarioNeto()],
);
conexion.cerrar();
}
// Responsabilidad 3: presentación — le importa a RRHH
generarReporte(): string {
return `
<div class="ficha-empleado">
<h2>${this.nombre}</h2>
<p>Cargo: ${this.cargo}</p>
<p>Salario neto: $${this.calcularSalarioNeto().toFixed(2)}</p>
</div>
`;
}
}
Los síntomas, uno por uno
Esta clase tiene tres razones para cambiar, cada una de un actor distinto:
- Finanzas cambia la política de bonos o la tasa de impuestos → hay que tocar
Empleado. - El DBA migra de MySQL a PostgreSQL, o cambia el esquema de la tabla → hay que tocar
Empleado. - RRHH quiere el reporte en PDF en vez de HTML, o con otro diseño → hay que tocar
Empleado.
Ahora imaginá el desastre concreto. Una desarrolladora está cambiando la fórmula del bono (pedido de Finanzas) mientras otro desarrollador migra la persistencia a PostgreSQL (pedido del DBA). Ambos tocan Empleado.ts al mismo tiempo. Colisión de merge garantizada, en código que no tiene ninguna relación conceptual. Peor aún: un cambio en la lógica de persistencia podría alterar por accidente el cálculo (comparten el método calcularSalarioNeto), y de golpe tenemos fragilidad —tocaste la base de datos y se rompió el cálculo del sueldo—.
Además, esta clase es imposible de testear en aislamiento: para probar el cálculo del salario necesitás una conexión real a MySQL, porque todo está soldado.
Refactor paso a paso hacia el ejemplo BUENO
La cura es separar cada responsabilidad en su propia clase, de modo que cada actor tenga su propio archivo donde pedir cambios sin molestar a los demás.
Paso 1: un modelo de datos puro
Primero, Empleado deja de ser una God class y se convierte en lo que debería haber sido: un contenedor de datos, sin comportamiento ligado a infraestructura.
// Solo datos. No sabe calcular, ni guardarse, ni imprimirse.
class Empleado {
constructor(
public readonly id: string,
public readonly nombre: string,
public readonly salarioBase: number,
public readonly horasExtra: number,
public readonly cargo: "junior" | "senior" | "gerente",
) {}
}
Paso 2: extraer la lógica de negocio (actor Finanzas)
// Responsable ante FINANZAS y nadie más.
class CalculadoraSalario {
private readonly TASA_IMPUESTO = 0.18;
calcularNeto(empleado: Empleado): number {
const bruto = this.calcularBruto(empleado);
return bruto - bruto * this.TASA_IMPUESTO;
}
private calcularBruto(empleado: Empleado): number {
const tarifaExtra = empleado.salarioBase / 160;
const pagoExtra = empleado.horasExtra * tarifaExtra * 1.5;
return empleado.salarioBase + pagoExtra + this.calcularBono(empleado);
}
private calcularBono(empleado: Empleado): number {
switch (empleado.cargo) {
case "gerente": return empleado.salarioBase * 0.2;
case "senior": return empleado.salarioBase * 0.1;
default: return 0;
}
}
}
Paso 3: extraer la persistencia (actor DBA)
// Responsable ante el DBA y nadie más.
class RepositorioEmpleado {
constructor(
private readonly conexion: ConexionBD,
private readonly calculadora: CalculadoraSalario,
) {}
guardar(empleado: Empleado): void {
this.conexion.ejecutar(
"INSERT INTO empleados (id, nombre, salario) VALUES (?, ?, ?)",
[empleado.id, empleado.nombre, this.calculadora.calcularNeto(empleado)],
);
}
}
Paso 4: extraer la presentación (actor RRHH)
// Responsable ante RRHH y nadie más.
class ReporteEmpleado {
constructor(private readonly calculadora: CalculadoraSalario) {}
generarHTML(empleado: Empleado): string {
const neto = this.calculadora.calcularNeto(empleado);
return `
<div class="ficha-empleado">
<h2>${empleado.nombre}</h2>
<p>Cargo: ${empleado.cargo}</p>
<p>Salario neto: $${neto.toFixed(2)}</p>
</div>
`;
}
}
Ahora cada clase tiene un único actor al que rinde cuentas. Finanzas cambia CalculadoraSalario sin rozar la base de datos; el DBA cambia RepositorioEmpleado sin rozar la fórmula del bono; RRHH rediseña ReporteEmpleado sin tocar nada más. Las colisiones de merge desaparecen, la fragilidad desaparece, y cada pieza se puede testear por separado (podés probar CalculadoraSalario sin ninguna base de datos).
Diagrama antes / después
classDiagram
class Empleado_God {
+calcularSalarioNeto()
+guardar()
+generarReporte()
}
note for Empleado_God "ANTES: 3 actores,\n3 razones para cambiar"
class Empleado {
+id
+nombre
+salarioBase
}
class CalculadoraSalario {
+calcularNeto(Empleado)
}
class RepositorioEmpleado {
+guardar(Empleado)
}
class ReporteEmpleado {
+generarHTML(Empleado)
}
note for Empleado "DESPUÉS: cada clase\nresponde a un solo actor"
CalculadoraSalario ..> Empleado : usa
RepositorioEmpleado ..> Empleado : usa
ReporteEmpleado ..> Empleado : usa
Matices, cuándo aplicarlo y errores comunes
SRP no es “una función por clase”
El error más frecuente es leer SRP como “cada clase hace una sola cosa” y llevarlo al extremo: una clase por método, clases anémicas por todos lados, indirecciones interminables. Eso no es SRP, es fragmentación. SRP habla de razones para cambiar, y una razón para cambiar puede perfectamente involucrar varios métodos que colaboran hacia el mismo fin y sirven al mismo actor. CalculadoraSalario tiene tres métodos (calcularNeto, calcularBruto, calcularBono) y respeta SRP a la perfección, porque los tres responden al mismo actor: Finanzas.
El peligro de la sobre-fragmentación
Si separás por separar, terminás con un enjambre de clases minúsculas que hay que orquestar, y el acoplamiento se muda del interior de una clase a la maraña de relaciones entre veinte. Eso es sobre-ingeniería, y es un anti-patrón que atacamos de frente en el capítulo 9. La regla práctica: separá cuando identifiques actores distintos que pueden pedir cambios por su cuenta, no antes.
¿Cómo detecto los actores en la práctica?
Preguntate: “si esta clase tuviera que cambiar, ¿quién en la organización lo pediría?”. Si la respuesta incluye a más de un grupo con intereses distintos —Finanzas y el DBA, o Marketing y Seguridad— tenés más de una responsabilidad y un candidato claro para separar.
Relación con los otros principios
- Cohesión (capítulo 1): SRP es, esencialmente, la búsqueda de alta cohesión aplicada a nivel de clase. Una clase que respeta SRP es una clase cohesiva: todo lo que contiene sirve al mismo propósito y al mismo actor.
- ISP (capítulo 5): el Principio de Segregación de Interfaces es “SRP a nivel de interfaz”. Así como SRP dice que una clase no debe servir a varios actores, ISP dice que una interfaz no debe obligar a un cliente a depender de métodos que no usa. Son la misma idea vista desde dos alturas distintas.
Resumen
- SRP: una clase debe tener una sola razón para cambiar, donde “razón” equivale a un actor (un grupo de stakeholders que puede pedir cambios por sus propios motivos).
- No significa “una clase hace una sola cosa” ni “una función por clase”. Significa responder a un solo actor.
- El code smell que ataca es la God class: baja cohesión, cambios constantes por razones no relacionadas y colisiones de merge entre features que no tienen nada que ver.
- El refactor consiste en separar cada responsabilidad en su clase: negocio (
CalculadoraSalario), persistencia (RepositorioEmpleado) y presentación (ReporteEmpleado), con un modelo de datos puro (Empleado). - El beneficio: sin colisiones de merge, sin fragilidad entre responsabilidades y cada pieza testeable en aislamiento.
- Cuidado con el extremo opuesto: la sobre-fragmentación también es un problema. Separá cuando identifiques actores reales, no por deporte.
Con las responsabilidades bien separadas, ya podemos hablar de cómo hacer que el código crezca sin tener que modificarlo: