El Principio de responsabilidad única es el primero de los cinco que componen SOLID. Si no habías oído hablar de ello hasta ahora, las reglas SOLID son el ABC de cualquier desarrollador experto. Son un conjunto de principios que, aplicados correctamente, te ayudarán a escribir software de calidad en cualquier lenguaje de programación orientada a objetos. Gracias a ellos, crearás código que será más fácil de leer, testear y mantener.

Los principios en los que se basa SOLID son los siguientes:

Estos principios son la base de mucha literatura que encontrarás en torno al desarrollo de software: muchas arquitecturas se basan en ellos para proveer flexibilidad, el testing necesita confiar en ellos para poder validar partes de código de forma independiente, y los procesos de refactorización serán mucho más sencillos si se cumplen estas reglas. Así que es muy conveniente que asimiles bien estos conceptos.

Fueron publicados por primera vez por Robert C. Martin, también conocido como Uncle Bob, en su libro Agile Software Development: Principles, Patterns, and Practices. Una persona que te recomiendo seguir, y echarle un vistazo a su blog de vez en cuando.

Principio de Responsabilidad Única

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.

¿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:

public class Vehicle {

    public int getWheelCount() {
        return 4;
    }

    public int getMaxSpeed() {
        return 200;
    }

    @Override public String toString() {
        return "wheelCount=" + getWheelCount() + ", maxSpeed=" + getMaxSpeed();
    }

    public void print() {
        System.out.println(toString());
    }
}

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.

Un solución muy simple sería crear una clase que se encargue de imprimir:

public class VehiclePrinter {
    public void print(Vehicle vehicle){
        System.out.println(vehicle.toString());
    }
}

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.