Wenn Ihre Anwendung auf einer Architektur mit mehreren Modulen basiert, müssen Sie viel Zeit darauf verwenden, sicherzustellen, dass die gesamte Kommunikation zwischen den Modulen korrekt in den Code geschrieben ist. Die Hälfte dieser Arbeit kann dem Dagger 2-Framework anvertraut werden. Der Leiter der Yandex.Map-Gruppe für Android Vladimir Tagakov
Noxa sprach über die Vor- und Nachteile der Multimodularität und der bequemen Organisation von DI in Modulen mit Dagger 2.
- Mein Name ist Vladimir, ich entwickle Yandex.Maps und heute werde ich Ihnen von der Modularität und dem zweiten Dolch erzählen.
Ich habe den längsten Teil verstanden, als ich ihn selbst studiert habe, den schnellsten. Den zweiten Teil, über den ich mehrere Wochen gesessen habe, werde ich Ihnen sehr schnell und präzise erzählen.

Warum haben wir einen schwierigen Prozess der Aufteilung in Module in Maps gestartet? Wir wollten nur die Build-Geschwindigkeit erhöhen, jeder weiß davon.
Der zweite Punkt des Ziels besteht darin, das Code-Hooking zu reduzieren. Ich habe die Ausrüstung von Wikipedia genommen. Dies bedeutet, dass wir die Verbindungen zwischen den Modulen reduzieren wollten, damit die Module getrennt sind und außerhalb der Anwendung verwendet werden können. Erste Erklärung des Problems: Andere Yandex-Projekte sollten in der Lage sein, einen Teil der Funktionalität von Maps genau wie wir zu nutzen. Und um diese Funktionalität zu entwickeln, sind wir an der Entwicklung des Projekts beteiligt.
Ich möchte einen brennenden Pantoffel in Richtung [k] apt werfen, was die Montagegeschwindigkeit verlangsamt. Ich hasse ihn nicht sehr, aber ich liebe ihn sehr. Er lässt mich Dolch benutzen.

Der Hauptnachteil des Modultrennungsprozesses ist paradoxerweise die Verlangsamung der Montagegeschwindigkeit. Besonders zu Beginn, wenn Sie die ersten beiden Module Common und einige Ihrer Funktionen herausnehmen, sinkt die allgemeine Erstellungsgeschwindigkeit des Projekts, unabhängig davon, wie Sie es versuchen. Am Ende erhöht sich die Erstellungsgeschwindigkeit, da weniger Code in Ihrem Hauptmodul verbleibt. Und das bedeutet nicht, dass alles sehr schlecht ist. Es gibt Möglichkeiten, dies zu umgehen und sogar vom ersten Modul zu profitieren.
Der zweite Nachteil ist, dass es schwierig ist, den Code in Module zu unterteilen. Wer es versucht hat, weiß, dass Sie anfangen, Abhängigkeiten und Klassiker zu ziehen, und am Ende haben Sie Ihr gesamtes Hauptmodul in ein anderes Modul kopiert und von vorne begonnen. Daher müssen Sie den Moment klar verstehen, in dem Sie die Verbindung mithilfe einer Art Abstraktion stoppen und trennen müssen. Der Nachteil sind mehr Abstraktionen. Mehr Abstraktionen - komplexeres Design - mehr Abstraktionen.
Es ist schwierig, neue Gradle-Module hinzuzufügen. Warum? Zum Beispiel kommt ein Entwickler, nimmt eine neue Funktion in die Entwicklung auf, macht sofort gut, macht ein separates Modul. Was ist das Problem? Er muss sich den gesamten verfügbaren Code im Hauptmodul merken, damit er gegebenenfalls wiederverwendet und in Common abgelegt werden kann. Weil der Prozess des Herausnehmens eines Moduls in Common konstant ist, bis sich Ihr Haupt-App-Modul in eine dünne Schicht verwandelt.
Module, Module, Module ... Gradle-Module, Dolchmodule, Schnittstellenmodule sind schrecklich.

Der Bericht wird aus drei Teilen bestehen: klein, groß und komplex. Erstens der Unterschied zwischen der Implementierung und der API in AGP. Android Gradle Plugin 3.0 ist vor relativ kurzer Zeit erschienen. Wie war alles vor ihm?

Hier ist ein typisches Projekt eines gesunden Entwicklers, das aus drei Modulen besteht: Das Hauptmodul der App wird in der Anwendung zusammengestellt und installiert, sowie zwei Funktionsmodule.
Sprechen Sie sofort über die Pfeile. Dies ist ein großer Schmerz, jeder zeichnet in die Richtung, in die es für ihn bequem ist, zu zeichnen. Bei mir bedeutet das, dass es von Core aus einen Pfeil zu Feature gibt. Feature kennt sich also mit Core aus und kann Klassen von Core verwenden. Wie Sie sehen können, gibt es keinen Pfeil zwischen dem Core und der App, was bedeutet, dass die App anscheinend keinen Core verwendet. Core ist kein allgemeines Modul, es ist, jeder hängt davon ab, es ist separat, es enthält wenig Code. Wir werden es zwar nicht berücksichtigen.
Unser Kernmodul hat sich geändert, wir müssen es irgendwie wiederholen. Wir ändern den Code darin. Gelbe Farbe - Codeänderung.

Nach dem Zusammenbau des Projekts. Es ist klar, dass ein Modul nach dem Ändern neu erstellt und kompiliert werden muss. Okay

Nachdem das Feature-Modul ebenfalls zusammengebaut ist, hängt das davon ab. Es ist auch klar, dass sich seine Abhängigkeit wieder zusammengesetzt hat und Sie sich selbst aktualisieren müssen. Wer weiß, was sich dort geändert hat.
Und hier passiert das Unangenehmste. Das App-Modul läuft, obwohl nicht klar ist, warum. Ich weiß mit Sicherheit, dass ich Core in keiner Weise verwende und warum die App neu erstellt wird, ist unklar. Und er ist sehr groß, denn ganz am Anfang des Weges, und das ist ein sehr großer Schmerz.

Wenn mehrere Funktionen, viele Module von Core abhängen, wird die ganze Welt wieder zusammengesetzt, was sehr lange dauert.
Lassen Sie uns auf die neue Version von AGP aktualisieren und, wie im Handbuch angegeben, alle Kompilierungen mit der API und nicht mit der Implementierung ersetzen, wie Sie dachten. Nichts ändert sich. Die Schemata sind identisch. Was ist die neue Methode zum Festlegen von Implementierungsabhängigkeiten? Stellen Sie sich dasselbe Schema vor, das nur dieses Schlüsselwort ohne API verwendet? Es wird so aussehen.

Hier in der Implementierung ist deutlich zu sehen, dass eine Verbindung zwischen Core und App besteht. Hier können wir klar verstehen, dass wir es nicht brauchen, wir wollen es loswerden, also entfernen Sie es einfach. Alles wird einfacher.

Jetzt ist fast alles gut, sogar mehr als. Wenn wir eine API in Core ändern, eine neue Klasse, eine neue öffentliche oder eine private Paketmethode hinzufügen, werden Core und Feature neu erstellt. Wenn Sie die Implementierung innerhalb der Methode ändern oder eine private Methode hinzufügen, sollte das Feature theoretisch überhaupt nicht neu erstellt werden, da sich nichts geändert hat.

Gehen wir weiter. So kam es, dass viele von unserem Kern abhängen. Kern ist wahrscheinlich eine Art Netzwerk- oder Benutzerdatenverarbeitung. Da dies Netzwerk ist, ändert sich alles ziemlich oft, alles wird neu aufgebaut und wir haben den gleichen Schmerz, vor dem wir vorsichtig weggelaufen sind.
Schauen wir uns zwei Möglichkeiten an, um damit umzugehen.

Wir können die APIs nur von unserem Kernmodul auf ein separates Modul übertragen, dessen API wir verwenden. Und in einem separaten Modul können wir die Implementierung dieser Schnittstellen übernehmen.

Sie können die Verbindung auf dem Bildschirm anzeigen. Core Impl ist für Funktionen nicht verfügbar. Das heißt, es besteht keine Verbindung zwischen den Funktionen und der Core-Implementierung. Und das gelb hervorgehobene Modul bietet nur Fabriken, die eine Implementierung Ihrer Schnittstellen ermöglichen, die niemandem bekannt ist.
Nach einer solchen Konvertierung möchte ich darauf hinweisen, dass die Core-API aufgrund der Tatsache, dass das API-Schlüsselwort steht, für alle Funktionen transitiv verfügbar sein wird.

Nach diesen Transformationen ändern wir etwas an der Implementierung, die Sie am häufigsten ausführen, und nur das Modul mit den Fabriken wird neu erstellt. Es ist sehr leicht, klein und Sie müssen nicht einmal überlegen, wie lange es dauert.

Eine andere Option funktioniert nicht immer. Wenn dies beispielsweise eine Art Netzwerk ist, kann ich mir kaum vorstellen, wie dies geschehen kann, aber wenn dies eine Art Benutzeranmeldebildschirm ist, kann dies durchaus der Fall sein.

Wir können Sample, das gleiche vollwertige Root-Modul wie App, erstellen und nur eine Funktion darin sammeln. Es wird sehr schnell sein und es kann schnell iterativ entwickelt werden. Am Ende der Präsentation zeige ich Ihnen, wie lange es dauert, ein Beispiel zu erstellen.
Mit dem ersten Teil fertig. Welche Module gibt es?

Es gibt drei Arten von Modulen. Common sollte natürlich so leicht wie möglich sein und keine Funktionen enthalten, sondern nur die Funktionen, die von allen verwendet werden. Für uns in unserem Team ist dies besonders wichtig. Wenn wir unsere Feature-Module anderen Anwendungen zur Verfügung stellen, werden wir sie auf jeden Fall zwingen, Common zu ziehen. Wenn er sehr fett ist, wird uns niemand lieben.

Wenn Sie ein kleineres Projekt haben, können Sie sich mit Common entspannter fühlen, und Sie müssen auch nicht sehr eifrig sein.

Der nächste Modultyp ist Standalone. Das gebräuchlichste und intuitivste Modul, das eine bestimmte Funktion enthält: eine Art Bildschirm, eine Art Benutzerskript und so weiter. Es sollte so unabhängig wie möglich sein, und dafür können Sie meistens eine Beispiel-App erstellen und darin entwickeln. Die Beispiel-App ist zu Beginn des Aufteilungsprozesses sehr wichtig, da sich alles noch langsam aufbaut und Sie so schnell wie möglich Gewinn erzielen möchten. Am Ende, wenn alles in Module zerlegt ist, können Sie alles neu erstellen, es wird schnell sein. Weil es nicht wieder aufgebaut wird.

Promi-Module. Ich selbst habe mir das Wort ausgedacht. Der Punkt ist, dass er für alle sehr berühmt ist und viele von ihm abhängen. Das gleiche Netzwerk. Ich habe bereits gesagt, wenn Sie es oft wieder zusammenbauen, wie können Sie die Tatsache vermeiden, dass alles von Ihnen wieder zusammengesetzt wird. Es gibt einen anderen Weg, der für kleine Projekte verwendet werden kann, für die es nicht das Ziel wert ist, alles als separate Sucht, als separates Artefakt auszugeben.

Wie sieht es aus? Wir wiederholen, dass Sie die API aus Celebrity herausnehmen, ihre Implementierung herausnehmen und jetzt Ihre Hände beobachten und auf die Pfeile von Feature zu Celebrity achten. Das passiert gerade. Die API Ihres Moduls fiel in Common, die Implementierung blieb darin und die Factory, die die Implementierung dieser API bereitstellt, wurde in Ihrem Hauptmodul angezeigt. Wenn jemand Mobius beobachtete, dann sprach Denis Neklyudov darüber. Sehr ähnliches Schema.
Wir verwenden Dagger im Projekt, es gefällt uns und wir wollten diesen Vorteil im Kontext verschiedener Module optimal nutzen.

Wir wollten, dass jedes Modul einen unabhängigen Abhängigkeitsgraphen hat, eine bestimmte Stammkomponente, von der aus Sie alles tun können. Wir wollten unseren eigenen generierten Code für jedes Gradle-Modul. Wir wollten nicht, dass sich der generierte Code in den Hauptcode einschleicht. Wir wollten so viel Validierung zur Kompilierungszeit wie möglich. Wir leiden unter [k] apt, zumindest sollten wir etwas Profit von dem bekommen, was Dolch gibt. Und mit all dem wollten wir niemanden zwingen, Dolch zu benutzen. Weder derjenige, der das neue Funktionsmodul separat implementiert, noch derjenige, der es dann verwendet, sind unsere Kollegen, die nach einigen Funktionen für sich selbst fragen.
Wie organisiere ich ein separates Abhängigkeitsdiagramm in unserem Funktionsmodul?

Sie können versuchen, Subcomponent zu verwenden, und es wird sogar funktionieren. Dies hat jedoch einige Mängel. Sie können sehen, dass in Subcomponent nicht klar ist, welche Abhängigkeiten von Component verwendet werden. Um dies zu verstehen, müssen Sie das Projekt lange und schmerzhaft wieder zusammensetzen, sich ansehen, was Dagger schwört, und es hinzufügen.
Darüber hinaus sind die Unterkomponenten so angeordnet, dass sie andere zur Verwendung von Dolch zwingen, und es wird für Ihre Kunden und sich selbst nicht einfach, wenn Sie sich entscheiden, in einem Modul abzulehnen.

Eines der ekelhaftesten Dinge ist, dass bei Verwendung von Subcomponent alle Abhängigkeiten in das Hauptmodul gezogen werden. Dolch ist so konzipiert, dass Unterkomponenten von einer eingebetteten Klasse ihrer Rahmenkomponenten, dem übergeordneten Element, generiert werden. Vielleicht hat sich jemand den generierten Code und seine Größe für die generierten Komponenten angesehen? Wir haben 20.000 Zeilen darin. Da Unterkomponenten immer verschachtelte Klassen für Komponenten sind, stellt sich heraus, dass Unterkomponenten von Unterkomponenten ebenfalls verschachtelt sind und der gesamte generierte Code in das Hauptmodul fällt, diese zwanzigzeilige Datei, die kompiliert und überarbeitet werden muss. Studio beginnt sich zu verlangsamen - Schmerz.
Aber es gibt eine Lösung. Sie können nur Component verwenden.

In Dagger kann eine Komponente Abhängigkeiten angeben. Dies wird im Code und im Bild gezeigt. Abhängigkeiten, in denen Sie Bereitstellungsmethoden angeben, Factory-Methoden, die anzeigen, von welchen Entitäten Ihre Komponente abhängt. Er will sie zum Zeitpunkt der Schöpfung.
Früher habe ich immer gedacht, dass in diesen Abhängigkeiten nur andere Komponenten angegeben werden können, und deshalb - so heißt es in der Dokumentation.

Jetzt verstehe ich, was es bedeutet, eine Komponentenschnittstelle zu verwenden, aber bevor ich dachte, es sei nur eine Komponente. Tatsächlich müssen Sie eine Schnittstelle verwenden, die gemäß den Regeln zum Erstellen einer Schnittstelle für eine Komponente zusammengesetzt ist. Kurz gesagt, einfach Bereitstellungsmethoden, wenn Sie nur Getter für eine Art von Abhängigkeiten haben. Beispielcode finden Sie auch in der Dolchdokumentation.

Dort wird auch OtherComponent geschrieben, was verwirrend ist, da Sie dort nicht nur Komponenten einfügen können.
Wie möchten wir dieses Geschäft in der Realität nutzen?

In Wirklichkeit gibt es ein Feature-Modul, es hat ein sichtbares API-Paket, das sich in der Nähe des Stamms aller Pakete befindet, und es gibt einen Einstiegspunkt - FeatureActivity. Es ist nicht notwendig, Typealien zu verwenden, nur um es klar zu machen. Es kann ein Fragment sein, es kann ein ViewController sein - es spielt keine Rolle. Und es gibt seine Abhängigkeiten, FeatureDeps, bei denen angegeben wird, dass er einen Kontext benötigt, einen Netzwerkdienst von Common, etwas, das Sie von der App erhalten möchten, und jeder Client ist verpflichtet, dies zu erfüllen. Wenn er es tut, wird alles funktionieren.

Wie verwenden wir all dies im Funktionsmodul? Hier verwende ich Aktivität, dies ist optional. Wie üblich erstellen wir unsere eigene Root-Dolch-Komponente und verwenden die magische Methode findComponentDependencies. Sie ist Dagger für Android sehr ähnlich, kann sie jedoch nicht primär verwenden, da wir keine Unterkomponenten ziehen möchten. Andernfalls können wir ihnen die gesamte Logik entziehen.
Zuerst habe ich versucht zu sagen, wie es funktioniert, aber Sie können es im Beispielprojekt am Freitag sehen. Wie soll dies von Ihren Bibliotheksclients in Ihrem Hauptmodul verwendet werden?

Zuallererst sind es nur Typealien. In der Tat hat es einen anderen Namen, aber der Kürze halber ist es. Die Schnittstellenklasse MapOfDepth by Dependency bietet Ihnen die Implementierung. In der App sagen wir, dass wir Abhängigkeiten auf die gleiche Weise wie in Dagger für Android erstellen können. Es ist sehr wichtig, dass die Komponente diese Schnittstelle erbt und automatisch Bereitstellungsmethoden empfängt. Dolch von diesem Moment an beginnt uns zu zwingen, diese Abhängigkeit bereitzustellen. Bis Sie es bereitstellen, wird es nicht kompiliert. Dies ist der Hauptkomfort: Sie haben beschlossen, eine Funktion einzurichten und Ihre Komponente mit dieser Schnittstelle zu erweitern - alles, bis Sie den Rest erledigen, wird nicht nur kompiliert, sondern es werden eindeutige Fehlermeldungen ausgegeben. Das Modul ist einfach, der Punkt ist, dass es Ihre Komponente an die Implementierung der Schnittstelle bindet. Ungefähr das gleiche wie in Dagger für Android.
Kommen wir zu den Ergebnissen.

Ich habe unseren Mainframe und meinen lokalen Laptop überprüft, bevor ich alles ausgeschaltet habe, was möglich war. Wenn wir Feature eine öffentliche Methode hinzufügen, unterscheidet sich die Erstellungszeit erheblich. Hier zeige ich die Unterschiede beim Erstellen eines Beispielprojekts. Das sind 16 Sekunden. Oder wenn ich alle Karten sammle, bedeutet das zwei Minuten, um auf jede, auch nur minimale Änderung zu warten. Daher entwickeln wir viele Funktionen und werden sie in Beispielprojekten entwickeln. Auf dem Mainframe ist die Zeit vergleichbar.

Ein weiteres wichtiges Ergebnis. Vor dem Hervorheben des Feature-Moduls sah es so aus: Auf dem Mainframe waren es 28 Sekunden, jetzt sind es 49 Sekunden. Wir haben das erste Modul zugewiesen und bereits fast zweimal eine Verzögerung der Montage erhalten.

Eine weitere Option ist eine einfache inkrementelle Montage unseres Moduls, keine Funktion wie in der vorherigen. 28 Sekunden waren, bis das Modul zugewiesen wurde. Wenn wir einen Code zugewiesen haben, der nicht jedes Mal neu erstellt werden musste, und [k] apt, der nicht jedes Mal ausgeführt werden musste, haben wir drei Sekunden gewonnen. Gott weiß was, aber ich hoffe, dass mit jedem neuen Modul die Zeit nur kürzer wird.
Hier finden Sie nützliche Links zu Artikeln:
API versus Implementierung ,
Artikel mit Messungen der Erstellungszeit ,
Beispielmodul . Die Präsentation wird
verfügbar sein . Vielen Dank.