Dismitificando SOLID: Principio de responsabilidad unica

Dismitificando SOLID: Principio de responsabilidad unica
Photo by Guillaume Bolduc / Unsplash

¡Bienvenido nuevamente a mi blog! Hoy vamos a desglosar uno de esos conceptos que, una vez que se hacen clic, transforman nuestra forma de escribir código: el Principio de Responsabilidad Única (SRP).

Estoy seguro de que has oído hablar de SOLID, pero por si acaso no lo has hecho, SOLID es un ingenioso acrónimo que agrupa 5 pilares del diseño de software. Estos principios no son reglas rígidas, sino que son guías que nos ayudan a construir software más escalable, comprensible y, sobre todo, fácil de mantener a lo largo del tiempo.

Debido a que cada letra de SOLID es tan completa que merece su propia entrada, hoy nos enfocaremos de lleno en la "S": el Principio de Responsabilidad Única.

Principio de Responsabilidad Única (SRP): La Base de Todo.

De ahora en adelante nos referiremos a este concepto como SRP, las cuales son las siglas de Single Responsability Principle, o bien conocido en español como Principio de Responsabilidad Única. Es la primera letra y, para muchos, el cimiento sobre el que se construyen los demás principios de SOLID.

Pero, ¿a qué se refiere realmente?

Este concepto fue formulado por Robert C. Martin (conocido como "Uncle Bob"), y establece de forma clara que:

"Una clase debe tener una, y sólo una, razón para cambiar."

Esa "razón para cambiar" es lo que Martin identifica como una responsabilidad. Básicamente, cada clase o modulo debería enfocarse en único problema o tarea bien definida. Podemos pensar en esto como tener especialistas en un equipo, donde cada uno se centra en lo que es mejor y le concierne.

Actores y Razones para cambiar

Para que no nos perdamos en lo abstracto que suena la frase "una razón para cambiar", Uncle Bob introdujo mas tarde el concepto de "actor", el cual se refiere a un grupo (que puede ser una o mas personas, vamos hasta otro sistema) que requiere un cambio en un sector de nuestro software en especifico.

Este punto de vista es importante, ya que esta vinculando las "razones para cambiar" con las necesidades o roles específicos dentro de un sistema. Por ejemplo, aunque una misma persona pueda desempeñar múltiples roles (como contador y administrador de bases de datos), estos roles representan actores distintos con diferentes necesidades de cambio en el software:

  • El "Rol de Contador" (actor) podría necesitar cambios en el modulo de calculo de impuestos.
  • El "Rol de administrador de BD" (actor) podría necesitar cambios en el modulo de optimización de consultas.

Por lo tanto, un modulo (o clase) debería ser responsable de resolver las solicitudes de un solo actor. Si diferentes grupos de usuarios o interesados requieren cambios por motivos distintos, esas responsabilidades deberían existir en módulos o clases separadas.

Ahora, en cuanto a la "razón para cambiar", el contexto de SRP, es prácticamente sinónimo de "responsabilidad". Si una clase tiene múltiples razones para cambiar entonces significa que esta asumiendo mas de una responsabilidad, justo ahí, es donde empiezan los problemas.

Ventajas del SRP

Si bien este concepto por ahora nos parece abstracto y sin sentido, tiene muchos beneficios a largo plazo:

  1. Mantenibilidad Mejorada: Cuando necesitamos hacer una modificación, sabemos exactamente donde ir, debido a que el código relacionado es mucho mas fácil de entender y podremos modificarlo sin temor a romper otras funcionalidades.
  2. Mayor cohesión: Los elementos dentro de una clase estan completamente relacionados y solo realizan una tarea.
  3. Menos acoplamiento: Una de las ventajas fuertes, nos permite minimizar las posibilidades de afectar a otros módulos.
  4. Testeabilidad Simplificada: Esto esta relacionado a otra buena practica, que consiste en implementar tests en nuestro código, bueno, al seguir SRP nos aseguramos de que sea más simple el proceso de crear esos test asegurándonos de que la única tarea que tienen la hagan bien.
  5. Reutilización facilitada: Otro concepto que se nos repite al aprender a escribir código limpio y mantenible, al usar SRP aseguramos la reutilización de módulos reduciendo al mínimo el código duplicado.
  6. Colaboración mas sencilla: Sin duda, un punto fuerte para implementar en proyectos de mas de 1 persona, si implementamos SRP podemos hacer cambios y ajustes a nuestras tareas sin temor a afectar el código de nuestro compañero.

Pero ya mucho bla, bla, bla. Hora de ver esto en acción.

Como desarrolladores muchas veces entendemos esto mas cuando lo vemos implementado, entonces manos a la obra, revisemos el siguiente ejemplo:

El anti-ejemplo: Vamos a violar intencionalmente SRP.

Vamos a crear un archivo llamado main.py

class GestorUsuarios:
    def __init__(self, nombre_usuario, email, detalles_pago_raw):
        self.nombre_usuario = nombre_usuario
        self.email = email
        self.detalles_pago_raw = detalles_pago_raw

    def validar_datos_usuario(self):
        print(f"Validando datos para {self.nombre_usuario}...")
        if not self.nombre_usuario or "@" not in self.email:
            print("Error: Nombre de usuario y email válido son requeridos.")
            return False
        # ... más lógica de validación de datos del usuario ...
        print("Datos de usuario validados.")
        return True

    def guardar_usuario_en_db(self):
        print(f"Guardando {self.nombre_usuario} en la base de datos...")
        # ... código para conectar a la BD y guardar el usuario ...
        # Imaginemos que implementamos SQL, ORM, etc.
        print(f"Usuario {self.nombre_usuario} guardado en la BD.")
        return True

    def procesar_pago_suscripcion(self):
        print(f"Procesando pago para {self.nombre_usuario} con datos: {self.detalles_pago_raw}")
        # ... lógica para conectar con la pasarela de pago, procesar, etc. ...
        # Esto podría cambiar si cambiamos de proveedor de pagos
        print("Pago procesado exitosamente.")
        return True

    def enviar_email_bienvenida(self):
        print(f"Enviando email de bienvenida a {self.email}...")
        # ... código para formatear y enviar un email ...
        # El contenido del email o el proveedor de email podría cambiar
        print(f"Email de bienvenida enviado a {self.email}.")
        return True

# Uso (violando SRP)
print("--- Ejecutando con Violación de SRP ---")
gestor_complejo = GestorUsuarios("AnaDev", "[email protected]", "VISA-4545-XXXX-XXXX")

if gestor_complejo.validar_datos_usuario():
    gestor_complejo.guardar_usuario_en_db()
    gestor_complejo.procesar_pago_suscripcion() # ¡Otra responsabilidad!
    gestor_complejo.enviar_email_bienvenida()
print("-" * 30)

En este ejemplo podemos almenos 4 errores:

  1. Validación de datos del usuario: Esto puede cambiar dependiendo de las reglas de negocio
  2. Persistencia del usuario en el mismo archivo: El motor de bases de datos puede cambiar en el futuro.
  3. Procesamiento de pagos en el mismo archivo: Estamos implementando la pasarela de pagos en el mismo lugar donde se guarda la informacion, nuevamente esto es otra responsabilidad.
  4. Envio de emails: Otra responsabilidad totalmente distinta.

En conclusión, ese archivo tiene muchas responsabilidades, lo que causaría que si queremos hacer una modificación a cualquiera de ellas estamos poniendo en peligro todas las demas funcionalidades.

La solución: Refactorización adoptando SRP.

Ahora vamos a hacerlo bien, dividamos las tareas en clases mas pequeñas y enfocadas.

Vamos a crear un archivo llamado srp_aplicado.py

# 1. Responsabilidad: Data del Usuario
class Usuario:
    def __init__(self, nombre_usuario, email):
        self.nombre_usuario = nombre_usuario
        self.email = email

# 2. Responsabilidad: Validación de datos del usuario
class ValidadorUsuario:
    def validar(self, usuario: Usuario):
        print(f"Validando datos para {usuario.nombre_usuario}...")
        if not usuario.nombre_usuario or "@" not in usuario.email:
            print("Error: Nombre de usuario y email válido son requeridos.")
            return False
        print("Datos de usuario validados.")
        return True

# 3. Responsabilidad: Persistencia del usuario
class RepositorioUsuarios:
    def guardar(self, usuario: Usuario):
        print(f"Guardando {usuario.nombre_usuario} en la base de datos...")
        # ... código BD ...
        print(f"Usuario {usuario.nombre_usuario} guardado en la BD.")
        return True

# 4. Responsabilidad: Procesamiento de pagos
class ServicioPagos:
    def procesar_pago(self, usuario: Usuario, detalles_pago_raw: str):
        print(f"Procesando pago para {usuario.nombre_usuario} con datos: {detalles_pago_raw}")
        # ... lógica pasarela de pago ...
        print("Pago procesado exitosamente.")
        return True

# 5. Responsabilidad: Envío de emails
class ServicioNotificaciones:
    def enviar_email_bienvenida(self, email_destino: str):
        print(f"Enviando email de bienvenida a {email_destino}...")
        # ... código envío email ...
        print(f"Email de bienvenida enviado a {email_destino}.")
        return True

# Clase Orquestadora (también puede ser un caso de uso o un servicio de aplicación)
# Esta clase SÍ puede cambiar si el flujo de registro cambia, pero no si la lógica interna de cada paso cambia.
class ServicioRegistroUsuario:
    def __init__(self, validador, repositorio, servicio_pagos, servicio_notificaciones):
        self.validador = validador
        self.repositorio = repositorio
        self.servicio_pagos = servicio_pagos
        self.servicio_notificaciones = servicio_notificaciones

    def registrar_y_suscribir_usuario(self, nombre, email, detalles_pago):
        nuevo_usuario = Usuario(nombre, email)

        if not self.validador.validar(nuevo_usuario):
            return False # Falló la validación

        if not self.repositorio.guardar(nuevo_usuario):
            return False # Falló el guardado en BD

        if not self.servicio_pagos.procesar_pago(nuevo_usuario, detalles_pago):
            # Aquí podríamos tener lógica de rollback o compensación si es necesario
            print(f"Error al procesar pago para {nuevo_usuario.nombre_usuario}. El usuario fue guardado pero el pago falló.")
            return False # Falló el pago

        self.servicio_notificaciones.enviar_email_bienvenida(nuevo_usuario.email)
        print(f"Usuario {nuevo_usuario.nombre_usuario} registrado y suscrito exitosamente.")
        return True


# Uso (aplicando SRP)
print("--- Ejecutando con SRP Aplicado ---")
validador_usr = ValidadorUsuario()
repo_usr = RepositorioUsuarios()
pagos_svc = ServicioPagos()
notif_svc = ServicioNotificaciones()

# Inyectamos las dependencias al servicio orquestador
servicio_registro = ServicioRegistroUsuario(
    validador=validador_usr,
    repositorio=repo_usr,
    servicio_pagos=pagos_svc,
    servicio_notificaciones=notif_svc
)

servicio_registro.registrar_y_suscribir_usuario("CarlosDev", "[email protected]", "MC-1212-XXXX-XXXX")
print("-" * 30)

# ¿Qué pasa si solo queremos validar un usuario sin registrarlo?
# ¡Ahora podemos!
print("--- Usando Validador de forma aislada ---")
usuario_prueba = Usuario("Test", "[email protected]")
validador_usr.validar(usuario_prueba)
print("-" * 30)

Observemos las diferencias, podemos notar una mayor cantidad de código, pero que nos permite identificar y modificar de manera mas segura y fácil el modulo que queramos. Si cambiamos la base de datos, no tendremos el miedo de que el pago deje de funcionar ya que estan totalmente separados (unidos únicamente por el orquestador)

Cuidado con los extremos

Si bien acabamos de observar las ventajas de SRP, como toda buena herramienta, no debemos pecar de usarla mal.

Debemos evitar las famosas "God Classes", estas serian el anti-srp por completo, son clases que hacen de todo y saben demasiado, nuestro primer ejemplo era un mini Dios.

No seamos extremistas, y sobre fragmentemos, no necesitamos una clase para cada linea de código o cada método simple, se debe separar por responsabilidad, esto es importante dejarlo en claro, ya que una responsabilidad puede requerir vamos métodos para cumplirse. Debemos buscar un un equilibrio entre claridad y la facilidad de cambio, no el mayor numero de clases.

Conclusión: El SRP es Tu Amigo

Para finalizar, podemos obtener que el SRP es mas que una regla; es una mentalidad que nos impulsa hacia código mas limpio y robusto. Al principio puede parecer que estamos escribiendo mas código, o nos dará mas trabajo, pero a largo plazo, nos ahorrara incontables dolores de cabeza, facilitara el trabajo en equipo y hará que el escribir código sea un placer de mantener y evolucionar.

En los próximos post, estaremos completando los siguientes puntos de SOLID,¡Hasta el próximo miércoles y feliz coding!