Imagen de cabecera

De RoboGuice a Dagger 2 – Parte 3

Dependencias cíclicas

Esta es la tercera parte del tutorial de migración de RoboGuice a Dagger 2. Puedes leerla sin necesidad de haber leído las anteriores, pero aquí tienes el enlace a las otras dos partes:

Dependencias cíclicas

RoboGuice permite crear dependencias cíclicas, pero en Dagger estas son imposibles de resolver. Una dependencia cíclica sucede cuando, durante la resolución de una instancia inyectada, se produce una inyección de la clase que ha inicializado la inyección.

Crear una dependencia cíclica

Vamos a recrear un ejemplo de dependencia cíclica para entenderlo más fácilmente.

Vamos a crear tres clases: ClaseA, ClaseB y ClaseC.

Ya has visto dónde está la dependencia cíclica, ¿verdad? ClaseA tiene una instancia inyectada de ClaseB, ClaseB tiene una instancia inyectada de ClaseC y ClaseC tiene una instancia inyectada de ClaseA. Cuando se intente inyectar cualquiera de las tres, una inyectará a la otra, y esta a la siguiente y esta a la primera y así para siempre.

Seguimos con el inyector de dependencias y el componente. El módulo no lo ponemos porque estará vacío:

Detección de la dependencia cíclica

Ahora ejecutamos la MainActivity. ¡Esto va a petar pero de manera estrepitosa!

Pero, sin embargo, tenemos suerte porque, antes de que el proyecto arranque, obtenemos este mensaje de error en tiempo de compilación:

Dagger ha podido detectar a tiempo la dependencia cíclica porque todas las clases implicadas se inyectan mediante la anotación @Inject en el constructor. Pero, ¿y si lo complicamos todo un poco más?

Vamos a eliminar la anotación @Inject del constructor de ClaseC y vamos a hacer que esta clase sea inyectable mediante un método provider en el módulo:

Y ahora intentemos volver a arrancar el proyecto. Y ¿qué obtenemos? ¡Vaya, Dagger no detecta la dependencia cíclica! En su lugar, el proyecto arranca y, en el momento en que requerimos la inyección de ClaseA comienza a inyectar sin parar, por lo que la UI se quedará bloqueada y, si tenemos suerte, al cabo de un rato, se produce una StackOverflowException que tiene una pinta tal que así:

Como puedes ver, en la inicialización de ClaseA se intenta inyectar ClaseB; en la inicialización de esta se intenta inyectar ClaseC y en la de esta se intenta inyectar ClaseA y ¡dependencia cíclica servida!

Más complicado

La dependencia cíclica se produce cuando en cualquier parte del código que se ejecuta para resolver una dependencia se acaba inyectando la clase inyectora. Esto no se limita a que una clase tenga un campo inyectado de otra y viceversa. Hay casos muy complicados, pero que suelen darse en los constructores de las clases.

Normalmente, dentro de un método provider lo que hacemos es llamar al constructor de la clase que queremos instanciar. Imagina por ejemplo que, para inyectar una instancia de ClaseA defines un método provider que llama a su constructor y el constructor a su vez llama a 40 métodos y cada uno crea 20 objetos nuevos y cada uno de estos objetos tiene 3 campos inyectados. La dependencia de ClaseA no se resuelve hasta que no termina la ejecución de todo ese código. Y si durante la ejecución de ese código se intenta inyectar la ClaseA en algún sitio, se produce una dependencia cíclica, ya que esta aún no está construida y volvería a comenzar a crearla y así hasta el infinito.

Mira este ejemplo:

Fíjate en que ClaseC ya no tiene un campo inyectado de ClaseA y que no inicializa ClaseA en su constructor. ¿Dónde está la dependencia cíclica? ¿Por qué no se llegaría a ejecutar este código?

Pues la dependencia cíclica se encuentra en el constructor de ClaseA. Este inicializa la inyección, lo que supone que se instancia su campo claseB. En su constructor, ClaseB inicializa ClaseC y llama al método realizarComprobacion(). Este método obtiene una instancia de ClaseA, que todavía no ha terminado de resolverse. Llama nuevamente a su constructor y vuelta a empezar. Y ya no hay escapatoria.

¿Soluciones?

Esto no tiene una solución fija. La solución es eliminar la referencia cíclica. A veces será sencillo y a veces será largo y complejo. Este error está relacionado con la responsabilidad de las clases y/o la arquitectura de capas. Tienes que decidir qué clase no necesita contener una inyección de la otra y quitársela:

  • A veces resulta que una de las clases solo tiene inyectada a la otra pero no la usa. En ese caso, la eliminas y punto.
  • Otras veces basta con pasarle la clase inyectada como parámetro en el constructor en lugar de como campo inyectado, o pasársela solo en la llamada al método que la use.
  • También puede ser que una clase no necesite a la clase inyectada, si no solo el resultado de uno de sus métodos o una de sus propiedades. En ese caso, en lugar de tener la clase inyectada, pásale el resultado en el momento en que lo necesite. Mira este ejemplo:

    Olvídate de lo que haga el método y del resto de la clase. Si te fijas, esta clase solo utiliza UserManager para obtener un shopID y un userAgent. Nos bastaría en este caso con obtener esos valores y pasárselos al método:

    En ocasiones es fácil porque quien llama tiene una instancia de la clase UserManager y en otras tenemos que dar más vueltas, pero es necesario.

  • En ocasiones se debe a una mala separación de la responsabilidad de las clases. Una clase tiene una inyección de otra porque tiene métodos que solo utiliza ella. En ese caso, cambia los métodos de clase. Lo mismo te das cuenta de que la clase inyectada ni siquiera tiene motivos para existir por sí misma.

    Este caso es un ejemplo muy reducido que encontré en el proyecto que se me asignó. Estudiando el código, observé que la clase PatchClientManager contiene la ruta a la base de datos y que utiliza DataBaseManager para varias cosas. Pero DataBaseManager solo utiliza a PatchClientManager para obtener esta ruta. ¿Qué sentido tiene entonces que la ruta esté fuera de la clase DataBaseManager?

    Esto lo solucionamos moviendo la ruta a DataBaseManager:

  • En otras ocasiones, puedes eliminar el campo inyectado y obtenerlo mediante inyección puntual cada vez que lo necesites (con cuidado de no inyectarlo en el constructor, si no, estaremos en las mismas). En este ejemplo, verdaderamente no necesitamos disponer de ClaseA en su constructor. Podemos obtenerla después. Así esto:

    Se podría escribir así:

  • Siguiendo con el ejemplo anterior, también puedes sustituir la inyección por un Provider<> o un Lazy<>, el cual no obtendrá la instancia hasta que no llames a su método get().

    Recuerda que la diferencia entre Lazy y Provider es que Lazy no inyecta la instancia hasta que no lo solicitas, mientras que Provider hace lo mismo pero, además, fuerza que la instancia devuelta sea nueva cada vez. Es decir, que si la instancia que quieres inyectar es singleton, usa Lazy, y si quieres una nueva cada vez, usa Provider

0 Comentarios

Contesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

*

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

©2018 Codictados Comunidad libre para el aprendizaje de codigo Online

o

Inicia Sesión con tu Usuario y Contraseña

o    

¿Olvidó sus datos?

o

Create Account