Aikido

¿Por qué las clases deberían seguir el principio de responsabilidad única?

Legibilidad

Regla
Las clases deben tener una única responsabilidad.
Las clases que gestionan múltiples preocupaciones violan
el Principio de Responsabilidad Única.

Lenguajes compatibles: JS, TS, PY, JAVA, C/C++,
C#, Swift/Objective C, Ruby. PHP, Kotlin, 
Scala, Rust, Haskell, Groovy, Dart. Julia,
Elixir, Clojure, OCaml, Delphi

Introducción

Las clases que hacen demasiado se convierten en cuellos de botella. Una clase que gestiona la autenticación, los correos electrónicos y la validación requiere cambios cada vez que evoluciona alguna de estas preocupaciones, lo que conlleva el riesgo de romper funcionalidades no relacionadas. Las pruebas requieren simular (mocking) la clase completa incluso al probar un solo aspecto. El Principio de Responsabilidad Única establece que una clase debería tener solo una razón para cambiar.

Por qué es importante

Mantenibilidad del código: Las clases con múltiples responsabilidades cambian con más frecuencia porque la evolución de cualquier preocupación afecta a toda la clase.

Complejidad de las pruebas: Probar clases con múltiples responsabilidades requiere simular todas las dependencias, incluso para probar una sola funcionalidad.

Reutilización: No se puede extraer una responsabilidad sin arrastrar todas las dependencias. Los desarrolladores duplican código en lugar de desenredar clases con múltiples responsabilidades.

Coordinación del equipo: Múltiples desarrolladores trabajando en la misma clase para diferentes funcionalidades generan frecuentes conflictos de fusión (merge conflicts). Las clases de responsabilidad única permiten el desarrollo paralelo sin conflictos.

Ejemplos de código

❌ No conforme:

class UserManager {
    async createUser(userData) {
        const user = await db.users.insert(userData);
        await this.sendWelcomeEmail(user.email);
        await this.logEvent('user_created', user.id);
        await cache.set(`user:${user.id}`, user);
        return user;
    }

    async sendWelcomeEmail(email) {
        const template = this.loadEmailTemplate('welcome');
        await emailService.send(email, template);
    }

    async logEvent(event, userId) {
        await analytics.track(event, { userId, timestamp: Date.now() });
    }
}

Por qué está mal: Esta clase gestiona operaciones de base de datos, envío de correos electrónicos, registro y caché. Los cambios en las plantillas de correo electrónico, los formatos de registro o la estrategia de caché requieren modificar esta clase. Probar la creación de usuarios implica simular servicios de correo electrónico, análisis y caché, lo que hace que las pruebas sean lentas y frágiles.

✅ Conforme:

class UserRepository {
    async create(userData) {
        return await db.users.insert(userData);
    }
}

class EmailNotificationService {
    async sendWelcomeEmail(email) {
        const template = await this.templateLoader.load('welcome');
        return await this.emailSender.send(email, template);
    }
}

class UserEventLogger {
    async logCreation(userId) {
        return await this.analytics.track('user_created', {
            userId,
            timestamp: Date.now()
        });
    }
}

class UserService {
    constructor(repository, emailService, eventLogger, cache) {
        this.repository = repository;
        this.emailService = emailService;
        this.eventLogger = eventLogger;
        this.cache = cache;
    }

    async createUser(userData) {
        const user = await this.repository.create(userData);
        await Promise.all([
            this.emailService.sendWelcomeEmail(user.email),
            this.eventLogger.logCreation(user.id),
            this.cache.set(`user:${user.id}`, user)
        ]);
        return user;
    }
}

¿Por qué esto importa? Cada clase tiene una responsabilidad clara: persistencia de datos, envío de correos electrónicos, registro de eventos u orquestación. Los cambios en las plantillas de correo electrónico solo afectan EmailNotificationService. La prueba de creación de usuarios puede utilizar stubs simples para las dependencias. Las clases pueden reutilizarse de forma independiente en diferentes funcionalidades.

Conclusión

El Principio de Responsabilidad Única no se trata de hacer las clases lo más pequeñas posible, sino de asegurar que cada clase tenga una única razón clara para cambiar. Cuando una clase empieza a manejar múltiples responsabilidades, refactorice extrayendo cada responsabilidad en su propia clase con una interfaz enfocada. Esto facilita la prueba, el mantenimiento y la evolución del código sin cambios en cascada en funcionalidades no relacionadas.

Preguntas frecuentes

¿Tiene preguntas?

¿Cómo identifico cuándo una clase tiene demasiadas responsabilidades?

Busca clases con múltiples razones para cambiar. Si modificar la lógica del correo electrónico, el formato de registro y el esquema de la base de datos requiere cambiar la misma clase, esta tiene demasiadas responsabilidades. Revisa los nombres de los métodos: si cubren verbos no relacionados como sendEmail(), logEvent() y validateData() en la misma clase, es una señal de alarma. Las clases con más de 300-400 líneas a menudo indican múltiples responsabilidades, aunque el tamaño por sí solo no es definitivo.

¿Dividir las clases no crea más archivos y complejidad?

Más archivos no equivale a más complejidad. Diez clases enfocadas de 50 líneas cada una son más fáciles de entender que una clase de 500 líneas que lo maneja todo. La clave es que cada clase sea simple y tenga un propósito claro. La navegación en los IDE modernos hace que el número de archivos sea irrelevante. La reducción de la complejidad proviene de poder razonar sobre cada clase de forma independiente sin considerar preocupaciones no relacionadas.

¿Qué pasa con las clases que naturalmente necesitan coordinar múltiples operaciones?

La coordinación es en sí misma una responsabilidad. Una clase UserService puede orquestar llamadas a UserRepository, EmailService y EventLogger sin implementar esas preocupaciones directamente. Este es el patrón orquestador o fachada. La diferencia es que el orquestador delega en clases especializadas en lugar de implementar múltiples preocupaciones directamente. Es código pegamento ligero, no lógica de negocio.

¿Cómo se aplica este principio a las clases de utilidad con métodos estáticos?

Las clases de utilidad son particularmente propensas a violar el principio de responsabilidad única porque es fácil seguir añadiendo métodos estáticos no relacionados. Una clase `StringUtils` podría empezar con ayudantes de formato, pero crecer hasta incluir validación, análisis, cifrado y codificación. Divida estas funcionalidades en clases de utilidad enfocadas como `StringFormatter`, `StringValidator` y `StringEncoder`. Cada una tiene un conjunto cohesivo de operaciones relacionadas.

¿Cómo refactorizo clases existentes que violan este principio?

Comienza identificando responsabilidades distintas dentro de la clase. Extrae primero la más sencilla a una nueva clase, actualiza las pruebas y verifica que todo funcione. Repite de forma iterativa en lugar de intentar una refactorización grande. Utiliza el patrón strangler fig: crea nuevas clases de responsabilidad única y mueve gradualmente el código de la clase antigua. Una vez que la clase antigua esté vacía o sea mínima, desapréciala. Cada paso debe ser un incremento funcional y testeable.

¿La responsabilidad única significa un solo método?

No. Una clase puede tener múltiples métodos siempre que todos estén relacionados con la misma responsabilidad. Una clase UserRepository podría tener métodos create(), update(), delete() y findById() porque todos cumplen la única responsabilidad de la persistencia de datos de usuario. Los métodos son variaciones cohesivas de la misma preocupación, no preocupaciones separadas empaquetadas juntas.

Asegúrate ahora.

Proteja su código, la nube y el entorno de ejecución en un único sistema central.
Encuentre y corrija vulnerabilidades de forma rápida y automática.

No se requiere tarjeta de crédito | Resultados del escaneo en 32 segundos.