El principio de responsabilidad única es uno de los principios SOLID. Defiende los beneficios de que clases y funciones tengan una responsabilidad clara y específica, de forma que solo se tengan que modificar por un motivo. Aplicándolo conseguimos que nuestro código sea más claro y mantenible.

El problema real

Seguro que en algún momento de tu vida como desarrollador te has cruzado con clases que validan datos, acceden a base de datos, construyen respuestas, escriben logs, aplican lógica de negocio… todo en un mismo sitio.

Aplicar una modificación en estas clases, por muy pequeña y sencilla que parezca, además de la dificultad de ubicarla genera una alta probabilidad de generar problemas.

Un ejemplo reducido de una clase que viola el principio podría ser el siguiente UserService.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class UserService {

    public void createUser(User user) {
    
	    // Validacion de los datos de entrada
        if (user.getEmail() == null) {
            throw new IllegalArgumentException("Email obligatorio");
        }

        // Guardar en base de datos
        database.save(user);

        // Enviar email
        emailSender.sendWelcomeEmail(user);

        // Log
        logger.info("Usuario creado: " + user.getEmail());
    }
}

Esta clase tiene las responsabilidades de: validar datos, persistencia, comunicación externa, logging y lógica de negocio. Demasiadas razones para cambiar.

Responsabilidad única en clases

Si aplicamos el principio de responsabilidad única a esa misma clase, podría quedar de la siguiente manera.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class UserService {

    private final UserValidator validator;
    private final UserRepository repository;
    private final NotificationService notificationService;

    public void createUser(User user) {
        validator.validate(user);
        repository.save(user);
	    notificationService.sendWelcome(user);
    }
}

De esta forma obtenemos:

  • Código más fácil de leer
  • Cambios más seguros
  • Test más simples
  • Menos miedo a tocar código antiguo
  • Menos clases Dios

Responsabilidad única en funciones

Este mismo problema surge a nivel de funciones. Una función debería hacer solo una cosa y hacerla bien. Y en este caso, una sola cosa no significa una sola línea, sino un único nivel de abstracción (qué hace frente al cómo lo hace).

De forma similar al ejemplo anterior de la clase, podríamos encontrarnos la siguiente función.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public void processOrder(Order order) {

    // Validación
    if (order == null || order.getItems().isEmpty()) {
        throw new IllegalArgumentException("Pedido inválido");
    }

    // Cálculo del total
    BigDecimal total = BigDecimal.ZERO;
    for (OrderItem item : order.getItems()) {
        total = total.add(item.getPrice().multiply(
                BigDecimal.valueOf(item.getQuantity())));
    }

    // Persistencia
    order.setTotal(total);
    orderRepository.save(order);

    // Comunicación externa
    emailService.sendConfirmation(order);
}

¿Cuántos motivos distintos podrían obligarme a modificar este código?

  • Cambio en las reglas de validación de los datos del pedido (dirección de entrega obligatoria).
  • Cambio en el cálculo del importe total del pedido (incluir gastos de envío).
  • Cambia la forma de almacenar los datos por infraestructura (se migra a otro sistema gestor de base de datos).
  • Cambia el canal de notificación (se envía un sms).

Aplicando SRP a la función, separando responsabilidades y manteniendo el comportamiento, podríamos hacer la siguiente división.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public void processOrder(Order order) {
    validate(order);
    BigDecimal total = calculateTotal(order);
    saveOrder(order, total);
    sendConfirmation(order);
}

private void validate(Order order) {
    if (order == null || order.getItems().isEmpty()) {
        throw new IllegalArgumentException("Pedido inválido");
    }
}

private BigDecimal calculateTotal(Order order) {
    BigDecimal total = BigDecimal.ZERO;

    for (OrderItem item : order.getItems()) {
        total = total.add(
            item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))
        );
    }

    return total;
}

private void saveOrder(Order order, BigDecimal total) {
    order.setTotal(total);
    orderRepository.save(order);
}

private void sendConfirmation(Order order) {
    emailService.sendConfirmation(order);
}

Aunque ahora hay más funciones, el código se lee como una historia. Permite cambiar una responsabilidad sin tocar las otras y facilita los test.

Es importante no confundir responsabilidad única con hacer una sola acción. La función processOrder sigue realizando varias acciones (validar, calcular, guardar y notificar), pero todas forman parte de una misma responsabilidad: gestionar el proceso completo de un pedido.

La función solo cambiará si cambia el flujo del proceso, no si cambian los detalles internos de validación, cálculo o persistencia.

No lo lleves al extremo

Algunos de los errores más comunes al intentar aplicar SRP son:

  • Reducir tanto como para llegar a una clase = un método
  • Crear abstracciones innecesarias, llegando a niveles muy profundos
  • Aplicarlo solo a clases y funciones grandes
  • Pensar ya lo simplificaremos (no se hará)

En aplicaciones pequeñas o scripts simples, dividir responsabilidades puede introducir más complejidad que beneficio. El diseño debe responder al contexto, no a principios aplicados de forma automática.

Conclusiones

El principio de responsabilidad única no es una regla estricta, es una alarma. Cuando una clase o función empieza a crecer sin control, probablemente no esté siendo “productiva”, sino acumulando responsabilidades que no le corresponden.