Arquitectura de múltiples módulos de Android. De la A a la Z

Hola a todos!

No hace mucho tiempo, nos dimos cuenta de que una aplicación móvil no es solo un cliente ligero, sino una gran cantidad de lógica muy diferente que debe racionalizarse. Es por eso que nos inspiramos en las ideas de la arquitectura Clean, sentimos lo que es DI, aprendimos a usar Dagger 2, y ahora con los ojos cerrados podemos dividir cualquier característica en capas.

Pero el mundo no se detiene, y con la solución de viejos problemas surgen nuevos problemas. Y el nombre de este nuevo problema es monomodularidad. Por lo general, se entera de este problema cuando el tiempo de ensamblaje vuela al espacio. Así es exactamente cuántos informes sobre la transición a la multimodularidad ( uno , dos ) comienzan.
Pero por alguna razón, todos al mismo tiempo de alguna manera olvidan que la monomodularidad afecta fuertemente no solo el tiempo de montaje, sino también su arquitectura. Aquí contesta las preguntas. ¿Qué tan grande es tu AppComponent? ¿Ves periódicamente en el código que la característica A por alguna razón extrae el repositorio de la característica B, aunque no debería parecer así, bueno, o debería ser de alguna manera más de alto nivel? ¿Las características tienen algún tipo de contrato? ¿Y cómo organizas la comunicación entre características? ¿Hay alguna regla?
¿Sientes que hemos resuelto el problema con las capas, es decir, verticalmente todo parece estar bien, pero horizontalmente algo va mal? Y simplemente desglosar los paquetes y controlar la revisión no resuelve el problema.

Y la pregunta de seguridad para los más experimentados. Cuando se mudó a la multimodularidad, ¿no tuvo que palear la mitad de la aplicación, arrastrar siempre el código de un módulo a otro y vivir un período de tiempo decente con un proyecto no ensamblado?

En mi artículo quiero contarte cómo llegué a la multimodularidad precisamente desde un punto de vista arquitectónico. Qué problemas me molestaron y cómo intenté resolverlos por etapas. Y al final, encontrará un algoritmo para cambiar de monomodularidad a multimodularidad sin lágrimas ni dolor.

Respondiendo a la primera pregunta, qué tan grande es el AppComponent, puedo confesar: grande, realmente grande. Y constantemente me atormentaba. Como sucedio En primer lugar, esto se debe a dicha organización DI. Es con DI que comenzaremos.

Como hice DI antes


Creo que muchas personas han formado en sus cabezas algo como este diagrama de las dependencias de los componentes y los ámbitos correspondientes:


Que tenemos aqui


AppComponent , que absorbió absolutamente todas las dependencias con ámbitos Singleton . Creo que casi todos tienen este componente.

FeatureComponents . Cada característica tenía su propio alcance y era un subcomponente de AppComponent o una característica principal.
Detengámonos un poco en las características. En primer lugar, ¿qué es una característica? Lo intentaré con mis propias palabras. Una característica es un módulo de programa máximo lógicamente completo e independiente que resuelve un problema específico del usuario, con dependencias externas claramente definidas, y que es relativamente fácil de usar nuevamente en otro programa. Las características pueden ser grandes y pequeñas. Las características pueden contener otras características. Y también pueden usar o ejecutar otras funciones a través de dependencias externas claramente definidas. Si tomamos nuestra aplicación (Kaspersky Internet Security para Android), entonces las características pueden considerarse antivirus, antirrobo, etc.

Componentes de pantalla . Un componente para una pantalla en particular, también con su propio alcance y también es un subcomponente del componente de función correspondiente.

Ahora una lista de "por qué"


¿Por qué subcomponentes?
En las dependencias de componentes, no me gustó el hecho de que un componente puede depender de varios componentes a la vez, lo que, en mi opinión, podría conducir al caos de los componentes y sus dependencias. Cuando tiene una relación estricta de uno a muchos (un componente y sus subcomponentes), entonces es más seguro y más obvio. Además, por defecto, todas las dependencias del padre están disponibles para el subcomponente, lo que también es más conveniente.

¿Por qué hay un alcance para cada característica?
Porque entonces procedí de las consideraciones de que cada característica es algún tipo de su propio ciclo de vida, que no es el mismo que el de los demás, por lo que es lógico crear su propio alcance. Hay un punto más para muchas cosas mezquinas, que mencionaré a continuación.

Como estamos hablando de Dagger 2 en el contexto de Clean, también mencionaré el momento en que se entregaron las dependencias. Los presentadores, interactuadores, repositorios y otras clases auxiliares de dependencias se suministraron a través del constructor. En las pruebas, sustituimos stubs o moki a través del constructor y probamos en silencio nuestra clase.
El cierre del gráfico de dependencia generalmente ocurre en actividad, fragmentos, a veces receptores y servicios, en general, en los lugares raíz desde los cuales el androide puede comenzar algo. La situación clásica es cuando se crea una actividad para una característica, el componente de la característica comienza y vive en la actividad, y en la característica misma hay tres pantallas que se implementan en tres fragmentos.

Entonces, todo parece ser lógico. Pero como siempre, la vida hace sus propios ajustes.

Problemas de la vida


Tarea de ejemplo


Veamos un ejemplo simple de nuestra aplicación. Tenemos una función de escáner y una función antirrobo. Ambas características tienen un preciado botón Comprar. Además, "Comprar" no es solo enviar una solicitud, sino también mucha lógica diferente relacionada con el proceso de compra. Esto es puramente lógica de negocios con algunos cuadros de diálogo para la compra inmediata. Es decir, hay una característica bastante diferente para sí mismo: la compra. Por lo tanto, en dos características necesitamos usar la tercera característica.
Desde el punto de vista de la interfaz de usuario y la navegación, tenemos la siguiente imagen. Se inicia la pantalla principal, en la que dos botones:


Al hacer clic en estos botones, accedemos a la función del escáner o antirrobo.
Considere la característica del escáner:


Al hacer clic en "Iniciar escaneo antivirus" se realiza algún tipo de trabajo de escaneo, al hacer clic en "Comprarme" solo queremos comprar, es decir, sacamos la función de Compras, pero con "Ayuda" llegamos a una pantalla simple con una ayuda.
La característica de Anti-Theft se ve casi igual.

Posibles soluciones


¿Cómo implementamos este ejemplo en términos de DI? Hay varias opciones

Primera opción


Seleccione una función de compra como un componente independiente que depende solo del AppComponent .


Pero entonces nos enfrentamos con el problema: ¿cómo inyectar dependencias de dos gráficos (componentes) diferentes en una clase a la vez? Solo a través de muletas sucias, que, por supuesto, es tal cosa.

Segunda opción


Seleccionamos la función de compra en el subcomponente, que depende del componente de la aplicación. Y los componentes del escáner y antirrobo pueden convertirse en subcomponentes del componente de compra.


Pero, como comprenderá, puede haber muchas situaciones similares en las aplicaciones. Y esto significa que la profundidad de las dependencias de los componentes puede ser realmente enorme y compleja. Y dicho gráfico será más confuso que hacer que su aplicación sea más coherente y comprensible.

Tercera opción


Seleccionamos la función de compra no en un componente separado, sino en un módulo Dagger separado . Dos formas son posibles aún más.

Primera forma
Agreguemos las características de ámbitos Singleton a todas las dependencias y conectemos al AppComponent .


La opción es popular, pero lleva a AppComponent hinchado. Como resultado, se infla en tamaño, contiene todas las clases de aplicación, y todo el punto de usar Dagger se reduce a la entrega más conveniente de dependencias a las clases, a través de campos o el constructor, y no a través de singletones. En principio, esto es DI, pero echamos de menos los puntos arquitectónicos, y resulta que todos saben de todos.
En general, al comienzo de la ruta, si no sabe dónde atribuir una clase a qué función, es más fácil hacerla global. Esto es bastante común cuando trabajas con Legacy e intentas incorporar al menos algún tipo de arquitectura, además de que todavía no conoces bien todo el código. Y allí, de hecho, los ojos se abren de par en par, y estas acciones están justificadas. El error es que cuando todo está más o menos cerca, nadie quiere abordar este AppComponent .

Segunda forma
Esta es una reducción de todas las funciones a un solo alcance, por ejemplo, PerFeature .


Luego, podemos conectar el módulo Dagger de Shopping a los componentes necesarios de forma fácil y sencilla.
Parece conveniente Pero arquitectónicamente no resulta aislado. Las características del escáner y antirrobo saben absolutamente todo acerca de la función de compra, todos sus despojos. Sin darse cuenta, algo puede estar involucrado. Es decir, la función de compra no tiene una API clara, el límite entre las características es borroso, no hay un contrato claro. Esto es malo Bueno, en multi-modular, el gredloid será difícil más tarde.

Dolor arquitectónico


Honestamente, durante mucho tiempo usé la tercera opción, la primera . Esta fue una medida necesaria cuando comenzamos a transferir gradualmente nuestro legado a los rieles normales. Pero, como mencioné, con este enfoque, sus características comienzan a mezclarse un poco. Todos pueden saber sobre cada uno, sobre los detalles de la implementación y esto para todos. Y AppComponent hinchado indica claramente que hay que hacer algo.
Por cierto, la tercera opción ayudaría con la descarga de AppComponent . La segunda forma . Pero el conocimiento de las implementaciones y las características de mezcla no irán a ninguna parte. Bueno, por supuesto, reutilizar funciones entre aplicaciones sería muy difícil.

Conclusiones intermedias.


Entonces, ¿qué queremos al final? ¿Qué problemas queremos resolver? Vayamos directamente al grano, comenzando desde DI y pasando a la arquitectura:

  • Un conveniente mecanismo DI que le permite usar funciones dentro de otras funciones (en nuestro ejemplo, queremos usar la función Compras dentro del escáner y antirrobo), sin muletas ni dolor.
  • El AppComponent más delgado.
  • Las características no deben tener en cuenta las implementaciones de otras características.
  • Las funciones no deberían ser accesibles de forma predeterminada para nadie, quiero tener algún tipo de mecanismo de control estricto.
  • Es posible dar la función a otra aplicación con un número mínimo de gestos.
  • Una transición lógica a la multimodularidad y las mejores prácticas para esta transición.

Hablé específicamente sobre la multimodularidad solo al final. La alcanzaremos, no nos adelantaremos.

"Vivir de una nueva manera"


Ahora intentaremos implementar gradualmente la lista de deseos anterior.
Vamos!

Mejoras DI


Comencemos con la misma DI.

Rechazo de una gran cantidad de alcances


Como escribí anteriormente, antes de mi enfoque era este: para cada característica, su propio alcance. De hecho, no hay beneficios especiales de esto. Solo obtenga una gran cantidad de ámbitos y una cierta cantidad de dolor de cabeza.
Esta cadena es suficiente: Singleton - PerFeature - PerScreen .

Abandono de subcomponentes en favor de las dependencias de los componentes.


Ya es un punto más interesante. Con los subcomponentes, parece tener una jerarquía más estricta, pero al mismo tiempo tiene las manos atadas por completo y no hay forma de maniobrar de alguna manera. Además, AppComponent conoce todas las características, y también obtienes una gran clase DaggerAppComponent generada.
Con las dependencias de los componentes, obtienes una ventaja genial. En las dependencias de componentes, puede especificar no componentes, sino interfaces limpias (gracias a Denis y Volodya). Gracias a esto, puede sustituir cualquier implementación de interfaz que desee, Dagger se comerá todo. Incluso si el componente con el mismo alcance es esta implementación:
@Component( dependencies = FeatureDependencies.class, modules = FeatureModule.class ) @PerFeature public abstract class FeatureComponent { // ... } public interface FeatureDependencies { SomeDependency someDependency(); } @Component( modules = AnotherFeatureModule.class ) @PerFeature public abstract class AnotherFeatureComponent implements FeatureDependencies { // ... } 


De mejoras DI a mejoras arquitectónicas


Repitamos la definición de características. Una característica es un módulo de programa lógicamente completo e independiente máximo que resuelve un problema específico del usuario, con dependencias externas claramente definidas, y que es relativamente fácil de reutilizar en otro programa. Una de las expresiones clave en la definición de una característica es "con dependencias externas claramente definidas". Por lo tanto, describamos todo lo que queremos del mundo exterior para las características, lo describiremos en una interfaz especial.
Aquí, digamos, la interfaz de dependencia externa de la función Compras:
 public interface PurchaseFeatureDependencies { HttpClientApi httpClient(); } 

O la interfaz de dependencia externa de la función de escáner:
 public interface ScannerFeatureDependencies { DbClientApi dbClient(); HttpClientApi httpClient(); SomeUtils someUtils(); //       PurchaseInteractor purchaseInteractor(); } 

Como ya se mencionó en la sección sobre DI, cualquiera puede implementar las dependencias y, como quiera, estas son interfaces puras y nuestras funciones se liberan de este conocimiento adicional.

Otro componente importante de una característica "pura" es la presencia de una API clara, por la cual el mundo exterior puede acceder a la característica.
Estas son las características de la API de compras:
 public interface PurchaseFeatureApi { PurchaseInteractor purchaseInteractor(); } 

Es decir, el mundo exterior puede obtener un PurchaseInteractor e intentar realizar una compra a través de él. En realidad, arriba vimos que el escáner necesitaba un PurchaseInteractor para completar la compra.

Y aquí están las características de la API del escáner:
 public interface ScannerFeatureApi { ScannerStarter scannerStarter(); } 

E inmediatamente traigo la interfaz y la implementación de ScannerStarter :
 public interface ScannerStarter { void start(Context context); } @PerFeature public class ScannerStarterImpl implements ScannerStarter { @Inject public ScannerStarterImpl() { } @Override public void start(Context context) { Class<?> cls = ScannerActivity.class; Intent intent = new Intent(context, cls); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } } 

Es más interesante aquí. El hecho es que el escáner y el antirrobo son características bastante cerradas y aisladas. En mi ejemplo, estas funciones se lanzan en Actividades separadas, con su propia navegación, etc. Es decir, es suficiente para que simplemente comencemos la Actividad aquí. La actividad muere: la función muere. Puede trabajar según el principio de "Actividad única", y luego a través de las características de la API pasar, digamos, un FragmentManager y alguna devolución de llamada a través de la cual la característica informa que se ha completado. Hay muchas variaciones
También podemos decir que tenemos el derecho de considerar características como Scanner y Anti-Theft como aplicaciones independientes. A diferencia de la característica de la compra, que es una adición de características a algo y por sí misma, de alguna manera no existe particularmente. Sí, es independiente, pero es un complemento lógico para otras características.

Como puede imaginar, debe haber algún punto que conecte las características, su implementación y las características necesarias de la dependencia. Este punto es el componente Daga.
Un ejemplo de un componente de función del escáner:
 @Component(modules = { ScannerFeatureModule.class, ScreenNavigationModule.class // ScannerFeatureDependencies - api    }, dependencies = ScannerFeatureDependencies.class) @PerFeature // ScannerFeatureApi - api   public abstract class ScannerFeatureComponent implements ScannerFeatureApi { private static volatile ScannerFeatureComponent sScannerFeatureComponent; //   public static ScannerFeatureApi initAndGet( ScannerFeatureDependencies scannerFeatureDependencies) { if (sScannerFeatureComponent == null) { synchronized (ScannerFeatureComponent.class) { if (sScannerFeatureComponent == null) { sScannerFeatureComponent = DaggerScannerFeatureComponent.builder() .scannerFeatureDependencies(scannerFeatureDependencies) .build(); } } } return sScannerFeatureComponent; } //           public static ScannerFeatureComponent get() { if (sScannerFeatureComponent == null) { throw new RuntimeException( "You must call 'initAndGet(ScannerFeatureDependenciesComponent scannerFeatureDependenciesComponent)' method" ); } return sScannerFeatureComponent; } //    (   ) public void resetComponent() { sScannerFeatureComponent = null; } public abstract void inject(ScannerActivity scannerActivity); //         Moxy public abstract ScannerScreenComponent scannerScreenComponent(); } 


No creo nada nuevo para ti.

Transición a la multimodularidad


Entonces, usted y yo pudimos definir claramente los límites de la función a través de la API de sus dependencias y la API externa. También descubrimos cómo ponerlo todo en Dagger. Y ahora llegamos al siguiente paso lógico e interesante: la división en módulos.
Abra un caso de prueba de inmediato, será más fácil.
Veamos la imagen en general:

Y mire la estructura del paquete del ejemplo:

Ahora hablemos con cuidado cada elemento.

En primer lugar, vemos cuatro bloques grandes: Aplicación , API , Impl y Utils . En las API , Impl y Utils, puede observar que todos los módulos comienzan en core- o feature- . Hablemos de ellos primero.

Separación en núcleo y característica


Divido todos los módulos en dos categorías: núcleo y función .
En características , como habrás adivinado, nuestras características. En el núcleo, existen cosas como utilidades, trabajar con una red, bases de datos, etc. Pero no hay interfaces de funciones allí. Y el núcleo no es un monolito. Estoy a favor de dividir el módulo central en piezas lógicas y no cargarlo con otras interfaces de funciones.
En el nombre del módulo, primero escriba core o característica . Además en el nombre del módulo hay un nombre lógico ( escáner , red , etc.).

Ahora unos cuatro bloques grandes: Aplicación, API, Impl y Utils.


API
Cada característica o módulo principal se divide en API e Impl . La API contiene una API externa a través de la cual puede acceder a una función o núcleo. Solo esto, y nada más:

Además, el módulo api no sabe nada de nadie, es un módulo absolutamente aislado.

Utils
La única excepción a la regla anterior puede considerarse algunas cosas completamente utilitarias, que no tiene sentido dividir en API e implementación.

Impl
Aquí tenemos una subdivisión en core-impl y feature-impl .
Los módulos en core-impl también son completamente independientes. Su única dependencia es el módulo api . Por ejemplo, eche un vistazo a build.gradle del módulo core-db-impl :
 // bla-bla-bla dependencies { implementation project(':core-db-api') // bla-bla-bla } 

Ahora sobre feature-impl . Ya existe la mayor parte de la lógica de la aplicación. Los módulos del grupo de características implícitas pueden conocer los módulos de la API o el grupo de Utils , pero ciertamente no saben nada sobre los otros módulos del grupo Impl .
Como recordamos, todas las dependencias externas de la característica se acumulan en las dependencias externas. Por ejemplo, para una función de Escaneo, esta API tiene el siguiente aspecto:
 public interface ScannerFeatureDependencies { // core-db-api DbClientApi dbClient(); // core-network-api HttpClientApi httpClient(); // core-utils SomeUtils someUtils(); // feature-purchase-api PurchaseInteractor purchaseInteractor(); } 

En consecuencia, build.gradle feature-scanner-impl será así:
 // bla-bla-bla dependencies { implementation project(':core-utils') implementation project(':core-network-api') implementation project(':core-db-api') implementation project(':feature-purchase-api') implementation project(':feature-scanner-api') // bla-bla-bla } 

Puede preguntar, ¿por qué la API de las dependencias externas no está en el módulo API? El hecho es que este es un detalle de implementación. Es decir, es una implementación particular que necesita algunas dependencias específicas. Para el escáner de API de dependencia está aquí:


Pequeño retiro arquitectónico
Analicemos todo lo anterior y comprendamos por nosotros mismos algunos puntos arquitectónicos con respecto a la característica -...- impl-modules y sus dependencias en otros módulos.
Conocí dos de los patrones de mapeo de dependencias más populares para un módulo:

  • Un módulo puede saber sobre cualquiera. No hay reglas No hay nada que comentar.
  • Los módulos solo saben sobre el módulo central . Y en el módulo central todas las interfaces de todas las características están concentradas. Este enfoque no es muy atractivo para mí, ya que existe el riesgo de convertir el núcleo en otro basurero. Además, si queremos transferir nuestro módulo a otra aplicación, necesitaremos copiar estas interfaces en otra aplicación y también colocarlo en el núcleo . El estúpido copiar y pegar de las interfaces en sí mismo no es muy atractivo y reutilizable en el futuro, cuando las interfaces se pueden actualizar.

En nuestro ejemplo, defiendo el conocimiento de los módulos api y solo api (bueno, utils-groups). Las características no saben nada sobre implementaciones.

Pero resulta que las funciones pueden conocer otras funciones (a través de la API, por supuesto) y ejecutarlas. ¿Podría ser un desastre?
Comentario justo Es difícil elaborar algunas reglas súper claras. Debe haber una medida en todo. Ya hemos tocado este tema un poco más arriba, dividiendo las características en independientes (escáner y antirrobo), completamente independientes y separadas, y características "en contexto", es decir, siempre se lanzan como parte de algo (compras) y generalmente implican lógica de negocios sin ui Es por eso que Scanner y Anti-Theft están al tanto de las compras.
Otro ejemplo. Imagine que en Anti-Theft existe algo así como borrar datos, es decir, borrar absolutamente todos los datos del teléfono. Hay mucha lógica de negocios, ui, está completamente aislada. Por lo tanto, es lógico asignar datos de borrado como una característica separada. Y luego el tenedor. Si los datos de borrado siempre se inician solo desde Anti-Theft y siempre están presentes en Anti-Theft, es lógico que Anti-Theft conozca los datos de borrado y los ejecute por sí mismo. Y el módulo acumulativo, la aplicación, solo sabría sobre Anti-Theft. Pero si los datos de borrado pueden comenzar en otro lugar o no siempre están presentes en Anti-Theft (es decir, pueden ser diferentes en diferentes aplicaciones), entonces es lógico que Anti-theft no conozca esta característica y solo diga algo externo (a través del enrutador, a través de alguna devolución de llamada, no importa) que el usuario haya presionado tal botón y qué lanzar debajo de él ya es asunto del consumidor de la función Antirrobo (aplicación específica, aplicación específica).

También hay una pregunta interesante sobre la transferencia de funciones a otra aplicación. Si, por ejemplo, queremos transferir el escáner a otra aplicación, también debemos transferir además de los módulos : feature-scanner-api y : feature-scanner-impl y los módulos de los que depende el escáner ( : core-utils ,: core-network- api ,: core-db-api ,: feature-adquirir-api ).
Si pero! En primer lugar, todos sus módulos api son completamente independientes y solo hay interfaces y modelos de datos. Sin lógica Y estos módulos están claramente separados lógicamente, y : core-utils suele ser un módulo común para todas las aplicaciones.
En segundo lugar, puede recopilar módulos de api en forma de aar y entregarlos a través de Maven a otra aplicación, o puede conectarlos en forma de un submódulo de concierto. Pero tendrá versiones, habrá control, habrá integridad.
Por lo tanto, la reutilización del módulo (más precisamente, el módulo de implementación) en otra aplicación parece mucho más simple, más claro y más seguro.

Solicitud


Parece que tenemos una imagen delgada y comprensible con características, módulos, sus dependencias y eso es todo. Ahora llegamos a un clímax: esta es una combinación de api y sus implementaciones, sustituyendo todas las dependencias necesarias, etc., pero desde el punto de vista de los módulos de Gredloi. El punto de conexión suele ser la propia aplicación .
Por cierto, en nuestro ejemplo, este punto sigue siendo feature-scanner-example . El enfoque anterior le permite ejecutar cada una de sus funciones como una aplicación separada, lo que ahorra mucho tiempo de compilación durante el desarrollo activo. Belleza!

Para empezar, consideremos cómo sucede todo a través de la aplicación con el ejemplo del ya querido Scanner.
Recupere rápidamente la función:
La API de dependencias externas de Sci es:
 public interface ScannerFeatureDependencies { // core-db-api DbClientApi dbClient(); // core-network-api HttpClientApi httpClient(); // core-utils SomeUtils someUtils(); // feature-purchase-api PurchaseInteractor purchaseInteractor(); } 

Por lo tanto : feature-scanner-impl depende de los siguientes módulos:
 // bla-bla-bla dependencies { implementation project(':core-utils') implementation project(':core-network-api') implementation project(':core-db-api') implementation project(':feature-purchase-api') implementation project(':feature-scanner-api') // bla-bla-bla } 


En base a esto, podemos crear un componente Dagger que implemente una API de dependencias externas:
 @Component(dependencies = { CoreUtilsApi.class, CoreNetworkApi.class, CoreDbApi.class, PurchaseFeatureApi.class }) @PerFeature interface ScannerFeatureDependenciesComponent extends ScannerFeatureDependencies { } 

Coloqué esta interfaz en ScannerFeatureComponent por conveniencia:
 @Component(modules = { ScannerFeatureModule.class, ScreenNavigationModule.class }, dependencies = ScannerFeatureDependencies.class) @PerFeature public abstract class ScannerFeatureComponent implements ScannerFeatureApi { // bla-bla-bla @Component(dependencies = { CoreUtilsApi.class, CoreNetworkApi.class, CoreDbApi.class, PurchaseFeatureApi.class }) @PerFeature interface ScannerFeatureDependenciesComponent extends ScannerFeatureDependencies { } } 


Ahora la aplicación. La aplicación conoce todos los módulos que necesita ( core-, feature-, api, impl ):
 // bla-bla-bla dependencies { implementation project(':core-utils') implementation project(':core-db-api') implementation project(':core-db-impl') implementation project(':core-network-api') implementation project(':core-network-impl') implementation project(':feature-scanner-api') implementation project(':feature-scanner-impl') implementation project(':feature-antitheft-api') implementation project(':feature-antitheft-impl') implementation project(':feature-purchase-api') implementation project(':feature-purchase-impl') // bla-bla-bla } 

A continuación, cree una clase auxiliar. Por ejemplo, FeatureProxyInjector . Ayudará a inicializar correctamente todos los componentes, y es a través de esta clase que pasaremos a las características. Veamos cómo se inicializa el componente de la función del escáner:
 public class FeatureProxyInjector { // another... public static ScannerFeatureApi getFeatureScanner() { return ScannerFeatureComponent.initAndGet( DaggerScannerFeatureComponent_ScannerFeatureDependenciesComponent.builder() .coreDbApi(CoreDbComponent.get()) .coreNetworkApi(CoreNetworkComponent.get()) .coreUtilsApi(CoreUtilsComponent.get()) .purchaseFeatureApi(featurePurchaseGet()) .build() ); } } 

Exteriormente, le damos la interfaz de características ( ScannerFeatureApi ), y en el interior simplemente inicializamos todo el gráfico de dependencia de implementación (a través del método ScannerFeatureComponent.initAndGet (...) ).
DaggerPurchaseComponent_PurchaseFeatureDependenciesComponent es la implementación del componente PurchaseFeatureDependenciesComponent generado por Dagger, del que hablamos anteriormente, donde sustituimos la implementación de api-módulos en el constructor.
Eso es todo magia. Ver el ejemplo nuevamente.

Hablando de ejemplo . Por ejemplo, también debemos satisfacer todas las dependencias externas : feature-scanner-impl . Pero como este es un ejemplo, podemos sustituir las clases ficticias.
Cómo se verá:
 //     ScannerFeatureDependencies public class ScannerFeatureDependenciesFake implements ScannerFeatureDependencies { @Override public DbClientApi dbClient() { return new DbClientFake(); } @Override public HttpClientApi httpClient() { return new HttpClientFake(); } @Override public SomeUtils someUtils() { return CoreUtilsComponent.get().someUtils(); } @Override public PurchaseInteractor purchaseInteractor() { return new PurchaseInteractorFake(); } } //  -  Application-   public class ScannerExampleApplication extends Application { @Override public void onCreate() { super.onCreate(); ScannerFeatureComponent.initAndGet( // ,     =) new ScannerFeatureDependenciesFake() ); } } 

Y la función del escáner en sí, por ejemplo, se inicia a través del manifiesto, para no bloquear la actividad vacía adicional:
 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.scanner_example"> <application android:name=".ScannerExampleApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <!--   --> <activity android:name="com.example.scanner.presentation.view.ScannerActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> 


Algoritmo de transición de monomodularidad a multimodularidad.


La vida es una cosa dura. Y la realidad es que todos trabajamos con Legacy. Si alguien ahora está aserrando un nuevo proyecto, donde puedes bendecir todo inmediatamente, entonces te envidio, hermano. Pero este no es el caso conmigo, y ese tipo también está equivocado =).

¿Cómo traducir su aplicación a varios módulos? Escuché principalmente sobre dos opciones.
El primero Particionando la aplicación aquí y ahora. Es cierto que su proyecto no puede ser ensamblado por un mes o dos =).
Segunda. Intenta sacar las características gradualmente. Pero al mismo tiempo, se extienden todo tipo de dependencias de estas características. Y aquí comienza la diversión. El código de dependencia puede extraer otro código, todo migra al módulo común , al módulo central y viceversa, y así sucesivamente. Como resultado, extraer una característica puede implicar trabajar con otra buena mitad de la aplicación. Y nuevamente, al principio, su proyecto no reunirá un período de tiempo decente.

Abogo por la transferencia gradual de la aplicación a la multimodularidad, ya que en paralelo todavía necesitamos ver nuevas características. La idea clave es que si su módulo necesita algunas de las dependencias, no debe arrastrar este código inmediatamente a los módulos también físicamente . Veamos el algoritmo de eliminación de módulos usando el escáner como ejemplo:

  • Cree funciones de API, colóquelo en un nuevo módulo de API. Es decir, para crear completamente un módulo : feature-scanner-api con todas las interfaces.
  • Crear : característica-escáner-impl . Transfiera físicamente todo el código relacionado con la función a este módulo. De todo lo que depende su función, el estudio lo resaltará de inmediato.
  • Identificar dependencias de características externas. Crea interfaces apropiadas. Estas interfaces se dividen en api-módulos lógicos. Es decir, en nuestro ejemplo, cree los módulos : core-utils ,: core-network-api ,: core-db-api ,: feature-adquirir-api con las interfaces correspondientes.
    Le aconsejo que invierta inmediatamente en el nombre y el significado de los módulos. Está claro que con el tiempo, las interfaces y los módulos se pueden mezclar un poco, colapsar, etc., esto es normal.
  • Cree una API de dependencias externas ( ScannerFeatureDependencies ). Dependiendo : feature-scanner-impl registra módulos api recién creados.
  • Como tenemos todo el legado en la aplicación , esto es lo que hacemos. En la aplicación, conectamos todos los módulos creados para la función (módulo de API de funciones, módulo de impl de funciones, módulos de API de dependencia externa de funciones).
    Punto super importante . A continuación, en la aplicación, creamos implementaciones de todas las interfaces de dependencia de características necesarias (Scanner en nuestro ejemplo). Estas implementaciones probablemente serán solo representantes de las dependencias de la API para la implementación actual de estas dependencias en el proyecto. Al inicializar un componente de característica, sustituya los datos de implementación.
    Difícil en palabras, ¿quieres un ejemplo? ¡Entonces ya lo está! De hecho, algo similar ya existe en feature-scanner-example. Una vez más, le daré un código ligeramente adaptado:
     //     ScannerFeatureDependencies  app- public class ScannerFeatureDependenciesLegacy implements ScannerFeatureDependencies { @Override public DbClientApi dbClient() { return new DbClientLegacy(); } @Override public HttpClientApi httpClient() { // -  // ,      return NetworkFabric.createHttpClientLegacy(); } @Override public SomeUtils someUtils() { return new SomeUtils(); } @Override public PurchaseInteractor purchaseInteractor() { return new PurchaseInteractorLegacy(); } } //  -   ScannerFeatureComponent.initAndGet( new ScannerFeatureDependenciesLegacy() ); 

    Es decir, el mensaje principal aquí es este. Deje que todo el código externo necesario para la función viva en la aplicación , como lo hizo. Y la característica en sí ya funcionará de la manera normal, a través de api (es decir, dependencias de api y módulos de api). En el futuro, la implementación se moverá gradualmente a los módulos. Pero luego evitaremos un juego interminable arrastrando desde el módulo al módulo el código externo necesario para la función. ¡Podemos movernos en iteraciones claras!
  • Ganancia

Aquí hay un algoritmo tan simple pero funcional que le permite avanzar hacia su objetivo paso a paso.

Consejos adicionales


¿Qué tan grandes / pequeñas deberían ser las características?
Todo depende del proyecto, etc. Pero al comienzo de la transición a la multimodularidad, le aconsejo que se divida en piezas grandes. Además, si es necesario, seleccionará más módulos de estos módulos. Pero no triturar.No haga esto: una / varias clases = un módulo.

Pureza del módulo de la aplicación
Al cambiar a la aplicación de varios módulos , tendremos bastante y, a partir de ahí, sus características resaltadas se contraerán. Es posible que durante el trabajo tenga que hacer cambios a este legado, para terminar algo allí, bueno, o simplemente tenga una versión, y no esté a la altura de los cortes en los módulos. En este caso, desea que la aplicación , y con todo el Legacy, conozca las características destacadas solo a través de la API, sin conocimiento de las implementaciones. Pero aplicación , de hecho, se combina con API y impl-ins , pero debido a que la aplicación sabe todo.
En este caso, puede crear un módulo especial: adaptador , que será solo el punto de conexión de api e impl, y luego la aplicación solo sabrá sobre api. Creo que la idea es clara. Puedes ver un ejemplo en la rama clean_app . Agregaré que con Moxy, o más bien MoxyReflector, hay algunos problemas al dividirme en módulos, por lo que tuve que crear otro módulo adicional : stub-moxy-java . Una ligera pizca de magia, sin ella.
La única enmienda. Esto funcionará solo cuando su característica y dependencias relacionadas ya estén transferidas físicamente a otros módulos. Si realizó una función, pero las dependencias aún viven en la aplicación , como en el algoritmo anterior, entonces esto no funcionará.

Epílogo


El artículo resultó bastante grande. Pero espero que realmente te ayude en la lucha contra la monomodularidad, entendiendo cómo debería ser y cómo hacer amigos con DI.
Si está interesado en sumergirse en un problema con la velocidad de construcción, cómo medir todo, le recomiendo informes de Denis Neklyudov y Zhenya Suvorov (Mobius 2018 Piter, los videos aún no están disponibles públicamente).
Sobre Gradle Vova Tagakov demostró perfectamente la diferencia entre api e implementación en gradle . Si desea reducir la repetitiva de múltiples módulos, puede comenzar aquí con este artículo .
Estaré encantado de comentarios, correcciones, así como me gusta! Todo el código limpio!

Source: https://habr.com/ru/post/es422555/


All Articles