Im offiziellen Handbuch zur Android-Anwendungsarchitektur wird empfohlen, die Repository-Klassen zu verwenden, um "eine saubere API bereitzustellen, damit der Rest der Anwendung Daten problemlos abrufen kann". Wenn Sie dieses Muster jedoch in Ihrem Projekt verwenden, werden Sie meiner Meinung nach garantiert in unordentlichem Spaghetti-Code stecken bleiben.
In diesem Artikel werde ich Ihnen etwas über das "Repository-Muster" erzählen und erklären, warum es tatsächlich ein Anti-Muster für Android-Anwendungen ist.
Repository
Das oben erwähnte Handbuch zur Anwendungsarchitektur empfiehlt die folgende Struktur zum Organisieren der Präsentationsebenenlogik:
Die Rolle des Repository-Objekts in dieser Struktur lautet wie folgt:
Die Repository-Module verarbeiten Datenoperationen. Sie bieten eine saubere API, sodass der Rest der Anwendung diese Daten problemlos abrufen kann. Sie wissen, woher die Daten stammen und welche API-Aufrufe sie bei der Aktualisierung ausführen müssen. Sie können sich Repositorys als Vermittler zwischen verschiedenen Datenquellen wie persistenten Modellen, Webdiensten und Caches vorstellen.
Grundsätzlich empfiehlt das Handbuch die Verwendung von Repositorys, um die Datenquelle in Ihrer Anwendung zu abstrahieren. Klingt sehr vernünftig und sogar nützlich, nicht wahr?
Vergessen wir jedoch nicht, dass beim Chatten keine Taschen geworfen werden (in diesem Fall wird Code geschrieben), sondern dass Architekturthemen mithilfe von UML-Diagrammen enthüllt werden - umso mehr. Der eigentliche Test eines Architekturmusters besteht darin, es in Code zu implementieren und dann seine Vor- und Nachteile zu ermitteln. Lassen Sie uns also etwas finden, das weniger abstrakt zu überprüfen ist.
Repository in Android Architecture Blueprints v2
Vor ungefähr zwei Jahren habe ich die "erste Version" von Android Architecture Blueprints überprüft. Theoretisch sollten sie ein sauberes MVP-Beispiel implementieren, aber in der Praxis führten diese Blaupausen zu einer ziemlich schmutzigen Codebasis. Sie enthielten Schnittstellen mit den Namen View und Presenter, legten jedoch keine architektonischen Grenzen fest, sodass es sich im Wesentlichen nicht um ein MVP handelte. Sie können die angegebene Codeüberprüfung hier sehen .
Seitdem hat Google Architekturentwürfe mithilfe von Kotlin, ViewModel und anderen "modernen" Methoden, einschließlich Repositorys, aktualisiert. Diesen aktualisierten Blaupausen wurde v2 vorangestellt. Werfen
wir einen Blick auf die TasksRepository- Oberfläche von v2 blueprints :
interface TasksRepository {
fun observeTasks(): LiveData<Result<List<Task>>>
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>
suspend fun refreshTasks()
fun observeTask(taskId: String): LiveData<Result<Task>>
suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>
suspend fun refreshTask(taskId: String)
suspend fun saveTask(task: Task)
suspend fun completeTask(task: Task)
suspend fun completeTask(taskId: String)
suspend fun activateTask(task: Task)
suspend fun activateTask(taskId: String)
suspend fun clearCompletedTasks()
suspend fun deleteAllTasks()
suspend fun deleteTask(taskId: String)
}
Noch bevor Sie den Code lesen, können Sie auf die Größe dieser Schnittstelle achten - dies ist bereits ein Weckruf. Eine solche Anzahl von Methoden in einer Oberfläche würde selbst in großen Android-Projekten Fragen aufwerfen, aber wir sprechen von einer ToDo-Anwendung mit nur 2000 Codezeilen. Warum benötigt diese eher triviale Anwendung eine Klasse mit einer so großen API-Oberfläche?
Aufbewahrungsort als Gottobjekt
Die Antwort auf die Frage aus dem vorherigen Abschnitt finden Sie in den Namen der TasksRepository-Methoden. Ich kann die Methoden dieser Schnittstelle grob in drei nicht überlappende Gruppen einteilen.
Gruppe 1:
fun observeTasks(): LiveData<Result<List<Task>>>
fun observeTask(taskId: String): LiveData<Result<Task>>
Gruppe 2:
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>
suspend fun refreshTasks()
suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>
suspend fun refreshTask(taskId: String)
suspend fun saveTask(task: Task)
suspend fun deleteAllTasks()
suspend fun deleteTask(taskId: String)
Gruppe 3:
suspend fun completeTask(task: Task)
suspend fun completeTask(taskId: String)
suspend fun clearCompletedTasks()
suspend fun activateTask(task: Task)
suspend fun activateTask(taskId: String)
Definieren wir nun die Verantwortungsbereiche jeder der oben genannten Gruppen.
Gruppe 1 ist im Grunde eine Implementierung des Observer-Musters unter Verwendung der LiveData-Funktion. Gruppe 2 ist das Gateway zum Datenspeicher sowie zwei Methoden
refresh, die erforderlich sind, da der entfernte Datenspeicher hinter dem Repository versteckt ist. Gruppe 3 enthält funktionale Methoden, die im Wesentlichen zwei Teile der Anwendungsdomänenlogik implementieren (Aufgabenabschluss und Aktivierung).
Diese eine Schnittstelle hat also drei verschiedene Verantwortlichkeiten. Kein Wunder, dass es so groß ist. Und obwohl argumentiert werden kann, dass das Vorhandensein der ersten und zweiten Gruppe als Teil einer einzelnen Schnittstelle akzeptabel ist, ist das Hinzufügen der dritten Gruppe nicht gerechtfertigt. Wenn dieses Projekt weiterentwickelt werden muss und es zu einer echten Android-Anwendung wird, wächst die dritte Gruppe direkt proportional zur Anzahl der Domain-Streams im Projekt. Hmm.
Wir haben einen speziellen Begriff für Klassen, die so viele Verantwortlichkeiten teilen: Göttliche Objekte. Dies ist ein gängiges Anti-Pattern in Android-Anwendungen. Aktivität und Fragment sind in diesem Zusammenhang Standardverdächtige, aber auch andere Klassen können zu göttlichen Objekten ausarten. Vor allem, wenn ihre Namen auf "Manager" enden, oder?
Warten Sie ... Ich glaube, ich habe einen besseren Namen für TasksRepository gefunden:
interface TasksManager {
fun observeTasks(): LiveData<Result<List<Task>>>
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>
suspend fun refreshTasks()
fun observeTask(taskId: String): LiveData<Result<Task>>
suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>
suspend fun refreshTask(taskId: String)
suspend fun saveTask(task: Task)
suspend fun completeTask(task: Task)
suspend fun completeTask(taskId: String)
suspend fun activateTask(task: Task)
suspend fun activateTask(taskId: String)
suspend fun clearCompletedTasks()
suspend fun deleteAllTasks()
suspend fun deleteTask(taskId: String)
}
Jetzt spiegelt der Name dieser Schnittstelle ihre Verantwortlichkeiten viel besser wider!
Anämische Repositories
Hier können Sie fragen: "Wenn ich die Domänenlogik aus dem Repository ziehe, wird das Problem dadurch gelöst?" Nun, zurück zum "Architekturdiagramm" aus dem Google-Handbuch.
Wenn Sie beispielsweise Methoden
completeTaskaus dem TasksRepository extrahieren möchten, wo würden Sie sie ablegen? Gemäß der von Google empfohlenen "Architektur" müssen Sie diese Logik in eines Ihrer ViewModels verschieben. Es scheint keine so schlechte Entscheidung zu sein, aber es ist wirklich so.
Stellen Sie sich zum Beispiel vor, Sie fügen diese Logik in ein ViewModel ein. Nach einem Monat möchte Ihr Account Manager den Benutzern ermöglichen, Aufgaben auf mehreren Bildschirmen auszuführen (dies ist für alle ToDo-Manager relevant, die ich jemals verwendet habe). Die Logik im ViewModel kann nicht wiederverwendet werden. Sie müssen sie daher entweder duplizieren oder an das TasksRepository zurückgeben. Offensichtlich sind beide Ansätze schlecht.
Ein besserer Ansatz wäre, diesen Domänenstrom in ein benutzerdefiniertes Objekt zu extrahieren und ihn dann zwischen ViewModel und Repository zu platzieren. Dann können verschiedene ViewModels dieses Objekt wiederverwenden, um diesen bestimmten Thread auszuführen. Diese Objekte werden als "Anwendungsfälle" oder "Interaktionen" bezeichnet.... Wenn Sie Ihrer Codebasis jedoch Anwendungsfälle hinzufügen, werden Repositorys im Wesentlichen zu einer nutzlosen Vorlage. Was auch immer sie tun, es passt besser zu den Anwendungsfällen. Gabor Varadi hat dieses Thema bereits in diesem Artikel behandelt , daher werde ich nicht auf Details eingehen. Ich abonniere fast alles, was er über "anämische Repositories" gesagt hat.
Aber warum sind Anwendungsfälle so viel besser als Repositorys? Die Antwort ist einfach: Anwendungsfälle kapseln separate Streams. Anstelle eines Repositorys (für jedes Domänenkonzept), das allmählich zu einem göttlichen Objekt heranwächst, stehen Ihnen daher mehrere sehr zielgerichtete Anwendungsfallklassen zur Verfügung. Wenn der Stream vom Netzwerk und den gespeicherten Daten abhängt, können Sie die entsprechenden Abstraktionen an die Anwendungsfallklasse übergeben und zwischen diesen Quellen "vermitteln".
Im Allgemeinen scheint es die einzige Möglichkeit zu sein, die Verschlechterung von Repositorys zu göttlichen Klassen zu verhindern und gleichzeitig unnötige Abstraktionen zu vermeiden, die Repositorys zu entfernen.
Repositories außerhalb von Android.
Jetzt fragen Sie sich vielleicht, ob Repositorys eine Erfindung von Google sind. Nein sind sie nicht. Das Repository-Muster wurde lange vor der Entscheidung von Google beschrieben, es in seinem Architekturhandbuch zu verwenden.
Zum Beispiel beschrieb Martin Fowler Repositorys in seinem Buch Patterns of Enterprise Application Architecture. Sein Blog hat auch einen Gastartikel , der das gleiche Konzept beschreibt. Laut Fowler ist ein Repository nur ein Wrapper um die Speicherebene, der eine übergeordnete Abfrageschnittstelle und möglicherweise In-Memory-Caching bietet. Ich würde sagen, dass sich Repositories aus Fowlers Sicht wie ORMs verhalten.
Eric Evans beschrieb Repositories auch in seinem Buch Domain Driven Design. Er schrieb:
, , , — . , . , , .
Beachten Sie, dass Sie das "Repository" im obigen Zitat durch "Room ORM" ersetzen können und es dennoch sinnvoll ist. Im Kontext von Domain Driven Design ist ein Repository ein ORM (manuell implementiert oder unter Verwendung eines Frameworks eines Drittanbieters).
Wie Sie sehen können, wurde das Repository in der Android-Welt nicht erfunden. Dies ist ein sehr vernünftiges Entwurfsmuster, auf dem alle ORM-Frameworks basieren. Beachten Sie jedoch, was Repositorys nicht sind: Keiner der "Klassiker" hat jemals argumentiert, dass Repositorys versuchen sollten, die Unterscheidung zwischen Netzwerkzugriff und Datenbankzugriff aufzuheben.
Tatsächlich bin ich mir ziemlich sicher, dass sie diese Idee naiv und selbstzerstörerisch finden werden. Um zu verstehen, warum, können Sie einen weiteren Artikel lesen, diesmal von Joel Spolsky (Gründer von StackOverflow) mit dem Titel"Das Gesetz der undichten Abstraktionen . " Einfach ausgedrückt: Die Vernetzung unterscheidet sich zu stark vom Datenbankzugriff, um ohne nennenswerte Lecks abstrakt zu sein.
Wie das Repository in Android zum Anti-Pattern wurde
Hat Google das Repository-Muster falsch interpretiert und die naive Idee eingeführt, den Netzwerkzugriff zu abstrahieren? Ich bezweifle es.
Ich habe den ältesten Link zu diesem Antimuster in diesem GitHub-Repository gefunden , das leider eine sehr beliebte Ressource ist. Ich weiß nicht, ob dieser Autor dieses Antimuster erfunden hat, aber es sieht so aus, als ob dieses Repo die allgemeine Idee innerhalb des Android-Ökosystems populär gemacht hat. Google-Entwickler haben es wahrscheinlich von dort oder von einer der sekundären Quellen erhalten.
Fazit
Das Repository in Android ist also zu einem Anti-Pattern geworden. Auf dem Papier sieht es gut aus, wird aber selbst bei trivialen Anwendungen problematisch und kann bei größeren Projekten zu echten Problemen führen.
In einem anderen Google-Entwurf, diesmal für Architekturkomponenten, führte die Verwendung von Repositorys schließlich zu Edelsteinen wie NetworkBoundResource . Denken Sie daran, dass der Beispielbrowser GitHub immer noch eine winzige ~ 2 KLOC-App ist.
Soweit ich das beurteilen kann, ist das in den offiziellen Dokumenten definierte "Repository-Muster" nicht mit sauberem und wartbarem Code kompatibel.
Vielen Dank fürs Lesen und wie immer können Sie Ihre Kommentare und Fragen unten hinterlassen.
