Architecture multi-modules Android. De A à Z

Bonjour à tous!

Il n'y a pas si longtemps, nous avons réalisé qu'une application mobile n'est pas seulement un client léger, mais un très grand nombre de logiques très différentes qui doivent être rationalisées. C'est pourquoi nous avons été inspirés par les idées de l'architecture propre, ressenti ce qu'est la DI, appris à utiliser Dagger 2, et maintenant, les yeux fermés, nous sommes en mesure de diviser n'importe quelle fonctionnalité en couches.

Mais le monde ne reste pas immobile, et avec la solution des anciens problèmes, de nouveaux viennent. Et le nom de ce nouveau problème est la monomodularité. Vous rencontrez généralement ce problème lorsque le temps d'assemblage s'envole dans l'espace. C'est exactement le nombre de rapports sur la transition vers la multimodularité ( un , deux ) qui commencent.
Mais pour une raison quelconque, tout le monde oublie en même temps que la monomodularité affecte fortement non seulement le temps d'assemblage, mais aussi votre architecture. Répondez ici aux questions. Quelle est la taille de votre AppComponent? Voyez-vous périodiquement dans le code que la fonctionnalité A tire pour quelque raison que ce soit le référentiel de la fonctionnalité B, bien que cela ne devrait pas ressembler à cela, eh bien, ou devrait-il être en quelque sorte plus de haut niveau? Les fonctionnalités ont-elles un type de contrat? Et comment organisez-vous la communication entre les fonctionnalités? Y a-t-il des règles?
Vous pensez que nous avons résolu le problème avec les calques, c'est-à-dire que verticalement tout semble aller bien, mais horizontalement quelque chose ne va pas? Et tout simplement décomposer les paquets et contrôler l'examen ne résout pas le problème.

Et la question de la sécurité pour les plus expérimentés. Lorsque vous êtes passé à la multi-modularité, n’avez-vous pas dû pelleter la moitié de l’application, toujours faire glisser le code d’un module à un autre et vivre une période décente avec un projet non assemblé?

Dans mon article, je veux vous raconter comment je suis arrivé à la multimodularité précisément d'un point de vue architectural. Quels problèmes me dérangeaient et comment j'essayais de les résoudre par étapes. Et à la fin, vous trouverez un algorithme pour passer de la monomodularité à la multimodularité sans larmes et sans douleur.

En répondant à la première question, quelle est la taille de l'AppComponent, je peux l'avouer - gros, vraiment gros. Et cela me tourmentait constamment. Comment est-ce arrivé? Tout d'abord, cela est dû à une telle organisation DI. C'est avec DI que nous allons commencer.

Comme je l'ai fait auparavant


Je pense que beaucoup de gens ont formé dans leur tête quelque chose comme ce diagramme des dépendances des composants et des portées correspondantes:


Qu'avons-nous ici


AppComponent , qui a absorbé absolument toutes les dépendances avec les étendues Singleton . Je pense que presque tout le monde a cette composante.

FeatureComponents . Chaque fonctionnalité avait sa propre portée et était un sous-composant d' AppComponent ou une fonctionnalité senior.
Arrêtons-nous un peu sur les fonctionnalités. Tout d'abord, qu'est-ce qu'une fonctionnalité? Je vais essayer avec mes propres mots. Une fonctionnalité est un module de programme indépendant, complet et logiquement complet, qui résout un problème utilisateur spécifique, avec des dépendances externes clairement définies et qui est relativement facile à réutiliser dans un autre programme. Les fonctionnalités peuvent être grandes et petites. Les fonctionnalités peuvent contenir d'autres fonctionnalités. Ils peuvent également utiliser ou exécuter d'autres fonctionnalités via des dépendances externes clairement définies. Si nous prenons notre application (Kaspersky Internet Security for Android), les fonctionnalités peuvent être considérées comme Anti-Virus, Anti-Theft, etc.

ScreenComponents . Un composant pour un écran particulier, également avec sa propre portée et étant également un sous-composant du composant de fonctionnalité correspondant.

Maintenant une liste de "pourquoi donc"


Pourquoi des sous-composants?
Dans les dépendances de composants, je n'aimais pas le fait qu'un composant puisse dépendre de plusieurs composants à la fois, ce qui, il me semblait, pouvait finalement conduire au chaos des composants et de leurs dépendances. Lorsque vous avez une relation un-à-plusieurs stricte (un composant et ses sous-composants), cela est plus sûr et plus évident. De plus, par défaut, toutes les dépendances du parent sont disponibles pour le sous-composant, ce qui est également plus pratique.

Pourquoi y a-t-il une portée pour chaque fonctionnalité?
Parce qu'alors je suis parti du principe que chaque fonctionnalité est une sorte de son propre cycle de vie, qui n'est pas le même que celui des autres, il est donc logique de créer votre propre portée. Il y a encore un point pour beaucoup de choses avares, que je mentionnerai ci-dessous.

Puisque nous parlons de Dagger 2 dans le contexte de Clean, je mentionnerai également le moment de la livraison des dépendances. Les présentateurs, les interacteurs, les référentiels et d'autres classes auxiliaires de dépendances ont été fournis par le constructeur. Dans les tests, nous remplaçons ensuite les stubs ou moki par le constructeur et testons tranquillement notre classe.
La fermeture du graphique de dépendance se produit généralement dans l'activité, les fragments, parfois les récepteurs et les services, en général, aux endroits racine à partir desquels l'androïde peut démarrer quelque chose. La situation classique est lorsqu'une activité est créée pour une fonction, le composant de fonction démarre et vit dans l'activité, et dans la fonction elle-même, trois écrans sont implémentés en trois fragments.

Donc, tout semble logique. Mais comme toujours, la vie fait ses propres ajustements.

Problèmes de vie


Exemple de tâche


Regardons un exemple simple de notre application. Nous avons une fonction Scanner et une fonction Antivol. Les deux fonctionnalités ont un bouton d'achat précieux. De plus, «acheter» ne signifie pas seulement envoyer une demande, mais aussi beaucoup de logiques différentes liées au processus d'achat. Il s'agit d'une logique purement commerciale avec quelques dialogues pour un achat immédiat. Autrement dit, il existe une fonctionnalité distincte pour lui-même - Achat. Ainsi, dans deux fonctionnalités, nous devons utiliser la troisième fonctionnalité.
Du point de vue de l'interface utilisateur et de la navigation, nous avons l'image suivante. L'écran principal démarre, sur lequel deux boutons:


En cliquant sur ces boutons, nous accédons à la fonction du scanner ou de l'antivol.
Considérez la fonction du scanner:


En cliquant sur "Démarrer l'analyse antivirus", une sorte de travail d'analyse est effectué, en cliquant sur "Achetez-moi", nous voulons juste acheter, c'est-à-dire que nous tirons la fonctionnalité des achats, mais sur "Aide" nous arrivons à un écran simple avec une aide.
La fonction d'Anti-Theft est presque la même.

Solutions potentielles


Comment implémenter cet exemple en termes d'ID? Il existe plusieurs options.

Première option


Sélectionnez une fonction d'achat en tant que composant indépendant qui ne dépend que de l' AppComponent .


Mais alors nous sommes confrontés au problème: comment injecter des dépendances de deux graphes (composants) différents dans une classe à la fois? Seulement à travers des béquilles sales, ce qui, bien sûr, est une telle chose.

Deuxième option


Nous sélectionnons la fonction d'achat dans le sous-composant, qui dépend de l'AppComponent. Et les composants du scanner et de l'antivol peuvent être transformés en sous-composants à partir du composant d'achat.


Mais, comme vous le comprenez, il peut y avoir beaucoup de situations similaires dans les applications. Et cela signifie que la profondeur des dépendances des composants peut être vraiment énorme et complexe. Et un tel graphique sera plus déroutant que de rendre votre application plus cohérente et compréhensible.

Troisième option


Nous sélectionnons la fonction d'achat non pas dans un composant séparé, mais dans un module Dagger distinct . Deux voies sont encore possibles.

Première voie
Ajoutons les fonctionnalités des étendues Singleton à toutes les dépendances et connectons-nous à AppComponent .


L'option est populaire, mais conduit à des ballonnements AppComponent . Par conséquent, il gonfle en taille, contient toutes les classes d'application et l'intérêt d'utiliser Dagger se résume à une livraison plus pratique des dépendances aux classes - via les champs ou le constructeur, et non via les singletones. En principe, c'est DI, mais nous manquons de points architecturaux, et il s'avère que tout le monde sait tout le monde.
En général, au début du chemin, si vous ne savez pas où attribuer une classe à quelle fonctionnalité, il est plus facile de la rendre globale. Ceci est assez courant lorsque vous travaillez avec Legacy et essayez d'apporter au moins une sorte d'architecture, en plus vous ne connaissez pas encore tout le code. Et là, en effet, les yeux s'écarquillent, et ces actions sont justifiées. L'erreur est que lorsque tout est plus ou moins imminent, personne ne veut s'attaquer à cet AppComponent .

Deuxième voie
Il s'agit d'une réduction de toutes les fonctionnalités à une seule étendue, par exemple PerFeature .


Ensuite, nous pouvons connecter le module Dagger du Shopping aux composants nécessaires facilement et simplement.
Cela semble commode. Mais architecturalement, cela ne se révèle pas de manière isolée. Les fonctionnalités du scanner et de l'antivol connaissent absolument tout sur la fonction d'achat, tous ses abats. Par inadvertance, quelque chose peut être impliqué. Autrement dit, la fonction d'achat n'a pas d'API claire, la frontière entre les fonctionnalités est floue, il n'y a pas de contrat clair. C'est mauvais. Eh bien, en multi-modulaire, le gredloid sera difficile plus tard.

Douleur architecturale


Honnêtement, j'ai utilisé pendant longtemps la troisième option, la première . C'était une mesure nécessaire lorsque nous avons commencé à transférer progressivement notre héritage sur des rails normaux. Mais, comme je l'ai mentionné, avec cette approche, vos fonctionnalités commencent à se mélanger un peu. Tout le monde peut connaître chacun, les détails de la mise en œuvre et cela pour tout le monde. Et ballonnement AppComponent a clairement indiqué que quelque chose doit être fait.
Soit dit en passant , la troisième option aiderait au déchargement d' AppComponent . La deuxième façon . Mais la connaissance des implémentations et des fonctionnalités de mixage n'ira nulle part. Eh bien, bien sûr, la réutilisation des fonctionnalités entre les applications serait très difficile.

Conclusions intermédiaires


Alors, que voulons-nous finalement? Quels problèmes voulons-nous résoudre? Allons droit au but, en commençant par DI et en passant à l'architecture:

  • Un mécanisme DI pratique qui vous permet d'utiliser des fonctionnalités dans d'autres fonctionnalités (dans notre exemple, nous voulons utiliser la fonction Shopping dans Scanner et Anti-Theft), sans béquille ni douleur.
  • L'AppComponent le plus fin.
  • Les fonctionnalités ne doivent pas connaître les implémentations d'autres fonctionnalités.
  • Les fonctionnalités ne devraient être accessibles par défaut à personne, je veux avoir une sorte de mécanisme de contrôle strict.
  • Il est possible de donner la fonctionnalité à une autre application avec un nombre minimum de gestes.
  • Une transition logique vers la multi-modularité et les meilleures pratiques pour cette transition.

Je n'ai spécifiquement parlé de multi-modularité qu'à la toute fin. Nous l'atteindrons, nous ne devancerons pas nous-mêmes.

”Vivre d'une nouvelle manière"


Nous allons maintenant essayer d'implémenter progressivement la liste de souhaits ci-dessus.
C'est parti!

Améliorations DI


Commençons par la même DI.

Refus d'un grand nombre de portées


Comme je l'ai écrit ci-dessus, avant mon approche était la suivante: pour chaque fonctionnalité sa propre portée. En fait, cela ne génère aucun bénéfice spécial. Obtenez seulement un grand nombre de portées et une certaine quantité de maux de tête.
Cette chaîne est tout à fait suffisante: Singleton - PerFeature - PerScreen .

Abandon des sous-composants au profit des dépendances des composants


Déjà un point plus intéressant. Avec les sous- composants, vous semblez avoir une hiérarchie plus stricte, mais en même temps, vous avez les mains complètement liées et il n'y a aucun moyen de manœuvrer d'une manière ou d'une autre. De plus, AppComponent connaît toutes les fonctionnalités et vous obtenez également une énorme classe générée DaggerAppComponent .
Avec les dépendances de composants, vous obtenez un avantage super cool. Dans les dépendances de composants, vous pouvez spécifier non pas des composants, mais des interfaces propres (grâce à Denis et Volodya). Grâce à cela, vous pouvez remplacer toutes les implémentations d'interface que vous aimez, Dagger mangera tout. Même si le composant avec la même portée est cette implémentation:
@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 { // ... } 


Des améliorations DI aux améliorations architecturales


Répétons la définition des fonctionnalités. Une fonctionnalité est un module de programme logiquement complet et indépendant au maximum qui résout un problème d'utilisateur spécifique, avec des dépendances externes clairement définies et qui est relativement facile à réutiliser dans un autre programme. L'une des expressions clés de la définition d'une entité est «avec des dépendances externes clairement définies». Par conséquent, décrivons tout ce que nous voulons du monde extérieur pour les fonctionnalités, nous décrirons dans une interface spéciale.
Ici, disons, l'interface de dépendance externe de la fonction Shopping:
 public interface PurchaseFeatureDependencies { HttpClientApi httpClient(); } 

Ou l'interface des dépendances externes de la fonction Scanner:
 public interface ScannerFeatureDependencies { DbClientApi dbClient(); HttpClientApi httpClient(); SomeUtils someUtils(); //       PurchaseInteractor purchaseInteractor(); } 

Comme déjà mentionné dans la section sur DI, les dépendances peuvent être implémentées par n'importe qui et comme vous le souhaitez, ce sont des interfaces pures, et nos fonctionnalités sont libérées de cette connaissance supplémentaire.

Un autre élément important d'une fonctionnalité «pure» est la présence d'une API claire, par laquelle le monde extérieur peut accéder à la fonctionnalité.
Voici les fonctionnalités api de Shopping:
 public interface PurchaseFeatureApi { PurchaseInteractor purchaseInteractor(); } 

Autrement dit, le monde extérieur peut obtenir un PurchaseInteractor et essayer de faire un achat à travers lui. En fait, nous avons vu ci-dessus que le scanner avait besoin d'un PurchaseInteractor pour finaliser l'achat.

Et voici les fonctionnalités api du scanner:
 public interface ScannerFeatureApi { ScannerStarter scannerStarter(); } 

Et immédiatement, j'apporte l'interface et l'implémentation 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); } } 

C'est plus intéressant ici. Le fait est que le scanner et l'antivol sont des fonctionnalités assez fermées et isolées. Dans mon exemple, ces fonctionnalités sont lancées sur des activités distinctes, avec leur propre navigation, etc. Autrement dit, il nous suffit de démarrer l'activité ici. L'activité meurt - la fonction meurt. Vous pouvez travailler sur la base du principe de «l'activité unique», puis, via les fonctionnalités de l'API, passer, disons, un FragmentManager et certains rappels par lesquels la fonctionnalité signale qu'elle est terminée. Il existe de nombreuses variantes.
Nous pouvons également dire que nous avons le droit de considérer des fonctionnalités telles que Scanner et Anti-Theft comme des applications indépendantes. Contrairement à la fonctionnalité de l'achat, qui est un ajout à quelque chose et en soi, elle n'existe pas d'une manière ou d'une autre. Oui, il est indépendant, mais c'est un complément logique à d'autres fonctionnalités.

Comme vous pouvez l'imaginer, il doit y avoir un point qui relie les fonctionnalités, son implémentation et les fonctionnalités nécessaires de la dépendance. Ce point est le composant Dagger.
Un exemple d'un composant de fonctionnalité du scanner:
 @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(); } 


Je pense que rien de nouveau pour toi.

Transition vers la multi-modularité


Ainsi, vous et moi avons pu identifier clairement les limites des fonctionnalités via l'api de ses dépendances et l'api externe. Nous avons également compris comment tout démarrer dans Dagger. Et maintenant, nous arrivons à la prochaine étape logique et intéressante - la division en modules.
Ouvrez un test immédiatement - ce sera plus facile.
Regardons l'image en général:

Et regardez la structure du package de l'exemple:

Voyons maintenant attentivement chaque élément.

Tout d'abord, nous voyons quatre grands blocs: Application , API , Impl et Utils . Dans les API , Impl et Utils, vous remarquerez peut-être que tous les modules démarrent au niveau du noyau ou de la fonctionnalité . Parlons d'abord d'eux.

Séparation en noyau et fonctionnalité


Je divise tous les modules en deux catégories: noyau et fonctionnalité .
En fonction , comme vous l'avez peut-être deviné, nos fonctionnalités. Dans le noyau, il y a des choses comme les utilitaires, travailler avec un réseau, des bases de données, etc. Mais il n'y a pas d'interfaces de fonctionnalités là-bas. Et le noyau n'est pas un monolithe. Je suis pour casser le core-module en morceaux logiques et pour ne pas le charger avec d'autres interfaces de fonctionnalités.
Au nom du module, écrivez d'abord le noyau ou la fonctionnalité . Plus loin dans le nom du module se trouve un nom logique ( scanner , réseau , etc.).

Maintenant environ quatre grands blocs: Application, API, Impl et Utils


API
Chaque fonctionnalité ou module principal est divisé en API et Impl . L' API contient une API externe à travers laquelle vous pouvez accéder à une fonctionnalité ou à un noyau. Seulement cela, et rien de plus:

De plus, le module api ne sait rien de personne, c'est un module absolument isolé.

Utils
La seule exception à la règle ci-dessus peut être considérée comme des choses complètement utilitaires, qu'il n'est pas logique de percer dans l'API et la mise en œuvre.

Impl
Ici, nous avons une subdivision en core-impl et feature-impl .
Les modules de core-impl sont également complètement indépendants. Leur seule dépendance est le module api . Pour un exemple, jetez un œil à build.gradle du module core-db-impl :
 // bla-bla-bla dependencies { implementation project(':core-db-api') // bla-bla-bla } 

Maintenant sur la fonctionnalité-impl . Il y a déjà la part du lion de la logique d'application. Les modules du groupe feature-impl peuvent connaître les modules du groupe API ou Utils , mais ils ne savent certainement rien des autres modules du groupe Impl .
Comme nous nous en souvenons, toutes les dépendances externes de la fonctionnalité sont accumulées dans les dépendances externes. Par exemple, pour une fonctionnalité de Scan, cette API se présente comme suit:
 public interface ScannerFeatureDependencies { // core-db-api DbClientApi dbClient(); // core-network-api HttpClientApi httpClient(); // core-utils SomeUtils someUtils(); // feature-purchase-api PurchaseInteractor purchaseInteractor(); } 

En conséquence, build.gradle feature-scanner-impl sera comme ceci:
 // 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 } 

Vous pouvez vous demander pourquoi l'api des dépendances externes n'est pas dans le module api? Le fait est qu'il s'agit d'un détail de mise en œuvre. Autrement dit, c'est une implémentation particulière qui nécessite des dépendances spécifiques. Pour le scanner d'api de dépendance est ici:


Petite retraite architecturale
Résumons tout ce qui précède et comprenons par nous-mêmes certains points architecturaux concernant la fonctionnalité -...- impl-modules et leurs dépendances avec d'autres modules.
J'ai rencontré deux des modèles de mappage de dépendance les plus populaires pour un module:

  • Un module peut connaître n'importe qui. Il n'y a pas de règles. Il n'y a rien à commenter.
  • Les modules ne connaissent que le module principal . Et dans le module principal, toutes les interfaces de toutes les fonctionnalités sont concentrées. Cette approche n'est pas très attrayante pour moi, car il y a un risque de transformer le cœur en un autre dépotoir. De plus, si nous voulons transférer notre module vers une autre application, nous devrons copier ces interfaces dans une autre application, et également le placer dans le noyau . Le stupide copier-coller des interfaces en soi n'est pas très attrayant et réutilisable à l'avenir, lorsque les interfaces peuvent être mises à jour.

Dans notre exemple, je préconise la connaissance des modules api et seulement des modules api (enfin, utils-groups). Les fonctionnalités ne connaissent rien aux implémentations.

Mais il s'avère que les fonctionnalités peuvent connaître d'autres fonctionnalités (via l'API, bien sûr) et les exécuter. Serait-ce un gâchis?
Bonne remarque. Il est difficile d'élaborer des règles très claires. Il devrait y avoir une mesure dans tout. Nous avons déjà abordé ce problème un peu plus haut, en divisant les fonctionnalités en fonctionnalités indépendantes (Scanner et Anti-Theft) - complètement indépendantes et séparées, et les fonctionnalités «en contexte», c'est-à-dire qu'elles sont toujours lancées dans le cadre de quelque chose (achat) et impliquent généralement une logique métier. sans interface utilisateur. C'est pourquoi le scanner et l'antivol sont conscients des achats.
Un autre exemple. Imaginez que dans Anti-Theft, il existe une chose telle que l'effacement des données, c'est-à-dire la suppression absolue de toutes les données du téléphone. Il y a beaucoup de logique métier, ui, elle est complètement isolée. Par conséquent, il est logique d'allouer des données d'effacement en tant que fonctionnalité distincte. Et puis la fourchette. Si les données d'effacement sont toujours lancées uniquement depuis Anti-Theft et sont toujours présentes dans Anti-Theft, il est logique qu'Anti-Theft connaisse les données d'effacement et les exécute de lui-même. Et le module d'accumulation, l'application, ne connaîtrait alors que l'antivol. Mais si les données d'effacement peuvent démarrer ailleurs ou ne sont pas toujours présentes dans Anti-Theft (c'est-à-dire qu'elles peuvent être différentes dans différentes applications), il est logique qu'Anti-theft ne connaisse pas cette fonctionnalité et dise simplement quelque chose d'extérieur (via le routeur, par le biais de certains rappels, cela n'a pas d'importance) que l'utilisateur ait appuyé sur tel ou tel bouton, et ce qu'il faut lancer est déjà l'affaire du consommateur de la fonction antivol (application spécifique, application spécifique).

Il y a aussi une question intéressante sur le transfert de fonctionnalités vers une autre application. Si, par exemple, nous voulons transférer le scanner vers une autre application, nous devons également transférer en plus des modules : feature-scanner-api et : feature-scanner-impl et les modules dont dépend le scanner ( : core-utils,: core-network- api,: core-db-api,: fonctionnalité-achat-api ).
Oui mais! Tout d'abord, tous vos modules api sont complètement indépendants, et il n'y a que des interfaces et des modèles de données. Pas de logique. Et ces modules sont clairement séparés logiquement, et : core-utils est généralement un module commun à toutes les applications.
Deuxièmement, vous pouvez collecter des api-modules sous la forme d'un aar et les livrer via le maven à une autre application, ou vous pouvez les connecter sous la forme d'un sous-module gig. Mais vous aurez des versions, il y aura du contrôle, il y aura de l'intégrité.
Ainsi, la réutilisation du module (plus précisément, le module d'implémentation) dans une autre application semble beaucoup plus simple, plus claire et plus sûre.

Candidature


Il semble que nous ayons une image élancée et compréhensible avec des fonctionnalités, des modules, leurs dépendances et c'est tout. Nous arrivons maintenant à un point culminant - il s'agit d'une combinaison d'api et de leurs implémentations, remplaçant toutes les dépendances nécessaires, etc., mais du point de vue des modules Gredloi. Le point de connexion est généralement l' application elle-même.
Par ailleurs, dans notre exemple, ce point est toujours un exemple de scanner de fonctionnalités . L'approche ci-dessus vous permet d'exécuter chacune de vos fonctionnalités en tant qu'application distincte, ce qui permet d'économiser considérablement le temps de construction pendant le développement actif. La beauté!

Pour commencer, considérons comment tout se passe dans l' application avec l'exemple du scanner déjà aimé.
Rappelez rapidement la fonctionnalité:
L'API des dépendances externes de Sci est:
 public interface ScannerFeatureDependencies { // core-db-api DbClientApi dbClient(); // core-network-api HttpClientApi httpClient(); // core-utils SomeUtils someUtils(); // feature-purchase-api PurchaseInteractor purchaseInteractor(); } 

Par conséquent : feature-scanner-impl dépend des modules suivants:
 // 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 } 


Sur cette base, nous pouvons créer un composant Dagger qui implémente une API de dépendances externes:
 @Component(dependencies = { CoreUtilsApi.class, CoreNetworkApi.class, CoreDbApi.class, PurchaseFeatureApi.class }) @PerFeature interface ScannerFeatureDependenciesComponent extends ScannerFeatureDependencies { } 

J'ai placé cette interface dans le ScannerFeatureComponent pour plus de commodité:
 @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 { } } 


Maintenant, l'application. L'application connaît tous les modules dont elle a besoin ( 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 } 

Ensuite, créez une classe d'assistance. Par exemple, FeatureProxyInjector . Cela aidera à initialiser correctement tous les composants, et c'est à travers cette classe que nous nous tournerons vers les fonctionnalités. Voyons comment le composant de fonctionnalité Scanner est initialisé:
 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() ); } } 

À l'extérieur, nous donnons l'interface de fonctionnalité ( ScannerFeatureApi ), et à l'intérieur nous initialisons simplement le graphique de dépendance d'implémentation entier (via la méthode ScannerFeatureComponent.initAndGet (...) ).
DaggerPurchaseComponent_PurchaseFeatureDependenciesComponent est l'implémentation du PurchaseFeatureDependenciesComponent généré par Dagger, dont nous avons parlé ci-dessus, où nous substituons l'implémentation d'api-modules dans le générateur.
C’est de la magie. Voir à nouveau l' exemple .

En parlant d' exemple . Par exemple, nous devons également satisfaire toutes les dépendances externes : feature-scanner-impl . Mais comme il s'agit d'un exemple, nous pouvons remplacer les classes factices.
À quoi cela ressemblera:
 //     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() ); } } 

Et la fonction Scanner elle-même dans l' exemple est lancée via le manifeste, afin de ne pas bloquer l'activité vide supplémentaire:
 <?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> 


Algorithme de transition de la monomodularité à la multimodularité


La vie est une chose dure. Et la réalité est que nous travaillons tous avec Legacy. Si quelqu'un scie maintenant un tout nouveau projet, où vous pouvez tout bénir immédiatement, alors je vous envie, mon frère. Mais ce n'est pas le cas avec moi, et ce type a également tort =).

Comment traduire votre application en multi-module? J'ai surtout entendu parler de deux options.
Le premier. Partitionner l'application ici et maintenant. Certes, votre projet peut ne pas être assemblé avant un mois ou deux =).
Deuxième. Essayez de retirer progressivement les fonctionnalités. Mais en même temps, toutes sortes de dépendances de ces fonctionnalités s'étirent. Et ici, le plaisir commence. Le code de dépendance peut extraire un autre code, le tout migre vers le module commun , vers le module principal et vice versa, etc. Par conséquent, tirer une fonctionnalité peut impliquer de travailler avec une autre bonne moitié de l'application. Et encore une fois, au début, votre projet ne réunira pas une période de temps décente.

Je préconise le transfert progressif de l'application vers la multi-modularité, car en parallèle nous avons encore besoin de voir de nouvelles fonctionnalités. L'idée clé est que si votre module a besoin de certaines des dépendances, vous ne devez pas immédiatement faire glisser ce code physiquement dans les modules également . Regardons l'algorithme de suppression de module en utilisant le scanner comme exemple:

  • Créez des fonctionnalités api, placez-le dans un nouveau module api. C'est-à-dire, pour créer entièrement un module : feature-scanner-api avec toutes les interfaces.
  • Créer : feature-scanner-impl . Transférez physiquement tout le code lié à la fonctionnalité à ce module. Tout ce dont dépend votre fonctionnalité, le studio le mettra immédiatement en évidence.
  • Identifiez les dépendances des fonctionnalités externes. Créez des interfaces appropriées. Ces interfaces sont divisées en api-modules logiques. C'est-à-dire, dans notre exemple, créer les modules : core-utils,: core-network-api,: core-db-api,: feature-purchase-api avec les interfaces correspondantes.
    Je vous conseille d'investir immédiatement dans le nom et la signification des modules. Il est clair qu'avec le temps, les interfaces et les modules peuvent être un peu mélangés, effondrés, etc., c'est normal.
  • Créez une API de dépendances externes ( ScannerFeatureDependencies ). Selon : feature-scanner-impl enregistre les modules api nouvellement créés.
  • Puisque nous avons tout l'héritage dans l' application , voici ce que nous faisons. Dans l' application, nous connectons tous les modules créés pour la fonctionnalité (module api de fonctionnalité, module implément de fonctionnalité, modules api de dépendance externe).
    Point super important . Ensuite, dans l' application, nous créons des implémentations de toutes les interfaces de dépendance de fonctionnalités nécessaires (Scanner dans notre exemple). Ces implémentations ne seront probablement que des proxys de vos dépendances api à l'implémentation actuelle de ces dépendances dans le projet. Lors de l'initialisation d'un composant de fonctionnalité, remplacez les données d'implémentation.
    Difficile en mots, vous voulez un exemple? Alors il l'est déjà! En fait, quelque chose de similaire existe déjà dans feature-scanner-example. Encore une fois, je vais lui donner un code légèrement adapté:
     //     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() ); 

    Autrement dit, le message principal ici est le suivant. Laissez tout le code externe nécessaire à la fonctionnalité vivre dans l' application , comme il l'a fait. Et la fonctionnalité elle-même fonctionnera déjà avec elle de manière normale, via api (c'est-à-dire les dépendances api et les modules api). À l'avenir, la mise en œuvre passera progressivement aux modules. Mais alors nous éviterons un jeu sans fin en faisant glisser du module vers le module le code externe nécessaire pour la fonctionnalité. On peut évoluer en itérations claires!
  • Bénéfice

Voici un algorithme simple mais efficace qui vous permet d'avancer pas à pas vers votre objectif.

Conseils supplémentaires


Quelle doit être la taille / la taille des fonctionnalités?
Tout dépend du projet, etc. Mais au début de la transition vers la multi-modularité, je recommande de le diviser en gros morceaux. De plus, si nécessaire, vous sélectionnerez plus de modules parmi ces modules. Mais ne moudre pas. Ne faites pas cela: une / plusieurs classes = un module.

Module d'application propre
Lorsque vous passez à la multi- modularité , l' application sera assez grande et à partir de là, vos fonctionnalités mises en évidence se contracteront. Il est possible que pendant le travail, vous deviez apporter des modifications à cet héritage, pour terminer quelque chose là, eh bien, ou que vous ayez juste une version, et que vous ne soyez pas à la hauteur des coupes en modules. Dans ce cas, vous voulez que l' application , et avec elle tout l'héritage, ne connaisse les fonctionnalités mises en évidence que via l'api, aucune connaissance des implémentations. Mais l' application , en fait, moissonneuses - batteuses api- et impl-ins , mais parce que l' application sait tout.
Dans ce cas, vous pouvez créer un module spécial : adaptateur , qui sera juste le point de connexion de l'api et de l'impl, mais déjàl'application ne connaîtra alors que l'api. Je pense que l'idée est claire. Vous pouvez voir un exemple dans la branche clean_app . J'ajouterai qu'avec Moxy, ou plutôt MoxyReflector, il y a quelques problèmes lors de la division en modules, à cause desquels j'ai dû créer un autre module supplémentaire : stub-moxy-java . Une légère pincée de magie, sans elle.
Le seul amendement. Cela ne fonctionnera que si votre fonctionnalité et les dépendances associées sont déjà physiquement transférées vers d'autres modules. Si vous avez créé une fonctionnalité, mais que les dépendances existent toujours dans l' application , comme dans l'algorithme ci-dessus, cela ne fonctionnera pas.

Postface


L'article s'est avéré assez volumineux. Mais j'espère que cela vous aidera vraiment dans la lutte contre la monomodularité, en comprenant comment cela devrait être et comment se lier d'amitié avec DI.
Si vous êtes intéressé à plonger dans un problème de vitesse de construction, comment tout mesurer, alors je recommande les rapports de Denis Neklyudov et Zhenya Suvorov (Mobius 2018 Piter, les vidéos ne sont pas encore accessibles au public).
À propos de Gradle. La différence entre l'API et la mise en œuvre dans Gradle a été parfaitement montrée par Vova Tagakov . Si vous souhaitez réduire le passe-partout multi-modules, vous pouvez commencer ici avec cet article .
Je me ferai un plaisir de faire des commentaires, des corrections, ainsi que des likes! Tout code propre!

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


All Articles