La semana pasada escribía en twitter:
Acabo de escribir un constructor que hace new’s de sus dependencias. Ahí, como los antiguos. Tampoco es tan terrible.
Aparte de por trollear discutir un poco y aprender de la gente tan lista que conozco en twitter, realmente lo escribí porque últimamente empiezo a pensar que la inyección de dependencias está sobrevalorada, especialmente eso en lo que se ha convertido la inyección de dependencias en lenguajes como C#. Y si hablamos de los contenedores de inversión de control, aún peor.
Por eso quiero escribir este post, para reflexionar un poco sobre lo que ganamos al no utilizar inyección de dependencias.
Por supuesto, también perdemos cosas y tenemos que ver cómo de terribles son y si podemos hacer algo al respecto. Como siempre, vamos a intentar analizar esto de forma objetiva y ver hasta dónde llegamos.
Parte de estos argumentos podrían considerarse motivados por un mal uso de la inyección de dependencias, y esto de acuerdo en ello, pero eso no quita que sean problemas frecuentes en bases de código que hacen uso de inyección de dependencias y, por tanto, los considero, cuanto menos, incentivados por ella.
Doy por hecho que todos lo tenemos claro, pero obviamente todo esto depende del contexto, no hay balas de plata, tienes que pensar en la aplicación que estás desarrollando y demás disclaimers habituales.
Lo que ganamos: simplicidad
Todo lo que ganamos gira en torno a un mismo tema: simplicidad. La inyección de dependencias, especialmente la inyección a través de constructor, implica una mayor complejidad que se manifiesta en distintos aspectos.
Por una parte, estamos obligando a los clientes de una clase a saber qué dependencias tiene para poder construirlas. Y no sólo eso. También tiene que conocer las dependencias de sus dependencias. Y las dependencias de las dependencias de sus dependencias… Creo que pilláis la idea.
La inyección de dependencias por contructor tiene un componente viral que hace que pronto sea complicado construir objetos en nuestra aplicación y tengamos que recurrir al uso de contenedores de inversión de control que lo hagan por nosotros. Puesto que ya hablamos de eso hace tiempo, os ahorro la discusión sobre si es bueno o malo usar un contenedor de inversión de control, pero en este caso vamos a quedarnos con que, como poco, incrementa la complejidad de la aplicación.
Además de simplificar la construcción de nuestros objetos, también estamos aumentando el encapsulamiento y la ocultación de información. Al no exponer las dependencias a través del constructor, los clientes de la clase no conocen detalles de la implementación de la clase y el comportamiento queda más encapsulado en ella. Podemos entrar en una discusión semántica sobre si las depedencia se pueden considerar parte del comportamiento y/o de la información de una clase, pero creo que el argumento seguiría siendo válido aunque cambiásemos los nombres.
La contraargumentación típica de esto va ligada a lo que a veces se asocia con el principio de inversión de dependencia. Hay que introducir un interface. Usa inyección de depedencias, pero haz que los clientes no dependan de la clase, sino del interface. Así los clientes no conoces las dependencias de su dependencia y todo queda encapsulado. Por supuesto, para que esto sea manejable, tenemos como antes a nuestro amigo el contenedor de inversión de control. Para ser justos, igual que en el caso anterior, se puede evitar el contenedor y construir el grafo de objetos a mano en algún punto de la aplicación, pero lo habitual suele ser ir por la vía del contenedor.
Esta evolución hacia los interfaces acaba generalmente en otro anti patrón: los header interfaces. Básicamente, acabamos con un montón de interfaces cuya implementación es única en toda la aplicación. Además, esa implementación tiene un API pública que es una copia exacta del interface. Vamos, que aportar, aporta más bien poco, excepto un poco de complejidad adicional al tener un nuevo interface que no sirve para nada, posiblemente otro fichero en disco y hacer algo más incómoda la navegación por el código.
Como decía en la introducción, parte de estos argumentos no pueden achacarse directamente a la inyección de dependencias, sino al abuso, o al mal uso de la misma, pero son escenarios lo bastante frecuentes como para que nos hagan pensar sobre ellos.
Lo que parece que perdemos
Claro, cuando empezamos a utilizar la inyección de dependencias no es que nos hubiéramos vuelto idiotas: tiene sus ventajas. Y, obviamente, esas ventajas son las que perdemos si dejamos de usarlas. Vamos a ver unas cuantas de ellas y si seremos capaces de vivir sin ellas.
Para muchos la ventaja fundamental de la inyección de dependencias es poder escribir tests unitarios. Reconozco que yo fui uno de ellos. Hace unos cuantos años, poder inyectar mocks (o cualquiera de sus amigos) para mi era crítico, porque era lo que me permitía escribir tests unitarios.
Y ojo, que esa es la palabra clave, unitarios. Partía de la base de que los mejores tests son los unitarios, y de que los tests unitarios sólo pueden testear una clase. Con el tiempo mi opinión cambió, ya no creo que un test unitario sólo pueda testear una clase y tampoco creo que los tests unitarios sean siempre la mejor opción. Además, existen formas de escribir el código para poder testear la lógica sin depender de dependencias externas.
Aún así, si quieres escribir tests unitarios de una clase que tiene dependencias y que necesitas testear de forma independiente, siempre puedes hacerlo sin viralizar la aplicación entera con inyección de dependencias y añadir un constructor adicional a esa clase con la o las dependencias que necesitas reemplazar en el test. Algo del estilo de inyección de dependencias para pobres o bastarda. Incluso puedes hacer ese constructor protegido y usarlo sólo desde una clase derivada creada exclusivamente para los tests. Si te pone nervioso tocar el código de producción sólo para poder testearlo, piensa que eso es exactamente lo que estabas haciendo cuando pasaste a usar inyección de dependencias sólo para inyectar stubs.
En cualquier caso, para protegernos de esta eventualidad y poder hacer fácil esta refactorización a un constructor que reciba las dependencias, es recomendable instanciar las dependencias en el constructor, en lugar de repartir los new
s por toda la clase y luego tener que perseguirlos. Así, en caso de necesitar inyectar esa dependencia, es una refactorización simple para introducir el parámetro en el constructor.
Instanciar todas las dependencias en el constructor nos ayuda a mitigar otro problema: la pérdida de documentación. Cuando usamos inyección de dependencias, es fácil encontrar todas las dependencias de una clase simplemente mirando la signatura del constructor. Al instanciar todas las dependencias en el constructor conseguimos algo parecido. No es igual de descriptivo, pero resuelve el problema en cierta medida.
Esto nos lleva un problema interesante que algunos mecionastéis en twitter (¡gracias!) y que realmente no es fácil resolver sin utilizar inyección de dependencias: el control del ciclo de vida de los objetos.
Cuando instanciamos las dependencias dentro de una clase podemos controlar el ciclo de vida de la dependencia, pero su creación será siempre posterior a la clase que la contiene. Podemos jugar con ligarlos por completo, es decir, instanciar la dependencia en el constructor, asignarla a un atributo de la clase y dejarla ahí hasta que nuestra clase muera. También podemos utilizarla localmente en un método y dejar que muera al terminar el método. Lo que resulta más complicado es hacer que la dependencia sobreviva a nuestra clase (a no ser que la expongamos a alguien que quiera hacerse cargo de ella) y es aún más complicado compartir dependencias entre clases (a alguna hay que inyectársela).
Si no usamos inyección de dependencias y tenemos que compartir dependencias entre clases la solución más habitual es recurrir a singletons, que dista mucho de ser la opción ideal en ese escenario.
Otro argumento a favor de la inyección de dependencias es que nos permite cambiar el comportamiento de un componente sin necesidad de modificarlo, aprovechando que podemos cambiar las dependencias que le inyectamos. Éste es, sin duda, un aspecto muy importante de esta técnica y, si lo aplicamos correctamente, podemos conseguir componentes poco acoplados entre sí, reutilizables en distintos contextos sin necesidad de modificarlos, con responsabilidades bien definidas… en fin, el sueño de todo clean coder.
¿Qué pasa cuando renunciamos a la inyección de dependencias? ¿Perdemos todo eso? En realidad sí. O no. Da igual. En realidad, la pregunta es, ¿necesitamos todo eso?¿Cuántos sistemas habéis visto en los que se aplica inyección de dependencias, posiblemente con un contenedor, y la configuración es completamente estática? ¿Cuántos sistemas conocéis en los que jamás se modifican las dependencias concretas que recibe cada componente, por mucho que se estén aisladas detrás de un interface? Yo conozco unos cuantos (y he diseñado personalmente muchos de ellos).
Que sea deseable cumplir con esas características, no quiere decir que sea necesario y hay que ser consciente de implica una complejidad adicional que tal vez no merezca la pena. Es similar al caso de abstrear por completo la base de datos “para poder trabajar con diferentes bases de datos” u ocultar el ORM detrás de un montón de interfaces “para poder cambiar de ORM”. ¿Cuántos proyectos necesitan cambiar de ORM o de base de datos? Y en los que así es, ¿realmente es más rentable asumir toda complejidad adicional desde el principio que reescribir la parte que sea necesaria cuando realmente sea necesario?
Donde sí es necesario prestar más atención a estos aspectos es cuando estamos desarrollando un conjunto de componentes cuyo uso escapará a nuestro a control. Si el usuario de nuestros componentes no va a poder modificar su código en el futuro, por ejemplo porque es una librería que vamos a liberar en forma binaria, hacer esa labor de abstracción y flexibilización prematura puede ser importante. Pero siempre buscando el equilibro para que el uso de nuestra librería no imponga una excesiva complejidad en sus clientes a la hora de construir los objetos con los que han de trabajar.
Conclusiones
No quiero convertir esto en un rant contra la inyección de dependencias, ni siquiera contra los contenedores de inversión de control. Lo que pretendo recalcar es que no es práctico utilizar la inyección de dependencias como técnica por defecto para todo. Actualmente parece que new
, más que una palabra reservada, es una palabra prohibida, y sigue siendo una forma perfectamente válida de construir un objeto. No todo tiene que ser inyectado por contenedores mágicos.
Cuando trabajo con lenguajes como Javascript, excepto cuando todavía tengo que padecer AngularJS, no utilizo inyección de dependencias. Al menos, en el sentido que se suele asociar con la inyección de dependencias en lenguajes como C#. No uso un contenedor y no empiezo diseñando cada módulo a partir de una función que recibe un puñado de dependencias.
Puedes modularizar una aplicación en componentes de un tamaño razonable, cada uno de ellos lo más autocontenido posible, y utilizar referencias estáticas (import
/require
) entre ellos. Eso no impide inyectar comportamiento allí donde es necesario, ya sea pasando objetos o funciones a otros objetos o funciones, pero en lugar de ser la norma, es algo que utilizo cuando necesito. No porque sí.
También tengo aplicaciones grandes en las que el uso de inyección de dependencias, con su contenedor de inversión de control, e incluso sus header interfaces, ofrece ventajas importantes. Puedo (y de hecho lo hago) separarlas en varios procesos o ejecutarlas en uno solo. Puedo reutilizar componentes en distintos escenarios, cambiando protocolos de serialización y comunicación de forma transparente. Puedo aprovechar el contenedor y aplicar AOP para determinadas cosas. Aunque tengo que pagar el coste de una complejidad adicional, merece la pena.
Tampoco es necesario que todo un sistema se rija por los mismos principios. Puede haber áreas en las que compense utilizar una técnica, y otras en la que no valga la pena. Podemos tener una aplicación que utiliza inyección de dependencias y un contenedor de inversión de control, pero que para un subsistema determinado emplea una clase que hace fachada y controla todo el subsistema de forma estática.
Lo importante, como ya os podéis imaginar, es mantener la mente abierta y no caer en el error de pensar que siempre compensa hacer las cosas de la misma manera. Estamos muy acostumbrados a hablar sobre eso (balas de platas, martillos y clavos, la herramienta adecuada para cada cosa, etc.), pero a veces nos cuesta llevar esas palabras a la vida real.
Posts relacionados:
- Inyección de dependencias en Javascript (y otros lenguajes dinámicos)
- AngularJS: Servicios, Inyección de Dependencias y Módulos
- Usa property injection para dependencias ambientales