El Principio de responsabilidad única es el primero de los cinco que componen SOLID.
El principio de Responsabilidad Única nos viene a decir que un objeto debe realizar una única cosa. Es muy habitual, si no prestamos atención a esto, que acabemos teniendo clases que tienen varias responsabilidades lógicas a la vez.
Este artículo forma parte de la serie de Principios SOLID:
- Qué son los Principios SOLID
- Principio de Responsabilidad Única
- Principio Open/Closed
- Principio de Sustitución de Liskov
- Principio de Segregación de Interfaces
- Principio de Inversión de Dependencias
Si quieres tenerlo más cómodo, puedes descargarte el contenido en formato PDF y leerlo donde quieras. Te he preparado esta guía de Principios SOLID para ti.
¿Qué es el Principio de Responsabilidad Única?
Si lo has visto por ahí, seguramente hayas una frase similar a esta:
El Principio de Responsabilidad Única nos dice que un módulo tiene una única razón para cambiar
En estos artículos verás que uso de forma indiferente las palabras módulo, entidad o clase. En realidad, cuando hablamos de lenguajes orientados a objetos, esto siempre se refiere a una clase.
A mí esta definición no me gusta mucho, porque lo de “una única razón para cambiar” me suena muy etéreo, y es difícil bajarlo a tierra.
Prefiero decir que el Principio de Responsabilidad Única se cumple cuando nuestra clase solo hace una cosa.
Tampoco es fácil definir qué es “una cosa”, pero ya tenemos herramientas más sencillas para detectarlo.
Por ejemplo, si cuando tienes que explicar el funcionamiento de una clase, dices que “esta clase hace esta cosa Y esta otra”, entonces sospecha.
Pero te lo voy a poner más fácil, te voy a dar unos truquillos para mejorar la detección:
¿Cómo detectar si estamos violando el Principio de Responsabilidad Única?
La respuesta a esta pregunta es bastante subjetiva. Sin necesidad de obsesionarnos con ello, podemos detectar situaciones en las que una clase podría dividirse en varias:
- En una misma clase están involucradas dos capas de la arquitectura: esta puede ser difícil de ver sin experiencia previa. En toda arquitectura, por simple que sea, debería haber una capa de presentación, una de lógica de negocio y otra de persistencia. Si mezclamos responsabilidades de dos capas en una misma clase, será un buen indicio.
- El número de métodos públicos: Si una clase hace muchas cosas, lo más probable es que tenga muchos métodos públicos, y que tengan poco que ver entre ellos. Detecta cómo puedes agruparlos para separarlos en distintas clases. Algunos de los puntos siguientes te pueden ayudar.
- Los métodos que usan cada uno de los campos de esa clase: si tenemos dos campos, y uno de ellos se usa en unos cuantos métodos y otro en otros cuantos, esto puede estar indicando que cada campo con sus correspondientes métodos podrían formar una clase independiente. Normalmente esto estará más difuso y habrá métodos en común, porque seguramente esas dos nuevas clases tendrán que interactuar entre ellas.
Por el número de imports: Si necesitamos importar demasiadas clases para hacer nuestro trabajo, es posible que estemos haciendo trabajo de más. También ayuda fijarse a qué paquetes pertenecen esos imports. Si vemos que se agrupan con facilidad, puede que nos esté avisando de que estamos haciendo cosas muy diferentes.
Nos cuesta testear la clase: si no somos capaces de escribir tests unitarios sobre ella, o no conseguimos el grado de granularidad que nos gustaría, es momento de plantearse dividir la clase en dos.
- Cada vez que escribes una nueva funcionalidad, esa clase se ve afectada: si una clase se modifica a menudo, es porque está involucrada en demasiadas cosas.
- Por el número de líneas: a veces es tan sencillo como eso. Si una clase es demasiado grande, intenta dividirla en clases más manejables.
En general no hay reglas de oro para estar 100% seguros. La práctica te irá haciendo ver cuándo es recomendable que cierto código se mueva a otra clase, pero estos indicios te ayudarán a detectar algunos casos donde tengas dudas.
Ejemplo
Un ejemplo típico es el de un objeto que necesita ser renderizado de alguna forma, por ejemplo imprimiéndose por pantalla. Podríamos tener una clase como esta:
class Vehicle( val wheelCount: Int, val maxSpeed: Int ) { fun print() { println("wheelCount=$wheelCount, maxSpeed=$maxSpeed") } }
Aunque a primera vista puede parecer una clase de lo más razonable, en seguida podemos detectar que estamos mezclando dos conceptos muy diferentes: la lógica de negocio y la lógica de presentación. Este código nos puede dar problemas en muchas situaciones distintas:
- En el caso de que queramos presentar el resultado de distinta manera, necesitamos cambiar una clase que especifica la forma que tienen los datos. Ahora mismo estamos imprimiendo por pantalla, pero imagina que necesitas que se renderice en un HTML. Tanto la estructura (seguramente quieras que la función devuelva el HTML), como la implementación cambiarían completamente.
Si queremos mostrar el mismo dato de dos formas distintas, no tenemos la opción si sólo tenemos un método
print()
.- Para testear esta clase, no podemos hacerlo sin los efectos de lado que suponen el imprimir por consola.
Hay casos como este que se ven muy claros, pero muchas veces los detalles serán más sutiles y probablemente no los detectarás a la primera. No tengas miedo de refactorizar lo que haga falta para que se ajuste a lo que necesites.
Una solución muy simple sería crear una clase que se encargue de imprimir:
class VehiclePrinter { fun print(vehicle: Vehicle) { println( "wheelCount=${vehicle.wheelCount}, " + "maxSpeed=${vehicle.maxSpeed}" ) }
Si necesitases distintas variaciones para presentar la misma clase de forma diferente (por ejemplo, texto plano y HTML), siempre puedes crear una interfaz y crear implementaciones específicas. Pero ese es un tema diferente.
Otro ejemplo que nos podemos encontrar a menudo es el de objetos a los que les añadimos el método save()
. Una vez más, la capa de lógica y la de persistencia deberían permanecer separadas. Seguramente hablaremos mucho de esto en futuros artículos.
Conclusión
El Principio de Responsabilidad Única es una herramienta indispensable para proteger nuestro código frente a cambios, ya que implica que sólo debería haber un motivo por el que modificar una clase.
En la práctica, muchas veces nos encontraremos con que estos límites tendrán más que ver con lo que realmente necesitemos que con complicadas técnicas de disección. Tu código te irá dando pistas según el software evolucione.
¿Crees que lo podrás aplicar a partir de ahora en tu día a día?
En el siguiente artículo hablaré del Principio Open/Closed, el segundo de los principios SOLID.
Y si lo prefieres, puedes descargarte lo que estamos viendo aquí en esta guía de Principios SOLID de forma gratuita.