Como hemos visto en el artículo Principios SOLID, el ABC del diseño software el primer principio es el Principio de Responsabilidad Única, o Single Responsibility Principle (SRP). Nos indica que cada módulo (o clase) debe tener una y sólo una responsabilidad y un único motivo de cambio ligado a esa responsabilidad.
En definitiva, nos viene a decir que cada parte del sistema (módulo) debe hacer una única cosa. De esta forma es más fácil que esa única cosa la haga mejor, y de una manera más efectiva, que si tenemos cosas mezcladas en el mismo lugar, es decir, varias responsabilidades. Es como si intentas tener una pelea samurái mientras comes una manzana, !ninguna de las dos cosas saldrá bien!
1. El camino hacía la Responsabilidad Única
Para detectar como auténticos samuráis cuando estamos violando el Principio de Responsabilidad Única podríamos seguir el siguiente guion:
- Contrasta el nombre de la clase con sus atributos y nombres de métodos. ¿Están relacionados?. ¿Tienen sentido?. Hay veces que claramente podemos observar, con ojo de samurái, que el nombre de la clase no tienen nada que ver con el nombre de sus atributos y métodos.
- Contrasta los nombres de los métodos entre ellos. Lo mismo que el punto anterior, pero comparando nombres de métodos para identificar algún error funcional. Cada clase tiene que tener una responsabilidad y, por tanto, una funcionalidad concreta.
- Difícil de entender o modificar. Si leemos una clase y nos resulta complicado, o incoherente, lo más probable es que haya que separar responsabilidades y crear varias clases.
- Mezcla lógica de diferentes capas. En un modelo por capas es responsabilidad de cada capa una funcionalidad específica. Por ejemplo, la capa de acceso a datos de encarga de gestionar la BD de las diferentes entidades, y la capa de presentación se encarga de mostrar en diferentes formatos la información.
Mediante esta lista podemos saber si nuestra clase cumple o no con el principio de responsabilidad única. A continuación veremos un ejemplo para que quede más claro.
2. Ejemplo de implementación
Para entender más fácilmente como aplicar el SRP partiremos de una clase de ejemplo que viole este principio y paso a paso iremos llegando a la solución final. Partiendo de esta clase Samurai:
@Data @NoArgsConstructor @AllArgsConstructor public class Samurai { private long id; private String nombre; private int edad; private String arma; private double pesoArma; private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public Samurai getSamuraiDB(long id) { this.jdbcTemplate.queryForObject( "select nombre, edad from samurai where id = ?", new Object[]{id}, new RowMapper<Samurai>() { public Samurai mapRow(ResultSet rs, int rowNum) throws SQLException { Samurai samurai = new Samurai(); samurai.setNombre(rs.getString("nombre")); samurai.setEdad(rs.getString("edad")); return samurai; } }); void atacar() { System.out.println("El samurai ataca con su espada"); } void defender() { System.out.println("El samurai defiende con su espada"); } void usarArma() { System.out.println("El samurai usa el arma"); } void ponerAPuntoArma() { System.out.println("El samurai pone a punto el arma"); } }
Omitimos la configuración JDBC por simplicidad. Para más información puede consultar la página de JDBC de Spring.
Siguiendo los punto previamente vistos podemos ver que:
Contrasta el nombre de la clase con sus atributos y nombres de métodos
Si contrastamos el nombre de la clase, Samurai, con los atributos, vemos que tres sí tendrían cabida (id, nombre y edad), mientras que los otros dos (arma, pesoArma) parece que no «encajan» en la clase. Si bien es cierto que un Samurai «tiene un» Arma, no se considera que Arma sea una característica de un Samurai. Son objetos distintos.
Por otro lado, si comparamos el nombre de la clase con los métodos ataca y defiende sí parecen tener sentido. Un samurái ataca y defiende. Sin embargo, los métodos usarArma y ponerAPuntoArma tiene más sentido en una entidad independiente que dentro de la clase Samurai. El método getSamuraiDB nos devuelve una instancia de Samurai recuperada de base de datos. Suena un poco raro, ¿verdad? Lo mismo podemos pensar del método setDataSource.
Contrasta los nombres de los métodos entre ellos
Si comparamos los métodos entre ellos, atacar y defender tienen sentido y parece que es algo que pueda hacer un samurái. Sin embargo, getSamuraiDB no parece que sea algo que haga intrínsecamente un samurái, simplemente devuelve una instancias de Samurai, como hemos visto en el punto anterior. Lo mismo podemos decir de los métodos usarArma y ponerAPuntoArma.
El método setDataSource se encarga de inicializar el atributo jdbcTemplate con la configuración del dataSource. Parece que no tiene mucho sentido en objecto de entidad como samurái. Parece que tiene que ver más con la capa de acceso a datos, en vez de la capa de dominio que es en la que se encuentra la clase Samurai. Veremos la solución en el punto 4.
Difícil de entender o modificar.
Al disponer de atributos y métodos de Samurai, Armas y de acceso a datos se hace complicado seguir la lógica de la clase. Es una especie de mezcla que no es fácil de leer. Además no es coherente en cuanto a los datos, ya que hay información de diferentes entidades.
En cuanto a las modificaciones, siempre que haya un cambio de datos de un Samurai (por ejemplo, incluir un campo altura), o de un Arma (por ejemplo, incluir el campo afilada) o un nuevo método de acceso a datos con respecto a Samurais o Armas impartarán directamente en la clase, con lo que las modificaciones serán más frequentes.
Mezcla lógica de diferentes capas
En cuanto a la separación en capas podemos ver que la clase Samurai pertenece a la capa de dominio, ya que trata de representar un samurái del mundo real. En cuanto a los atributos y métodos referidos a arma, es correcto que un samurái tiene un arma, pero se debe considerar como otra clase diferente del dominio. Es conveniente realizar dicha separación.
En cuanto a los métodos setDataSource y getSamuraiDB es claro que no pertenecen a la capa de dominio, sino a la capa de acceso al dato, con lo que se debe sacar a una clase nueva de esa capa. Después, internamente, usará el objecto de dominio Samurai.
3. Refactorizando que es gerundio
Con lo visto previamente en cada uno de los puntos, a continuación refactorizamos el código para adecuarlo al SRP. Por claridad, se muestra el diagrama de clases de cómo quedaría:
La clase Samurai quedaría de la siguiente forma (la misma que tenemos en Principios SOLID, el ABC del diseño software). Se ha creado también una interface Combatiente aplicando el principio ISP:
Combatiente.java
// Interface Combatiente public interface Combatiente { void atacar(); void defender(); }
Samurai.java
// Clase Samurai que implementa la interface previa @Data @NoArgsConstructor @AllArgsConstructor public class Samurai implements Combatiente { private long id; private String nombre; private List<Arma> arma; @Override void atacar() { System.out.println("El samurai ataca con su espada"); } @Override void defender() { System.out.println("El samurai defiende con su espada"); } }
En base al punto 1 y 2, se ha sacado fuera lo referente al Arma, siguiendo el principio ISP (Interface Segregation Principle) creamos una interface llamado Arma y después una implementación de una nueva clase llamada Espada, por ejemplo. También se han renombrado los atributos de la clase:
Arma.java
public interface Arma { void usar(); void ponerAPunto(); }
Espada.java
// Clase Espada que implementa la interface previa @Data @NoArgsConstructor @AllArgsConstructor public class Espada implements Arma { private long id; private double peso; @Override void usar() { System.out.println("El samurai usa el arma"); } @Override void ponerAPunto() { System.out.println("El samurai pone a punto el arma"); } }
Por último se muestra la interfaz que implementa el acceso a datos de un Samurai. Hemos refactorizado y en lugar de usar JdbcTemplate usaremos Spring Data JPA por simplicidad:
SamuraiRepository.java
interface SamuraiRepository extends CrudRepository<Samurai, Long> { }
Sería conveniente crear una clase SamuraiService que usara Samurai Repository y lo expusiese como un servicio. Se omite por simplicidad del código.
4. Conclusión
Como hemos podido comprobar al aplicar el principio SOLID de Principio de Responsabilidad Única (SRP) hemos pasado de una única clase la cual tenía las siguientes desventajas:
- Grande y compleja.
- Poco mantenible.
- Implementación de las pruebas más compleja y pesada.
- Mayor impacto a la hora de implementar o modificar un requisito.
Ahora tenemos un diseño con tres interfaces y dos clases con la responsabilidad de cada una bien definida, separada en capas bien diferenciadas e identificables.