Multimodularité et poignard 2. Conférence Yandex

Lorsque votre application est construite sur une architecture multi-modules, vous devez consacrer beaucoup de temps à vous assurer que toutes les communications entre les modules sont correctement écrites dans le code. La moitié de ce travail peut être confiée au framework Dagger 2. Le chef du groupe Yandex.Map pour Android, Vladimir Tagakov Noxa, a parlé des avantages et des inconvénients de la multi- modularité et de l'organisation pratique de la DI à l'intérieur des modules à l'aide de Dagger 2.


- Je m'appelle Vladimir, je développe Yandex.Maps et aujourd'hui je vais vous parler de la modularité et du second Dagger.

J'ai compris la partie la plus longue lorsque je l'ai étudiée moi-même, la plus rapide. La deuxième partie, sur laquelle j'ai siégé pendant plusieurs semaines, je vais vous en parler très rapidement et de manière concise.



Pourquoi avons-nous commencé un processus difficile de division en modules dans Maps? Nous voulions juste augmenter la vitesse de construction, tout le monde le sait.

Le deuxième point de l'objectif est de réduire l'accrochage au code. J'ai pris l'équipement de Wikipédia. Cela signifie que nous voulions réduire les interconnexions entre les modules afin que les modules soient séparés et puissent être utilisés en dehors de l'application. Énoncé initial du problème: les autres projets Yandex devraient pouvoir utiliser une partie des fonctionnalités de Maps exactement comme nous. Et pour développer cette fonctionnalité, nous nous sommes engagés dans le développement du projet.

Je veux lancer une pantoufle brûlante vers [k] apt, ce qui ralentit la vitesse d'assemblage. Je ne le déteste pas beaucoup, mais je l'aime beaucoup. Il me laisse utiliser Dagger.



Le principal inconvénient du processus de séparation des modules est, paradoxalement, le ralentissement de la vitesse d'assemblage. Surtout au tout début, lorsque vous retirez les deux premiers modules, Common et certaines de vos fonctionnalités, la vitesse de construction globale du projet diminue, peu importe comment vous essayez. Au final, comme il reste moins de code dans votre module principal, la vitesse de construction augmentera. Et encore, cela ne signifie pas que tout est très mauvais, il existe des moyens de contourner cela et même de tirer profit du premier module.

Le deuxième inconvénient est qu'il est difficile de séparer le code en modules. Qui a essayé, sait que vous commencez à tirer une sorte de dépendances, quelques classiques, et tout cela finit par copier votre module principal entier dans un autre module et recommencer. Par conséquent, vous devez comprendre clairement le moment où vous devez arrêter et rompre la connexion à l'aide d'une sorte d'abstraction. L'inconvénient est plus d'abstractions. Plus d'abstractions - conception plus complexe - plus d'abstractions.

Il est difficile d'ajouter de nouveaux modules Gradle. Pourquoi? Par exemple, un développeur arrive, prend une nouvelle fonctionnalité en développement, fait immédiatement bien, crée un module séparé. Quel est le problème? Il doit se souvenir de tout le code disponible, qui se trouve dans le module principal, afin que, le cas échéant, le réutiliser et le mettre en commun. Parce que le processus de retrait d'un module en commun est constant jusqu'à ce que votre module d'application principal se transforme en une couche mince.

Modules, modules, modules ... Les modules Gradle, les modules Dagger, les modules d'interface sont horribles.



Le rapport comprendra trois parties: petite, grande et complexe. Tout d'abord, la différence entre l'implémentation et l'API dans AGP. Android Gradle Plugin 3.0 est apparu relativement récemment. Comment était tout avant lui?



Voici un projet typique d'un développeur sain, composé de trois modules: le module App, qui est le principal, est assemblé et installé dans l'application, et deux modules Feature.

Parlez immédiatement des flèches. C'est une grande douleur, tout le monde dessine dans la direction dans laquelle il lui convient de dessiner. Pour moi, cela signifie que depuis Core, il y a une flèche vers Feature. Donc, Feature connaît Core, peut utiliser des classes de Core. Comme vous pouvez le voir, il n'y a pas de flèche entre le Core et l'application, ce qui signifie que l'application ne semble pas utiliser Core. Le noyau n'est pas un module commun, il l'est, tout le monde en dépend, il est séparé, il contient peu de code. Bien que nous n'en tiendrons pas compte.

Notre module principal a changé, nous devons le refaire en quelque sorte. Nous y changeons le code. Couleur jaune - changement de code.



Après avoir remonté le projet. Il est clair qu'après avoir changé un module, il devra être reconstruit, recompilé. Ok



Une fois que le module Feature est également assemblé, cela en dépend. C'est aussi clair, sa dépendance s'est réassemblée, et vous devez vous mettre à jour. Qui sait ce qui a changé là-bas.

Et ici, la chose la plus désagréable se produit. Le module App va, mais on ne sait pas pourquoi. Je sais avec certitude que je n'utilise Core d'aucune façon, et pourquoi l'appli est en cours de reconstruction n'est pas claire. Et il est très grand, car au tout début du chemin, et c'est une très grosse douleur.



De plus, si plusieurs fonctionnalités, de nombreux modules dépendent du Core, alors le monde entier sera remonté, cela prend très longtemps.

Passons à la nouvelle version d'AGP et remplaçons, comme le dit le manuel, tous compilent avec l'API, et non avec l'implémentation, comme vous le pensiez. Rien ne change. Les schémas sont identiques. Quelle est la nouvelle façon de spécifier les dépendances d'implémentation? Imaginez le même schéma en utilisant uniquement ce mot-clé, sans API? Cela ressemblera à ceci.



Ici, dans l'implémentation, on voit clairement qu'il existe une connexion entre Core et App. Ici, nous pouvons clairement comprendre que nous n'en avons pas besoin, nous voulons nous en débarrasser, alors supprimez-le. Tout devient plus facile.



Maintenant, presque tout va bien, encore plus. Si nous changeons certaines API dans Core, ajoutons une nouvelle classe, une nouvelle méthode publique ou un package privé, Core et Feature seront reconstruits. Si vous modifiez l'implémentation à l'intérieur de la méthode ou ajoutez une méthode privée, alors la reconstruction théorique de Feature ne devrait pas se produire du tout, car rien n'a changé.



Allons plus loin. Il se trouve que beaucoup dépendent de notre Core. Le noyau est probablement une sorte de traitement de données réseau ou utilisateur. Parce que c'est Network, tout change assez souvent, tout est reconstruit, et nous avons eu la même douleur dont nous avons soigneusement fui.
Examinons deux façons de gérer cela.



Nous pouvons uniquement transférer les API de notre module Core vers un module distinct, son API, que nous utilisons. Et dans un module séparé, nous pouvons retirer l'implémentation de ces interfaces.



Vous pouvez regarder la connexion à l'écran. Core Impl ne sera pas disponible pour les fonctionnalités. Autrement dit, il n'y aura aucun lien entre les fonctionnalités et l'implémentation de Core. Et le module, surligné en jaune, ne fournira que des usines qui fourniront une sorte d'implémentation de vos interfaces inconnue de personne.

Après une telle conversion, je tiens à attirer l'attention sur le fait que l'API Core, du fait que le mot clé API est debout, sera disponible de manière transitoire pour toutes les fonctionnalités.



Après ces transformations, nous changeons quelque chose dans l'implémentation que vous faites le plus souvent, et seul le module avec les usines sera reconstruit, il est très léger, petit, vous n'avez même pas à considérer le temps qu'il faut.



Une autre option ne fonctionne pas toujours. Par exemple, s'il s'agit d'une sorte de réseau, je peux à peine imaginer comment cela peut se produire, mais s'il s'agit d'une sorte d'écran de connexion utilisateur, alors il se pourrait bien que ce soit le cas.



Nous pouvons créer Sample, le même module racine à part entière qu'App, et ne collecter qu'une seule fonctionnalité, ce sera très rapide et il pourra être développé rapidement de manière itérative. À la fin de la présentation, je vais vous montrer combien de temps il faut pour créer et créer un exemple.

Avec la première partie terminée. Quels modules sont là?



Il existe trois types de modules. Bien entendu, les éléments communs doivent être aussi légers que possible et ne doivent contenir aucune fonctionnalité, mais uniquement les fonctionnalités utilisées par tout le monde. Pour nous, dans notre équipe, cela est particulièrement important. Si nous fournissons nos modules de fonctionnalités à d'autres applications, nous les forcerons à faire glisser Common dans tous les cas. S'il est très gros, personne ne nous aimera.



Si vous avez un projet plus petit, alors avec Common, vous pouvez vous sentir plus détendu, vous n'avez pas non plus à être très zélé.



Le type de module suivant est autonome. Le module le plus courant et intuitif qui contient une fonctionnalité spécifique: une sorte d'écran, une sorte de script utilisateur et ainsi de suite. Elle doit être aussi indépendante que possible, et pour cela, vous pouvez le plus souvent créer un exemple d'application et la développer. L'exemple d'application est très important au début du processus de fractionnement, car tout se construit encore lentement et vous souhaitez obtenir des bénéfices le plus rapidement possible. Au final, quand tout est battu en modules, vous pouvez tout reconstruire, ce sera rapide. Parce qu'il ne sera pas reconstruit une fois de plus.



Modules de célébrités. J'ai moi-même trouvé le mot. Le fait est qu'il est très célèbre pour tout le monde, et beaucoup dépendent de lui. Le même réseau. J'ai déjà dit, si vous le réassemblez souvent, comment éviter le fait que tout soit remonté de vous. Il existe un autre moyen qui peut être utilisé pour de petits projets pour lesquels cela ne vaut pas la peine de tout donner comme une dépendance distincte, un artefact séparé.



À quoi ça ressemble? Nous répétons que retirer l'API de Celebrity, retirer son implémentation, et maintenant surveiller vos mains, faites attention aux flèches de Feature à Celebrity. Cela arrive. L'API de votre module est devenue Common, l'implémentation y est restée et l'usine qui fournit l'implémentation de cette API est apparue dans votre module principal. Si quelqu'un regardait Mobius, Denis Neklyudov en parlait. Schéma très similaire.

Nous utilisons Dagger dans le projet, nous l'aimons et nous voulions tirer le meilleur parti de cet avantage dans le cadre de différents modules.



Nous voulions que chaque module ait un graphe de dépendances indépendant, un composant racine spécifique à partir duquel vous pouvez faire n'importe quoi, nous voulions avoir notre propre code généré pour chaque module Gradle. Nous ne voulions pas que le code généré se glisse dans le code principal. Nous voulions autant de validation à la compilation que possible. Nous souffrons de [k] apt, au moins nous devrions tirer un profit de ce que Dagger donne. Et avec tout cela, nous ne voulions forcer personne à utiliser Dagger. Ni celui qui implémente le nouveau module de fonctionnalités séparément, ni celui qui le consomme ensuite, ne sont nos collègues qui demandent des fonctionnalités pour eux-mêmes.

Comment organiser un graphe de dépendance distinct dans notre module de fonctionnalités?



Vous pouvez essayer d'utiliser Subcomponent, et cela fonctionnera même. Mais cela a pas mal de défauts. Vous pouvez voir que dans Subcomponent, il n'est pas clair quelles dépendances il utilise depuis Component. Pour comprendre cela, vous devez remonter le projet de manière longue et douloureuse, regarder ce que Dagger jure et l'ajouter.
De plus, les sous-composants sont organisés de manière à obliger les autres à utiliser Dagger, et cela ne fonctionnera pas facilement pour vos clients et vous-même si vous décidez de refuser dans un module.



L'une des choses les plus dégoûtantes est que lorsque vous utilisez Subcomponent, toutes les dépendances sont tirées dans le module principal. La dague est conçue pour que les sous-composants soient générés par une classe incorporée de leurs composants de cadrage, le parent. Peut-être que quelqu'un regardait le code généré et sa taille sur leurs composants générés? Nous avons 20 000 lignes dedans. Étant donné que les sous-composants sont toujours des classes imbriquées pour les composants, il s'avère que les sous-composants des sous-composants sont également imbriqués, et tout le code généré tombe dans le module principal, ce fichier de vingt lignes qui doit être compilé et doit être refactorisé, Studio commence à ralentir - douleur.

Mais il y a une solution. Vous pouvez utiliser uniquement Component.



Dans Dagger, un composant peut spécifier des dépendances. Ceci est montré dans le code et montré dans l'image. Dépendances où vous spécifiez des méthodes de provisionnement, des méthodes de fabrique qui montrent de quelles entités dépend votre composant. Il les veut au moment de la création.
Avant, j'ai toujours pensé que seuls d'autres composants pouvaient être spécifiés dans ces dépendances, et c'est pourquoi - la documentation le dit.



Maintenant, je comprends ce que cela signifie d'utiliser l'interface de composant, mais avant je pensais que c'était juste un composant. En fait, vous devez utiliser une interface composée selon les règles de création d'une interface pour un composant. En bref, approvisionnez simplement les méthodes, lorsque vous n'avez que des getters pour certaines sortes de dépendances. Vous pouvez également trouver un exemple de code dans la documentation de Dagger.



OtherComponent y est également écrit, et cela prête à confusion, car en fait, vous pouvez non seulement y insérer des composants.

Comment aimerions-nous utiliser cette entreprise en réalité?



En réalité, il y a un module Feature, il a un package API visible, situé près de la racine de tous les packages, et il dit qu'il y a un point d'entrée - FeatureActivity. Il n'est pas nécessaire d'utiliser des typealias, juste pour être clair. Ça peut être un fragment, ça peut être un ViewController - ça n'a pas d'importance. Et il y a ses dépendances, FeatureDeps, où il est indiqué qu'il a besoin d'un contexte, d'un service réseau, de Common, d'une sorte de chose que vous voulez obtenir de l'application, et tout client est obligé de le satisfaire. Quand il le fera, tout fonctionnera.



Comment utilisons-nous tout cela dans le module Fonctionnalité? Ici, j'utilise Activity, c'est facultatif. Comme d'habitude, nous créons notre propre composant racine Dagger et utilisons la méthode magique findComponentDependencies, il est très similaire à Dagger pour Android, mais nous ne pouvons pas l'utiliser principalement parce que nous ne voulons pas faire glisser les sous-composants. Sinon, nous pouvons leur retirer toute la logique.

Au début, j'ai essayé de dire comment cela fonctionne, mais vous pouvez le voir dans l'exemple de projet vendredi. Comment cela devrait-il être utilisé par vos clients de bibliothèque dans votre module principal?



Tout d'abord, ce ne sont que des typealias. En fait, il a un nom différent, mais pour être bref, il l'est. La classe d'interface MapOfDepth by Dependency vous donne son implémentation. Dans l'application, nous disons que nous pouvons créer des dépendances de la même manière que dans Dagger pour Android, et il est très important que le composant hérite de cette interface et reçoive automatiquement les méthodes de provisionnement. La dague de ce moment commence à nous forcer à fournir cette dépendance. Tant que vous ne l'avez pas fourni, il ne sera pas compilé. C'est la principale commodité: vous avez décidé d'arranger une fonctionnalité, développé votre composant avec cette interface - tout jusqu'à ce que vous fassiez le reste, il ne se contentera pas de compiler, mais produira des messages d'erreur clairs. Le module est simple, le fait est qu'il lie votre composant à l'implémentation de l'interface. À peu près les mêmes que dans Dagger pour Android.

Passons aux résultats.



J'ai vérifié sur notre ordinateur central et sur mon ordinateur portable local, avant de désactiver tout ce qui était possible. Si nous ajoutons une méthode publique à Feature, le temps de construction est considérablement différent. Ici, je montre les différences lorsque je crée un exemple de projet. Cela fait 16 secondes. Ou quand je récupère toutes les cartes - cela signifie deux minutes pour s'asseoir et attendre chaque changement, même minime. Par conséquent, de nombreuses fonctionnalités que nous développons et développerons dans des exemples de projets. Sur le mainframe, le temps est comparable.



Un autre résultat important. Avant de mettre en évidence le module Feature, il ressemblait à ceci: sur le mainframe, c'était 28 secondes, maintenant c'est 49 secondes. Nous avons alloué le premier module, et déjà reçu une décélération de montage presque deux fois.



Et une autre option est un simple assemblage incrémental de notre module, pas une fonctionnalité, comme dans le précédent. 28 secondes se sont écoulées jusqu'à l'allocation du module. Lorsque nous avons alloué un code qui n'avait pas besoin d'être reconstruit à chaque fois, et [k] apt, qui n'avait pas besoin d'être exécuté à chaque fois, nous avons gagné trois secondes. Dieu sait quoi, mais j'espère qu'à chaque nouveau module, le temps ne fera que diminuer.

Voici des liens utiles vers des articles: API versus implémentation , article avec des mesures de temps de construction , exemple de module . La présentation sera disponible . Je vous remercie

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


All Articles