Ir al contenido principal

Errores de diseño con Spring: Autowired en propiedades.

Spring es un excepcional framework de desarrollo en Java. Ofrece una solución elegante y compacta para varios de los principios SOLID de manera casi transparente para un desarrollador. Gracias a Spring, cualquier proyecto Java por muy pequeño que sea, tendrán una buena base con:
  • Inversión de dependencias: Spring controla la creación e inyección de dependencias.
  • Principio de segregación de dependencias: gracias al anterior principio, es extremadamente sencillo definir una interfaz, y spring se encargará de inyectar la dependencia.
  • Principio de sustitución de Liskov: igual que el anterior punto, al ser tan fácil la definición por interfaces; en seguida consegimos esa separación de las interfaces, y sus implementaciones.
Sin embargo no es una herramienta mágica, y es fácil caer en el uso de atajos de Spring que van a complicar el código de manera innecesaria y peligrosa.
Foto by Matt Bango from StockSnap
Uno de los mayores abusos que se ven en el código construido para Spring es la existencia de propiedades de una clase que son dependencias, y que se marcan para que Spring los inyecte directamente con un @Autowired.
@Service
public class ServicioNominasImp implements IServicioNominas {
	@Autowired
    private ServicioFichajes fichajes;
    
    public Double salario(Empleado empleado, Date desde, Date hasta) {
    	double[] horas = fichages.getHorasPorDia(desde, hasta);
        ....
    }

}
¿Que puede estar mal en este código....?: El primer problema que tenemos es que fichajes es una propiedad privada. Si no utilizamos el contexto de Spring nos va a resultar imposible inicializarla y darle valor. Un caso tan sencillo como crear un test:
ServicioNominasImp nominas = new ServicioNominasImp();
AssertEquasl(100, nominas.salario(juan, desde, hasta));
Sufriremos un excepción por Null pointer excepción, ya que nadie inicializa la dependencia de fichajes que tiene nominas. De echo es completamente imposible hacer esa inicialización sin usar el API de reflexión de Java. Aqui no lo comentaremos, pero usar el api de reflexión de Java en un test o en el código de aplicación es una muy mala idea... Con ella se rompe toda la estructuración del código Java, y se pierden las ventajas de la definición de tipos. Podemos pensar en hacer accesible la varibale de fichajes:
@Service
public class ServicioNominasImp implements IServicioNominas {
	@Autowired
    private ServicioFichajes fichajes;
    
    public void setFichajes(ServicioFichajes fichajes) {
    	this.fichajes = fichajes;
    }
    
    public Double salario(Empleado empleado, Date desde, Date hasta) {
    	double[] horas = fichages.getHorasPorDia(desde, hasta);
        ....
    }

}
tras este cambio:
ServicioNominasImp nominas = new ServicioNominasImp();
nominas.setFichajes( servicoFichajes );
AssertEquasl(100, nominas.salario(juan, desde, hasta));
¿Todo correcto ahora? ¿no? Tras el cambio el código ya es funcional, y podemos asignar la dependencia para que sea funcional... Sin embargo hay dos problemas de diseño graves:
  • Tenemos un servicio Java que tiene una dependencia funcional: esto significa que necesita de otro servicio para poder funcionar; y sin embargo podemos construir una instancia sin esa dependencia. Con ello estaríamos creando una instancia de un servicio que no va a funcionar. Oblgamos a poner un aviso de uso que los demás programadores deberán leer: Ojo, para poder usar esta clase, hay que asignar un servicio de fichajes despues de instanciarla... y confiar en que todo el mundo lo lea. De echo, a pesar de incluir el setter, nada impide a un programador escribir el código del primer test (instaciar y llamar a calcular salario) y seguir sufriendo en Null Pointer Exception.
  • Estamos ofreciendo un método para modificar la dependencia del servicio. Aunque lo inicialicemos correctamente en un bloque de código, nada impide a un programador llamar a ese setter para asignarle un valor nulo (y volver a estar como al principio); o aun peor: otra instancia de fichajes cuyo comportamiento sea distinto, y que de repente el programa ofrezca resultados inesperados sin saber porque...
Para evitar este problema, la mejor solución es usar la inyección de dependencias por constructor de Spring:
@Service
public class ServicioNominasImp implements IServicioNominas {
    private transient final ServicioFichajes fichajes;
    
    public ServicioNominasImp(ServicioFichajes fichajes) {
    	super();
    	this.fichajes = fichajes;
    }
    
    public Double salario(Empleado empleado, Date desde, Date hasta) {
    	double[] horas = fichages.getHorasPorDia(desde, hasta);
        ....
    }

}
De esta forma:
  • Añadimos la dependencia en el constructor: cualquiera que quiera usar el servicio debe proporcionar las dependencias para poder instanciarlo. Siempre que recibamos una instancia de ServicioNominas en nuestro código estaremos seguros de que esta lista para ser utilizada sin riesgos.
  • Marcamos la dependencia como final: es imposible modificar la dependencia con fichajes durante la ejecución del programa, con lo que nos aseguramos coherencia en los resultados.
tras ese cambio:
ServicioNominasImp nominas = new ServicioNominasImp( servicioFichajes );
AssertEquasl(100, nominas.salario(juan, desde, hasta));
Podremos tener casos "avanzados" o "extraños" en los que la dependencia debería poder cambiar durante la ejecución del programa... Para esos casos seguramente sea preferible inyectar como dependencia una factoría, hacer que la factoría sea inmutable, y usar la factoría para recuperar las instancias del servicio requerido según el momento.
Al colocar las dependencias en el constructor, podremos encontrarnos fácilmente con un servicio que tiene muchas dependencias y un constructor con gran cantidad de dependencias. Esto seguramente viole las reglas de PMD o de Sonar y aparezca marcado con un aviso... Es correcto, generalmente si tenemos un servicio que tiene muchas dependencias es porque los estamos haciendo mal. Probablemente tengamos un servicio que haga muchas cosas, y que esté rompiendo con le principio de responsabilidad única... Lo ideal sería repensar el servicio y dividirlo; generalmente en estos casos es fácil separar las dependencias con los diferentes métodos del servicio.

Comentarios