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
.
1 2 3 4 5 6 7 8 9 |
public class ClaseA { @Inject ClaseB mClaseB; @Inject public ClaseA() { DIManager.getComponente().inject(this); } } |
1 2 3 4 5 6 7 8 9 10 |
public class ClaseB { @Inject ClaseC mClaseC; @Inject public ClaseB() { DIManager.getComponente().inject(this); } } |
1 2 3 4 5 6 7 8 9 |
public class ClaseC { @Inject ClaseA mClaseA; @Inject public ClaseC() { DIManager.getComponente().inject(this); } } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class DIManager { private static Componente sComponente; private DIManager(){} public static void init() { sComponente = DaggerComponente.builder() .build(); } public static Componente getComponente() { return sComponente; } } |
1 2 3 4 5 6 7 8 |
@Singleton @Component(modules = Modulo.class) public interface Componente { void inject(MainActivity activity); void inject(ClaseA claseA); void inject(ClaseB claseB); void inject(ClaseC claseC); } |
Detección de la dependencia cíclica
Ahora ejecutamos laMainActivity
. ¡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:
1 2 3 4 5 6 7 8 9 10 11 |
error: Found a dependency cycle: es.codictados.dependencia.ciclica.ClaseA is injected at es.codictados.dependencia.ciclica.ClaseC.mClaseA es.codictados.dependencia.ciclica.ClaseC is injected at es.codictados.dependencia.ciclica.ClaseB.mClaseC es.codictados.dependencia.ciclica.ClaseB is injected at es.codictados.dependencia.ciclica.ClaseA.mClaseB es.codictados.dependencia.ciclica.ClaseA is injected at es.codictados.dependencia.ciclica.MainActivity.mClaseA es.codictados.dependencia.ciclica.MainActivity is injected at es.codictados.dependencia.ciclica.dagger.Componente.inject(es.codictados.dependencia.ciclica.MainActivity) |
@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:
1 2 3 4 5 6 7 8 |
@Module public class Modulo { @Provides ClaseC provideClaseC() { return new ClaseC(); } } |
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í:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
at es.codictados.dependencia.ciclica.ClaseA.(ClaseA.java:13) at es.codictados.dependencia.ciclica.ClaseA_Factory.newClaseA(ClaseA_Factory.java:30) at es.codictados.dependencia.ciclica.dagger.DaggerComponente.getClaseA(DaggerComponente.java:36) at es.codictados.dependencia.ciclica.dagger.DaggerComponente.injectClaseC(DaggerComponente.java:81) at es.codictados.dependencia.ciclica.dagger.DaggerComponente.inject(DaggerComponente.java:61) at es.codictados.dependencia.ciclica.ClaseC.(ClaseC.java:12) at es.codictados.dependencia.ciclica.dagger.Modulo.provideClaseC(Modulo.java:12) at es.codictados.dependencia.ciclica.dagger.Modulo_ProvideClaseCFactory.proxyProvideClaseC(Modulo_ProvideClaseCFactory.java:29) at es.codictados.dependencia.ciclica.dagger.DaggerComponente.injectClaseB(DaggerComponente.java:66) at es.codictados.dependencia.ciclica.dagger.DaggerComponente.inject(DaggerComponente.java:56) at es.codictados.dependencia.ciclica.ClaseB.(ClaseB.java:13) at es.codictados.dependencia.ciclica.ClaseB_Factory.newClaseB(ClaseB_Factory.java:30) at es.codictados.dependencia.ciclica.dagger.DaggerComponente.getClaseB(DaggerComponente.java:32) at es.codictados.dependencia.ciclica.dagger.DaggerComponente.injectClaseA(DaggerComponente.java:71) at es.codictados.dependencia.ciclica.dagger.DaggerComponente.inject(DaggerComponente.java:51) at es.codictados.dependencia.ciclica.ClaseA.(ClaseA.java:13) at es.codictados.dependencia.ciclica.ClaseA_Factory.newClaseA(ClaseA_Factory.java:30) at es.codictados.dependencia.ciclica.dagger.DaggerComponente.getClaseA(DaggerComponente.java:36) at es.codictados.dependencia.ciclica.dagger.DaggerComponente.injectClaseC(DaggerComponente.java:81) at es.codictados.dependencia.ciclica.dagger.DaggerComponente.inject(DaggerComponente.java:61) |
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 deClaseA
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:
1 2 3 4 5 6 7 8 9 10 11 |
public class MainActivity extends AppCompatActivity { @Inject ClaseA claseA; @Override protected onCreate() { super.onCreate(); DIManager.getAppComponent().inject(this); claseA.hacerAlgo("Un dato cualquiera"); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class ClaseA { @Inject ClaseB claseB; private Date fechaLimite; @Inject public ClaseA() { Calendar cal = Calendar.getInstance(); cal.set(Calendar.YEAR, 2000); cal.set(Calendar.MONTH, 1); cal.set(Calendar.DAY, 1); fechaLimite = cal.getTime(); DIManager.getAppComponent().inject(this); } public hacerAlgo(String dato) { claseB.hacerAlgo(dato); } public Date getFechaLimite() { return fechaLimite; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class ClaseB { private ClaseC claseC; @Inject public ClaseB(ClaseC claseC) { this.claseC = claseC; Date fecha = obtenerFechaBaseDatos(); if (fecha != null) { mostrarFecha(claseC.realizarComprobacion(fecha)); } } public void mostrarFecha(boolean esFechaValida) { if (esFechaValida) { System.out.print("La fecha es válida"); [...] // Lógica para mostrar la fecha en algún lado } else { System.out.print("La fecha no es válida"); } public hacerAlgo(String dato) { claseC.hacerOtroAlgoMas(dato); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class ClaseC { @Inject public ClaseC() { } public boolean realizarComprobacion(Date fecha) { return DIManager.getComponent().getClaseA().getFechaLimite().after(fecha); } public void hacerOtroAlgoMas(String dato) { Date fechaLimite = DIManager.getComponent().getClaseA().getFechaLimite(); System.out.print("Se ha recogido el dato " + dato); if (fechaLimite.after(new Date(Calendar.getInstance().getTime()))) { System.out.print("Pero ya está fuera de plazo."); } } } |
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:
12345678910111213141516171819202122232425public class PatchClientManagerImpl {private File databaseDirectory;@Inject UserManager userManager;@Inject DatabaseManager dbManager;protected void hacerAlgoQueMeDaIgualLoQueSea(boolean isConnected) {String workingPath = databaseDirectory + "/work";String summaryFileURL = "";if (isConnected) {final int shopID = userManager.getUserSession().getShopID();if (shopID != 0) {String shopIDstr = String.format(Locale.ENGLISH, "%d", shopID);summaryFileURL = config.getMAG_SUMMARY().replace("#", shopIDstr);}}String userAgent = userManager.getUserAgent();new PatchClientExecutor(databaseDirectory.getAbsolutePath(), workingPath,summaryFileURL, userAgent).update();}});}
UserManager
para obtener unshopID
y unuserAgent
. Nos bastaría en este caso con obtener esos valores y pasárselos al método:1234567891011121314151617181920public class PatchClientManager {private File databaseDirectory;@Inject DatabaseManager dbManager;protected void hacerAlgoMeDaIgualElQue(boolean isConnected, int shopID, String userAgent) {String workingPath = databaseDirectory + "/work";String summaryFileURL = "";if (isConnected && shopID != 0) {String shopIDstr = String.format(Locale.ENGLISH, "%d", shopID);summaryFileURL = config.getMAG_SUMMARY().replace("#", shopIDstr);}new PatchClientExecutor(databaseDirectory.getAbsolutePath(), workingPath,summaryFileURL, userAgent).update();}}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.
123456789101112131415public class DataBaseManager {@Inject PatchClientManager patchClientManager;public DataBaseManager()DIManager.getComponente().inject(this);}public void copiarBaseDatos(File destino) {File dataBasePath = patchClientManager.getDataBasePath();FileUtils.copiarArchivo(dataBasePath, destino);}}1234567891011121314151617181920public class PatchClientManager {private File dataBasePath;@Inject DatabaseManager dbManager;@Inject Context context;public PatchClientManager() {DIManager.getComponente().inject(this);dataBasePath = new File(context.getDatabaseDir()) + "myDataBase.sqlite");}public File getDataBasePath() {return dataBasePath;}public void hacerCopiaSeguridad() {dbManager.copiarBaseDatos(dataBasePath + "/copiaBaseDatos.sqlite");}}
PatchClientManager
contiene la ruta a la base de datos y que utilizaDataBaseManager
para varias cosas. PeroDataBaseManager
solo utiliza aPatchClientManager
para obtener esta ruta. ¿Qué sentido tiene entonces que la ruta esté fuera de la claseDataBaseManager
? Esto lo solucionamos moviendo la ruta aDataBaseManager
:12345678910111213141516171819public class DataBaseManager {private File dataBasePath;@Inject Context context;public DataBaseManager()DIManager.getComponente().inject(this);dataBasePath = new File(context.getDatabaseDir()) + "myDataBase.sqlite");}public File getDataBasePath() {return dataBasePath;}public void copiarBaseDatos(File directorio) {FileUtils.copiarArchivo(dataBasePath, directorio);}}123456789101112public class PatchClientManager {@Inject DatabaseManager dbManager;public PatchClientManager() {DIManager.getComponente().inject(this);}public void hacerCopiaSeguridad() {dbManager.copiarBaseDatos(dbManager.getDataBasePath() "/copiaBaseDatos.sqlite");}} - 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:1234567891011121314151617181920public class ClaseA {@Inject ClaseB claseB;private String etiqueta;@Injectpublic ClaseA() {DIManager.getAppComponent().inject(this);etiqueta = "SYSTEM-H";}// Imagina que este método hace muchas más cosas y es imposible// refactorizarlo a otro lugarpublic String incluirEtiqueta(List datos) {if (datos.isEmpty()) {System.out.print("No hay datos");} else if (// Algunas comprobaciones){claseB.procesaDatos(datos, etiqueta);}}12345678910111213141516171819public class ClaseB {@Inject ClaseA claseA;@Injectpublic ClaseB() {DIManager.getAppComponent().inject(this); // Esto causaría dependencia cíclica,// ya que este método sería llamado en el constructor de ClaseA y volvería a llamarlo.}public void datosRecibidos(List datos) {claseA.incluirEtiqueta(datos);}public void procesarDatos(List datos) {// Hacer algo con los datos}}123456789101112131415161718public class ClaseA {@Inject ClaseB claseB;private String etiqueta;@Injectpublic ClaseA() {DIManager.getAppComponent().inject(this);etiqueta = "SYSTEM-H";}public String incluirEtiqueta(List datos) {if (datos.isEmpty()) {System.out.print("No hay datos");} else if (// Algunas comprobaciones){claseB.procesaDatos(datos, etiqueta);}}123456789101112131415161718public class ClaseB {// Hemos eliminado el campo inyectado@Injectpublic ClaseB() {}public void datosRecibidos(List datos) {DIManager.getAppComponent().getClaseA().incluirEtiqueta(datos); // Obtenemos el campo// solo cuando nos hace falta.}public void procesarDatos(List datos) {// Hacer algo con los datos}} - Siguiendo con el ejemplo anterior, también puedes sustituir la inyección por un
Provider<>
o unLazy<>
, el cual no obtendrá la instancia hasta que no llames a su métodoget()
.12345678910111213141516public class ClaseB {@Inject Lazy<ClaseA> claseA;@Injectpublic ClaseB() {}public void datosRecibidos(List datos) {claseA.get().incluirEtiqueta(datos); // Obtenemos el campo solo cuando nos hace falta.}public void procesarDatos(List datos) {// Hacer algo con los datos}}Lazy
yProvider
es queLazy
no inyecta la instancia hasta que no lo solicitas, mientras queProvider
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, usaLazy
, y si quieres una nueva cada vez, usaProvider