SRP

SRP: El principio de una sola responsabilidad

April 6, 2021

¿Qué es SRP?

Hay muchas definiciones sobre este principio. La que más me gusta es:

Una entidad de software debe tener una y solo una razón para cambiar

Este principio fue recopilado y documentado por Robert C. Martin junto a otros 4 (SOLID) con la finalidad de promover un "código limpio" al que haré referencia como código mantenible de aquí en adelante.

Eso es en cuando a la definición. No es muy detallada, ¿verdad? ¿Qué se considera como una razón de cambio?

Me encanta mucho como Robert C. Martin nos invita a encontrar las razones de cambio haciéndonos la siguiente pregunta:

¿A quién debe satisfacer el diseño del código?

La respuesta a esta pregunta nos dará una razón de cambio: personas. Él explica que son las personas quienes piden los cambios. Y no queremos esos códigos, con distintos interesados, se mezclen.

Personalmente me ha ayudado mucho entender cómo identificar que mi código está alejándose de este principio. Y hasta ahora lo he relacionado mucho con los conceptos de cohesión y acoplamiento.

Cohesión: el nivel de relación del código dentro un módulo

Acoplamiento: el nivel de relación entre módulos

Es conocido que, para producir un código mantenible, debemos aumentar la cohesión y disminuir el acoplamiento de nuestras entidades de software. Bajo esta idea, podríamos decir que un código altamente cohesionado es aquél que tiene una sola razón de cambio. Cuando nos acoplamos a alguna dependencia más de lo que es necesario, nuestra razón de cambio será ampliada y eso no es lo que queremos.

Veamos un ejemplo

Bueno, la teoría hace que todo parezca simple, así que mejor veamos un ejemplo en los que se puede identificar que el código no está siguiendo el principio de una sola responsabilidad.

Esto no se trata de criticar el proyecto a continuación, sino de mostrar que hay oportunidades de mejora. Sin embargo, el hecho de que algo pueda ser mejorado, no significa que debamos obsesionarnos con SRP y querer cambiarlo todo. Lo importante es ser conscientes de que se puede mejorar y en el momento apropiado, hacerlo.

BroadcastManager::routes() de Laravel

<?php

    // https://github.com/laravel/framework/blob/8.x/src/Illuminate/Broadcasting/BroadcastManager.php#L63

    public function routes(array $attributes = null)
    {
        if ($this->app instanceof CachesRoutes && $this->app->routesAreCached()) {
            return;
        }

        $attributes = $attributes ?: ['middleware' => ['web']];

        $this->app['router']->group($attributes, function ($router) {
            $router->match(
                ['get', 'post'], '/broadcasting/auth',
                '\\'.BroadcastController::class.'@authenticate'
            )->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);
        });
    }

En la clase BroadcastManager está definido el método routes() y aunque es muy útil para personalizar el framework a nuestras necesidades, tiene un código con muy baja cohesión con respecto a los demás métodos. Además está acoplando la clase al router y también a VerifyCsrfToken.

¿Cómo es que he determinado esto?

La tarea principal de las clases con sufijo Manager dentro de Laravel es muy similar a la de un Factory. Estas clases se suelen encargar de la creación de objetos complejos que comparten una misma interfaz. En este escenario, todos implementan la misma interfaz \Illuminate\Contracts\Broadcasting\Broadcaster.

Ahora que sabemos para qué sirve esta clase, hablemos sobre su razón de cambio. ¿Cuándo deberíamos tener la necesidad de cambiar esta clase? ¡Exacto! Cuando se necesite agregar una nueva implementación de \Illuminate\Contracts\Broadcasting\Broadcaster al sistema. Como hemos notado que routes() no tiene que ver con eso, este método tiene una baja cohesión en la clase.

Debido a que tenemos al método routes() en la clase, tenemos una nueva razón de cambio. Cada vez que cambien las API del router (group() y match()), nos veremos obligados a venir a este archivo y cambiarlo también. Esto pasa porque nos hemos acoplado al router para lograr esta funcionalidad.

¿Y cómo podríamos seguir el principio SRP y resolver este caso?

Primero hay que identificar qué entidad es la encargada de orquestar la relación entre módulos. ¿Alguna idea? Si pensaste en el Contenedor de Dependencias, estás en lo correcto. Pero no podemos ir directamente al contenedor de dependencias y agregar este código. En Laravel tenemos unos componentes llamados Service Providers que se encargan de nutrir al Contenedor de dependencias.

Cada paquete / módulo de Laravel tiene su propio Service Provider con las configuraciones de las instancias que expone el paquete. Este paquete también tiene su respectivo provider llamado Illuminate\Broadcasting\BroadcastServiceProvider y es ahí donde pondría este código. Pero ya no se podría recibir los $attributes por un método. Tendríamos que cambiar esto para que los atributos sean leídos de otra forma, por ejemplo, a través de un archivo de configuración.

Observación

Hay muchas formas de interpretar el SRP y aquí he expuesto la forma en que he aprendido a verlo. También he dado una propuesta de solución pero no significa que sea la única forma de hacerlo. Estoy seguro de que hay muchas formas creativas y mantenibles de resolver este caso. Incluso dejarlo tal y como está está bien. El SRP es una guía, no es una ley.

Conclusión

La definición del SRP puede ser simple, pero sus implicancias hacen que nos cuestionemos la ubicación de nuestro código. Podríamos resolver este principio extrayendo funcionalidad en un método, en una clase nueva, en un módulo nuevo, etc.

Los invito a cuestionar estas ideas y construir software que cada vez sea más mantenible y con mejor calidad.