Gängige Modularisierungsmuster

Es gibt keine einzelne Modularisierungsstrategie, die für alle Projekte geeignet ist. Aufgrund von Da Gradle flexibel ist, gibt es nur wenige Einschränkungen um ein Projekt zu organisieren. Diese Seite bietet einen Überblick über einige allgemeine Regeln und häufige Mustern, die du bei der Entwicklung mehrteiliger Android-Apps nutzen kannst.

Prinzip der hohen Kohäsion und niedriger Kopplung

Eine Möglichkeit zur Charakterisierung einer modularen Codebasis wäre die Verwendung der Kopplung und cohesion-Eigenschaften. Die Kopplung misst den Grad, in dem Module voneinander abhängig sind. Kohäsion misst in diesem Kontext, wie die Elemente einer einzelnen Modulen zusammenhängen. In der Regel sollten Sie geringe Kopplung und hohe Kohäsion:

  • Geringe Kopplung bedeutet, dass die Module so unabhängig wie möglich von den sodass Änderungen an einem Modul keine oder nur minimale Auswirkungen anderen Modulen. Module sollten keine Kenntnisse über das Innenleben eines anderen Modulen.
  • Hohe Kohäsion bedeutet, dass Module eine Sammlung von Code enthalten sollten, agiert als System. Sie sollten klar definierte Verantwortlichkeiten haben und innerhalb der Grenzen eines bestimmten Domänenwissens liegen. Beispiel-E-Book ansehen . Es könnte unangemessen sein, Code für Buch und Zahlung zu mischen im selben Modul, da es sich um unterschiedliche Funktionsbereiche handelt.

Modultypen

Die Art und Weise, wie Sie Ihre Module organisieren, hängt hauptsächlich von Ihrer Anwendungsarchitektur ab. Darunter sind einige gängige Modultypen, die Sie in Ihrer App einführen können, unserer empfohlenen Anwendungsarchitektur.

Datenmodule

Ein Datenmodul enthält normalerweise ein Repository, Datenquellen und Modellklassen. Die drei Hauptaufgaben eines Datenmoduls sind:

  1. Alle Daten und Geschäftslogik einer bestimmten Domain verkapseln: Alle Daten -Moduls verantwortlich für die Verarbeitung von Daten sein, . Es kann viele Datentypen verarbeiten, solange sie zusammenhängen.
  2. Repository als externe API verfügbar machen: Die öffentliche API von Daten. sollte ein Repository sein, da sie dafür verantwortlich sind, für den Rest der App.
  3. So blenden Sie alle Implementierungsdetails und Datenquellen aus: Datenquellen sollten nur für Repositories aus demselben Modul zugänglich sein. Sie bleiben nach außen verborgen. Sie können dies erzwingen, indem Sie die private oder internal Sichtbarkeits-Keyword.
<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
</ph>
Abbildung 1. Beispiele für Datenmodule und deren Inhalte.

Funktionsmodule

Eine Funktion ist ein isolierter Teil der Funktionalität einer App, der in der Regel auf einem Bildschirm oder einer Reihe von eng verwandten Bildschirmen, z. B. Anmeldungen oder Bezahlvorgänge, Ablauf. Verfügt Ihre App über eine Leiste am unteren Rand, ist eine Funktion.

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
</ph>
Abbildung 2: Jeder Tab dieser Anwendung kann als Funktion definiert werden.

Die Funktionen sind mit Bildschirmen oder Zielen in Ihrer App verknüpft. Dementsprechend wird haben sie wahrscheinlich eine zugehörige Benutzeroberfläche und ViewModel, um ihre Logik zu verarbeiten. und Bundesstaat. Ein einzelnes Feature muss nicht auf eine einzelne Ansicht oder Navigationsziel. Funktionsmodule hängen von Datenmodulen ab.

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
</ph>
Abbildung 3: Beispiele für Funktionsmodule und ihre Inhalte.

App-Module

App-Module sind ein Einstiegspunkt für die Anwendung. Sie hängen von der Funktion ab, Module und bieten normalerweise Root-Navigation. Ein einzelnes App-Modul kann kompiliert werden mithilfe von Build-Varianten in verschiedene Binärprogramme umwandeln.

<ph type="x-smartling-placeholder">
</ph>
Abbildung 4: *Demo* und *vollständiges* Diagramm zu Abhängigkeitsmodulen für Produkt-Flavor-Module.

Wenn Ihre App auf mehrere Gerätetypen ausgerichtet ist, etwa „Auto“, „Wear“ oder „TV“, definieren Sie für jeden Gerätetyp ein App-Modul. So lassen sich plattformspezifische Abhängigkeiten.

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
</ph>
Abbildung 5: Diagramm mit Abhängigkeit von Wear-App

Gängige Module

Gängige Module, auch als Kernmodule bezeichnet, enthalten Code, den andere Module die Sie häufig verwenden. Sie reduzieren die Redundanz und stellen keine bestimmte Schicht in die Architektur einer App. Im Folgenden finden Sie Beispiele für gängige Module:

  • UI-Modul: Wenn Sie benutzerdefinierte UI-Elemente oder ein ausgeklügeltes Branding in Ihren App ist, sollten Sie erwägen, Ihre Widget-Sammlung in einem Modul wiederverwendbar. Dies könnte dazu beitragen, Ihre UI auf allen unterschiedliche Funktionen. Sind Ihre Themen beispielsweise zentralisiert, können Sie vermeiden, bei einem Rebranding eine schmerzhafte Refaktorierung ist.
  • Analytics-Modul: Tracking wird oft durch Geschäftsanforderungen bestimmt. für die Softwarearchitektur wenig berücksichtigen. Analytics-Tracker sind die oft in vielen zusammenhängenden Komponenten verwendet werden. In diesem Fall kann es sein, empfiehlt es sich, ein eigenes Analytics-Modul zu haben.
  • Netzwerkmodul: Wenn viele Module eine Netzwerkverbindung erfordern, können Sie haben Sie die Möglichkeit, ein Modul speziell für die Bereitstellung eines HTTP-Clients zu verwenden. Es ist besonders nützlich, wenn Ihr Client eine benutzerdefinierte Konfiguration benötigt.
  • Dienstprogrammmodul: Dienstprogramme, auch als Hilfsprogramme bezeichnet, sind in der Regel kleine Teile. die in der gesamten Anwendung wiederverwendet werden. Beispiele für Dienstprogramme: eine Funktion zum Formatieren von Währungen, eine E-Mail-Validierung oder .

Testmodule

Testmodule sind Android-Module, die nur zu Testzwecken verwendet werden. Die Module enthalten Testcode, Testressourcen und Testabhängigkeiten, die nur sind zum Ausführen von Tests erforderlich und werden während der Laufzeit der Anwendung nicht benötigt. Testmodule werden erstellt, um testspezifischen Code vom Hauptcode zu trennen -Anwendung, wodurch der Modulcode einfacher zu verwalten und zu pflegen ist.

Anwendungsfälle für Testmodule

Die folgenden Beispiele zeigen Situationen, in denen Testmodule kann besonders vorteilhaft sein:

  • Gemeinsam genutzter Testcode: Wenn Ihr Projekt mehrere Module und einige der Testcode auf mehr als ein Modul anwendbar ist, können Sie um den Code zu teilen. So lassen sich Duplikate vermeiden und der Test einfacher zu verwalten. Ein gemeinsamer Testcode kann Dienstprogrammklassen oder Funktionen wie benutzerdefinierte Assertions oder Matcher sowie Testdaten wie simulierten JSON-Antworten.

  • Sauberere Build-Konfigurationen: Testmodule ermöglichen eine sauberere Build-Konfigurationen erstellen, da sie eine eigene build.gradle-Datei haben können. Das solltest du nicht tun die Datei build.gradle deines App-Moduls mit Konfigurationen, nur für Tests relevant.

  • Integrationstests: Testmodule können zum Speichern der Integration verwendet werden. Tests, mit denen Interaktionen zwischen verschiedenen Teilen der App getestet werden. einschließlich Benutzerschnittstelle, Geschäftslogik, Netzwerkanfragen und Datenbankabfragen.

  • Umfangreiche Anwendungen: Testmodule sind besonders nützlich für umfangreichen Anwendungen mit komplexen Codebasen und mehreren Modulen. In solchen Fällen können Testmodule dazu beitragen, die Organisation und Verwaltbarkeit des Codes zu verbessern.

<ph type="x-smartling-placeholder">
</ph>
Abbildung 6. Testmodule können verwendet werden, um Module zu isolieren, die andernfalls voneinander abhängig wären.

Kommunikation zwischen Modulen

Module sind selten vollständig getrennt und stützen sich oft auf andere Module und mit ihnen kommunizieren können. Es ist wichtig, die Kopplung gering zu halten, auch wenn Module zusammenarbeiten und Informationen austauschen. Manchmal direkt Kommunikation zwischen zwei Modulen ist entweder nicht wünschenswert, wie im Fall von Architektureinschränkungen. Möglicherweise ist es unmöglich, wie z. B. bei zyklischen Abhängigkeiten.

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
</ph>
Abbildung 7: Aufgrund zyklischer Abhängigkeiten ist eine direkte wechselseitige Kommunikation zwischen Modulen nicht möglich. Ein Vermittlungsmodul ist erforderlich, um den Datenfluss zwischen zwei anderen unabhängigen Modulen zu koordinieren.

Um dieses Problem zu lösen, kann ein drittes Modul die Vermittlung zwischen zwei anderen Modulen. Das Vermittlermodul kann Nachrichten von beiden und leiten Sie sie bei Bedarf weiter. In unserer Beispiel-App Bildschirm muss wissen, welches Buch gekauft werden soll, separaten Bildschirm, der Teil einer anderen Funktion ist. In diesem Fall „Mediator“ ist das Modul mit dem Navigationsdiagramm (in der Regel ein App-Modul). In diesem Beispiel verwenden wir die Navigation, um die Daten von der Home-Funktion an die der Direktkauf-Funktion mithilfe der Komponente Navigation.

navController.navigate("checkout/$bookId")

Das Checkout-Ziel erhält eine Buch-ID als Argument, die es verwendet, um Informationen über das Buch abzurufen. Mit dem Ziehpunkt für den gespeicherten Status können Sie Navigationsargumente im ViewModel eines Zielelements abrufen.

class CheckoutViewModel(savedStateHandle: SavedStateHandle, …) : ViewModel() {

   val uiState: StateFlow<CheckoutUiState> =
      savedStateHandle.getStateFlow<String>("bookId", "").map { bookId ->
          // produce UI state calling bookRepository.getBook(bookId)
      }
      …
}

Sie sollten Objekte nicht als Navigationsargumente übergeben. Verwenden Sie stattdessen einfache IDs. mit denen Funktionen auf die gewünschten Ressourcen aus der Datenschicht zugreifen und diese laden können. So halten Sie die Kopplung gering und verletzen nicht die Single Source of Truth Prinzip.

Im folgenden Beispiel hängen beide Funktionsmodule von demselben Datenmodul ab. Dieses ermöglicht, die Datenmenge zu minimieren, die das Vermittlermodul benötigt um die Kopplung zwischen den Modulen gering zu halten. Anstelle von sollten Module primitive IDs austauschen und die Ressourcen aus einer Modul für freigegebene Daten.

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
</ph>
Abbildung 8: Zwei Funktionsmodule, die auf einem gemeinsamen Datenmodul basieren.

Abhängigkeitsumkehr

Eine Abhängigkeitsumkehr liegt vor, wenn Sie Ihren Code so organisieren, dass die Abstraktion getrennt von einer konkreten Implementierung.

  • Abstraktion: Ein Vertrag, der festlegt, wie Komponenten oder Module Anwendungen miteinander interagieren. Abstraktionsmodule definieren die API von Ihres Systems und enthalten Schnittstellen und Modelle.
  • Konkrete Implementierung: Module, die vom Abstraktionsmodul abhängen und das Verhalten einer Abstraktion zu implementieren.

Module, die auf dem im Abstraktionsmodul definierten Verhalten basieren, sollten nur hängen von der Abstraktion selbst ab, nicht von den spezifischen Implementierungen.

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
</ph>
Abbildung 9: Anstelle von übergeordneten Modulen, die direkt von Low-Level-Modulen abhängig sind, hängen übergeordnete Module und Implementierungsmodule vom Abstraktionsmodul ab.

Beispiel

Stellen Sie sich ein Funktionsmodul vor, für das eine Datenbank benötigt wird. Das Funktionsmodul ist wie die Datenbank implementiert wird, sei es eine lokale Raumdatenbank oder eine Datenbank Remote-Firestore-Instanz. Er muss nur die Anwendungsdaten speichern und lesen.

Dazu ist das Funktionsmodul auf das Abstraktionsmodul angewiesen, als bei einer bestimmten Datenbankimplementierung. Diese Abstraktion definiert die Database API verwenden können. Mit anderen Worten, es legt die Regeln für die Interaktion mit der Datenbank. Dadurch kann das Funktionsmodul jede Datenbank verwenden, die zugrunde liegenden Implementierungsdetails kennen.

Das konkrete Implementierungsmodul liefert die eigentliche Implementierung des Im Abstraktionsmodul definierte APIs Dazu muss die Implementierung hängt auch vom Abstraktionsmodul ab.

Abhängigkeitsinjektion

Sie fragen sich jetzt vielleicht, wie das Funktionsmodul mit dem Implementierungsmodul. Die Antwort lautet Dependency Injection. Die Funktion die erforderliche Datenbankinstanz nicht direkt erstellt. Stattdessen werden sie gibt an, welche Abhängigkeiten benötigt werden. Diese Abhängigkeiten werden dann bereitgestellt, extern, in der Regel im App-Modul.

releaseImplementation(project(":database:impl:firestore"))

debugImplementation(project(":database:impl:room"))

androidTestImplementation(project(":database:impl:mock"))

Vorteile

Die Trennung Ihrer APIs und ihrer Implementierungen bietet folgende Vorteile:

  • Austauschbarkeit: Mit klarer Trennung von API und Implementierung können Sie mehrere Implementierungen für dieselbe API entwickeln und ohne den Code zu ändern, der die API verwendet. Dabei kann es sich um besonders nützlich, wenn Sie unterschiedliche Fähigkeiten oder Verhalten in verschiedenen Kontexten. Beispiel: Ein simulierter für Tests und eine echte Implementierung für die Produktion.
  • Entkopplung: Die Trennung bedeutet, dass Module, die Abstraktionen verwenden, von einer bestimmten Technologie abhängen. Wenn Sie Ihre Datenbank von noch Raum für Firestore wäre, wäre es einfacher, da die Änderungen nur in dem jeweiligen Modul für die Aufgabe (Implementierungsmodul) stattfinden mit der API Ihrer Datenbank andere Module beeinflussen.
  • Testbarkeit: Die Trennung von APIs von ihren Implementierungen kann erheblich sein. das Testen erleichtern. Sie können Testfälle für die API-Verträge schreiben. Sie können verschiedene Implementierungen verwenden, um verschiedene Szenarien und Grenzfälle zu testen, einschließlich simulierter Implementierungen.
  • Verbesserte Build-Leistung: Wenn Sie eine API und ihre die Implementierung in verschiedene Module, das Build-System nicht zwingen, die Module neu zu kompilieren, API-Modul. Dies führt zu kürzeren Build-Daueren und erhöhter Produktivität, insbesondere bei großen Projekten, bei denen die Build-Dauer sehr lange dauern kann.

Wann trennen

Es ist sinnvoll, Ihre APIs von ihren Implementierungen in der folgenden Fällen:

  • Vielfältige Funktionen: Wenn Sie Teile Ihres Systems ermöglicht eine klare API die Austauschbarkeit verschiedener Implementierungen. Angenommen, Sie haben ein Rendering-System, das OpenGL oder Vulkan oder einem Abrechnungssystem, das mit Google Play oder Ihrer internen Abrechnung funktioniert der API erstellen.
  • Mehrere Anwendungen: Wenn Sie mehrere Anwendungen mit gemeinsame Funktionen für verschiedene Plattformen nutzen, können Sie spezifische Implementierungen für jede Plattform entwickeln.
  • Unabhängige Teams: Durch die Trennung können verschiedene Entwickler oder Teams gleichzeitig an verschiedenen Teilen der Codebasis arbeiten. Entwickler sollten sich auf zu den API-Verträgen und ihrer korrekten Verwendung. Sie müssen nicht die Implementierungsdetails anderer Module.
  • Große Codebasis: Wenn die Codebasis groß oder komplex ist und die API voneinander trennt. der Implementierung den Code überschaubarer machen. So können Sie die in detailliertere, verständliche und wartbare Einheiten unterteilt.

Implementierung

So implementieren Sie die Abhängigkeitsumkehr:

  1. Abstraktionsmodul erstellen: Dieses Modul sollte APIs (Schnittstellen) enthalten. und Modelle), die das Verhalten Ihrer Funktion definieren.
  2. Implementierungsmodule erstellen: Implementierungsmodule sollten auf dem API-Modul und implementieren das Verhalten einer Abstraktion. <ph type="x-smartling-placeholder">
    </ph> <ph type="x-smartling-placeholder">
    </ph> Anstelle von High-Level-Modulen, die direkt von Low-Level-Modulen abhängig sind, hängen übergeordnete Module und Implementierungsmodule vom Abstraktionsmodul ab.
    Abbildung 10: Implementierungsmodule hängen vom Abstraktionsmodul ab.
  3. Machen Sie übergeordnete Module von Abstraktionsmodulen abhängig: Anstelle von direkt von einer spezifischen Implementierung abhängig machen, Abstraktionsmodule. Für übergeordnete Module ist keine Kenntnis der Implementierung erforderlich. benötigen sie nur den Vertrag (API). <ph type="x-smartling-placeholder">
    </ph> <ph type="x-smartling-placeholder">
    </ph> High-Level-Module sind von Abstraktionen und nicht von der Implementierung abhängig.
    Abbildung 11: High-Level-Module sind von Abstraktionen und nicht von der Implementierung abhängig.
  4. Implementierungsmodul bereitstellen: Abschließend müssen Sie das Implementierung für Ihre Abhängigkeiten. Die jeweilige Implementierung hängt davon ab, In der Regel ist das App-Modul dafür aber gut geeignet. Um die Implementierung bereitzustellen, geben Sie sie als Abhängigkeit für die von Ihnen ausgewählte eine Variante oder einen Testquellensatz erstellen. <ph type="x-smartling-placeholder">
    </ph> <ph type="x-smartling-placeholder">
    </ph> Das App-Modul bietet eine tatsächliche Implementierung.
    Abbildung 12: Das App-Modul bietet die tatsächliche Implementierung.

Allgemeine Best Practices

Wie anfangs erwähnt wird, gibt es nicht nur einen einzigen richtigen Weg, App mit mehreren Modulen. So wie es viele Softwarearchitekturen gibt, eine App zu modularisieren. Dennoch gilt die folgende allgemeine Empfehlungen können Ihnen helfen, Ihren Code lesbarer, besser wartbar überprüfbar.

Konfiguration konsistent halten

Jedes Modul führt zu einem zusätzlichen Konfigurationsaufwand. Wenn die Anzahl Ihrer Module einen bestimmten Schwellenwert erreicht, wird die Verwaltung einer konsistenten Konfiguration Herausforderung. Zum Beispiel ist es wichtig, dass Module Abhängigkeiten Version. Wenn Sie eine große Anzahl von Modulen aktualisieren müssen, Abhängigkeitsversion ist nicht nur ein Aufwand, sondern auch ein Raum für potenzielle Fehler. Um dieses Problem zu lösen, können Sie eines der Gradle-Tools verwenden, Konfiguration zentralisieren:

  • Versionskataloge sind eine typsichere Liste von Abhängigkeiten. die bei der Synchronisierung von Gradle generiert werden. Hier können Sie alle Ihre Abhängigkeiten und ist für alle Module eines Projekts verfügbar.
  • Verwenden Sie Konventions-Plug-ins, um die Build-Logik zwischen Modulen freizugeben.

Möglichst wenig preisgeben

Die öffentliche Oberfläche eines Moduls sollte minimal sein und nur die die wichtigsten Infos. Es sollten keine Implementierungsdetails nach außen gelangen. Umfang alles so weit wie möglich zu verwirklichen. private oder internal von Kotlin verwenden Sichtbarkeitsbereich, um das Modul für die Deklarationen privat zu machen. Bei der Deklaration Abhängigkeiten in Ihrem Modul verwenden, bevorzugen Sie implementation gegenüber api. Letzteres stellt den Nutzern Ihres Moduls transitive Abhängigkeiten bereit. Mit Implementierung kann die Build-Dauer verkürzen, da die Anzahl der Module reduziert wird. die neu aufgebaut werden müssen.

Ich bevorzuge Kotlin und Java-Module

Android Studio unterstützt drei grundlegende Modultypen:

  • App-Module sind ein Einstiegspunkt für Ihre Anwendung. Sie können Folgendes enthalten: Quellcode, Ressourcen, Assets und AndroidManifest.xml. Die Ausgabe eines Das App-Modul ist ein Android App Bundle (AAB) oder ein Android-Anwendungspaket (APK).
  • Bibliotheksmodule haben denselben Inhalt wie die App-Module. Sie sind von anderen Android-Modulen als Abhängigkeit verwendet. Ausgabe eines Bibliotheksmoduls die ein Android-Archiv sind, sind strukturell identisch mit App-Modulen, werden zu einer Android Archive-Datei (AAR) kompiliert, die später von anderen Module als Abhängigkeit. Mit einem Bibliotheksmodul die gleiche Logik und Ressourcen in vielen Anwendungsmodulen kapseln und wiederverwenden.
  • Die Kotlin- und Java-Bibliotheken enthalten keine Android-Ressourcen, -Assets oder Manifestdateien.

Da Android-Module viel Aufwand verursachen, empfiehlt es sich, das Modul Kotlin- oder Java-Code so gut wie möglich verwenden.