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.
Muy bueno, no conocia las reglas “SOLID”, tenía claro lo de no hacer todo en una clase pero no lo conocía de esta manera. Esperando que los hagas todos y más del D (Dependencias) que nunca he tenido un ejemplo muy claro. Un saludo.
¡Gracias! Me alegro de que te sean de utilidad. Al final es poner en palabras lo que en el día solemos aplicar con la lógica. Pero tenerlos definidos ayuda a que no se nos olviden con tanta facilidad 🙂
Buen artículo, especialmente interesantes las pistas para detectar dónde hace falta aplicar SRP.
Para los que quieran bucear más en SOLID, esta charla de Kevlin Henney es muy interesante porque descompone y explica el por qué de cada principio:
https://yow.eventer.com/yow-2013-1080/the-solid-design-principles-deconstructed-by-kevlin-henney-1386
Lo que más me interesó es el matiz del SRP como realmente “single reason to change” (única razón para cambiar) porque lo que realmente buscamos es aislar los cambios, no las responsabilidades, aunque normalmente las responsabilidades y las razones para hacer cambios van estrechamente ligados.
Interesante! Le echaré un vistazo al vídeo, muchas gracias. Sí, en realidad es el punto fuerte de la definición, pero es incluso más difícil de detectar en mi opinión el hecho de que sólo haya una razón de cambio, así que intentar conseguir que sólo haga una cosa es un aproximación que muchas veces coincide. Al final todo es cuestión de práctica.
Muy buen artículo :), especialmente útil los consejos sobre como detectar una posible necesidad de refactor, gracias!
Me ha gustado mucho el artículo, pero me surge una duda al leer que se puede identificar el error con el número de líneas, ya que se trata de una especificación muy ambigua. ¿Cuál es el máximo?, ¿Cuánto el mínimo?, ¿Porqué el número de líneas es tan importante?
Muchas gracias por las respuestas.
No hay una respuesta exacta a esto, pero cuanto más grandes sean las clases, más difíciles son de leer. Si una clase es corta, tardarás menos en entender qué hace y ser capaz de interactuar con ella o modificarla. Pero sobre todo es importante por lo que se comenta aquí, es un indicio de que con casi toda seguridad esa clase está haciendo demasiadas cosas. Se suele indicar un tope máximo de unas 500 líneas, pero eso ya depende un poco de las reglas que te quieras imponer. Desde luego, si ves una clase de 1000 líneas, eso ya empieza a ser inmanejable.
Te agradezco mucho la respuesta Antonio, ya que no soy programador profesional ni tengo formación académica para ello, pero me encanta crearme aplicaciones y aprender todo lo que puedo y siempre he leído lo de las clases “cortas” pero, gasto ahora, nada específico.
De nada, espero que te haya ayudado a aclararte un poco.
He visto métodos de más de 1000 líneas. Hacen de todo. Abre una conexion a bd, transforma el datatable, actualiza, abre conexion a SFTP, guarda en archivos y manda correo…
Un asco seguir se tipo de programación.
Correcto, cuando todo está en la misma clase, y peor aún, en el mismo método, es imposible entender lo que ocurre para alguien que llega a leer ese código. Tiene muchas más implicaciones, pero esa es la más evidente.
Enhorabuena por el Blog. Los artículos acerca de SOLID son un must aunque los conozcas.
Una cuestión. La barrera entre arte e ingeniería se convierte difusa cuando sólo hay principios pero no leyes formales a la hora de evaluar objetivamente la calidad del código. Sabemos que el código huele mal (smell code) a poco que vemos ciertos antipatrones. Sin embargo, ¿existen herramientas que examinando el código fuente midan factores como el tamaño óptimo, el acoplamiento entre clases, el abuso de Interfaces, etc?
Yo no conozco muchas. En Java se suele utilizar Codestyle, que mediante un fichero de configuración le indicas las reglas que quieres que tu código siga: formato, número de líneas de clases, de métodos… Y seguro que en otros lenguajes hay alternativas similares. También algunos IDEs incluyen herramientas extra. IntelliJ tiene una pestaña Analyze que te da muchas otras funciones. Pero en lo personal, no uso más que eso, puede ser que haya otras.
Otra forma de detectar si violamos SRP: Si no llevas mucho siguiendo SOLID y el nombre de tu clase termina en “Activity” o en “Fragment”, seguro violas el SRP :PP
muy buen post!! salu2!
Bueno, usar “Activity” y “Fragment” en el caso de Android es más un convención. Pero no tiene nada que ver con violar el SRP, no? A no ser que lo hagas todo directamente en la actividad, pero ese es otro tema. Es cierto que debido a cómo funciona el framework, una actividad siempre va a hacer más de una cosa, pero se puede reducir su responsabilidad al mínimo usando una buena arquitectura.
Gracias por el articulo. Muy útil y conciso.
Gracias a ti por leerlo y comentar 🙂
Muy bueno el artículo! Muchas gracias ?.
Felicidades por el blog! Está lleno de temas interesantes ??.
Gracias Vero! Ya sabes que estás invitada a escribir un artículo cuando quieras!