Die Domainebene ist eine optionale Ebene, die sich zwischen der UI-Ebene und der Datenschicht befindet.
Die Domainebene ist für die Kapselung komplexer Geschäftslogik bzw. einfacher Geschäftslogik verantwortlich, die von mehreren ViewModels wiederverwendet wird. Diese Ebene ist optional, da nicht alle Anwendungen diese Anforderungen erfüllen. Sie sollten sie nur verwenden, wenn es erforderlich ist, z. B. um Komplexität zu bewältigen oder die Wiederverwendbarkeit zu bevorzugen.
Eine Domainebene bietet folgende Vorteile:
- Es vermeidet Codeduplikate.
- Es verbessert die Lesbarkeit in Klassen, die Klassen der Domainebene verwenden.
- Sie verbessert die Testbarkeit der App.
- Es werden große Klassen vermieden, da Sie die Zuständigkeiten aufteilen können.
Damit diese Klassen einfach und schlank bleiben, sollte jeder Anwendungsfall nur für eine einzelne Funktionalität verantwortlich sein und keine änderbaren Daten enthalten. Sie sollten stattdessen veränderliche Daten in Ihrer UI oder in Datenschichten verarbeiten.
Namenskonventionen in diesem Leitfaden
In diesem Leitfaden werden Anwendungsfälle nach der einzelnen Aktion benannt, für die sie verantwortlich sind. Dafür gilt folgende Konvention:
Verb im Präsens + Substantiv/was (optional) + Anwendungsfall.
Beispiel: FormatDateUseCase
, LogOutUserUseCase
, GetLatestNewsWithAuthorsUseCase
oder MakeLoginRequestUseCase
.
Abhängigkeiten
In einer typischen App-Architektur passen Anwendungsfallklassen zwischen ViewModels aus der UI-Ebene und Repositories aus der Datenschicht. Anwendungsfallklassen hängen also in der Regel von Repository-Klassen ab und kommunizieren mit der UI-Ebene auf die gleiche Weise wie Repositories – entweder mit Callbacks (für Java) oder Koroutinen (für Kotlin). Weitere Informationen finden Sie auf der Seite Datenschichten.
Angenommen, Sie haben in Ihrer App eine Anwendungsfallklasse, die Daten aus einem Nachrichten- und einem Autoren-Repository abruft und kombiniert:
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository
) { /* ... */ }
Da Anwendungsfälle wiederverwendbare Logik enthalten, können sie auch in anderen Anwendungsfällen verwendet werden. Es ist normal, dass mehrere Ebenen von Anwendungsfällen auf der Domainebene vorhanden sind. Für den im folgenden Beispiel definierten Anwendungsfall kann beispielsweise der Anwendungsfall FormatDateUseCase
verwendet werden, wenn mehrere Klassen aus der UI-Ebene Zeitzonen verwenden, um die richtige Nachricht auf dem Bildschirm anzuzeigen:
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository,
private val formatDateUseCase: FormatDateUseCase
) { /* ... */ }
Anwendungsfälle in Kotlin
In Kotlin können Sie Anwendungsfallklasseninstanzen als Funktionen aufrufbar machen. Dazu definieren Sie die Funktion invoke()
mit dem Modifikator operator
. Hier ein Beispiel:
class FormatDateUseCase(userRepository: UserRepository) {
private val formatter = SimpleDateFormat(
userRepository.getPreferredDateFormat(),
userRepository.getPreferredLocale()
)
operator fun invoke(date: Date): String {
return formatter.format(date)
}
}
In diesem Beispiel können Sie mit der Methode invoke()
in FormatDateUseCase
Instanzen der Klasse so aufrufen, als wären es Funktionen. Die Methode invoke()
ist nicht auf eine bestimmte Signatur beschränkt – sie kann beliebig viele Parameter annehmen und jeden Typ zurückgeben. Sie können invoke()
auch mit anderen Signaturen in Ihrer Klasse überladen. Sie würden den Anwendungsfall aus dem obigen Beispiel so aufrufen:
class MyViewModel(formatDateUseCase: FormatDateUseCase) : ViewModel() {
init {
val today = Calendar.getInstance()
val todaysDate = formatDateUseCase(today)
/* ... */
}
}
Weitere Informationen zum Operator invoke()
finden Sie in der Kotlin-Dokumentation.
Lebenszyklus
Anwendungsfälle haben keinen eigenen Lebenszyklus. Stattdessen werden sie der Klasse zugeordnet, die sie verwendet. Das bedeutet, dass Sie Anwendungsfälle aus Klassen auf der UI-Ebene, aus Diensten oder aus der Klasse Application
selbst aufrufen können. Da Anwendungsfälle keine änderbaren Daten enthalten sollten, sollten Sie jedes Mal eine neue Instanz einer Anwendungsfallklasse erstellen, wenn Sie sie als Abhängigkeit übergeben.
Mit Gewinde
Anwendungsfälle auf der Domainebene müssen hauptsicher sein, d. h., sie müssen sicher aus dem Hauptthread aufgerufen werden können. Wenn Anwendungsfallklassen Blockiervorgänge mit langer Ausführungszeit ausführen, sind sie dafür verantwortlich, diese Logik in den entsprechenden Thread zu verschieben. Prüfen Sie vorher jedoch, ob diese blockierenden Vorgänge besser auf anderen Hierarchieebenen platziert werden könnten. In der Regel finden komplexe Berechnungen in der Datenschicht statt, um die Wiederverwendbarkeit oder das Caching zu fördern. Ein ressourcenintensiver Vorgang auf einer großen Liste sollte beispielsweise besser auf der Datenschicht als auf der Domainebene platziert werden, wenn das Ergebnis im Cache gespeichert werden muss, um es auf mehreren Bildschirmen der Anwendung wiederzuverwenden.
Das folgende Beispiel zeigt einen Anwendungsfall, bei dem ein Hintergrundthread ausgeführt wird:
class MyUseCase(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend operator fun invoke(...) = withContext(defaultDispatcher) {
// Long-running blocking operations happen on a background thread.
}
}
Allgemeine Aufgaben
In diesem Abschnitt wird beschrieben, wie Sie allgemeine Aufgaben der Domainebene ausführen.
Wiederverwendbare einfache Geschäftslogik
Sie sollten die wiederholbare Geschäftslogik, die auf der UI-Ebene vorhanden ist, in einer Anwendungsfallklasse kapseln. Dadurch ist es einfacher, Änderungen überall dort vorzunehmen, wo die Logik verwendet wird. Außerdem können Sie die Logik isoliert testen.
Betrachten Sie das zuvor beschriebene Beispiel FormatDateUseCase
. Wenn sich Ihre geschäftlichen Anforderungen an die Datumsformatierung in Zukunft ändern, müssen Sie den Code nur an einem zentralen Ort ändern.
Repositories kombinieren
In einer Nachrichten-App gibt es möglicherweise die Klassen NewsRepository
und AuthorsRepository
, die Nachrichten- bzw. Autorendatenvorgänge verarbeiten. Die von NewsRepository
bereitgestellte Klasse Article
enthält nur den Namen des Autors. Sie möchten aber weitere Informationen zum Autor auf dem Bildschirm anzeigen lassen. Informationen zum Autor können im AuthorsRepository
abgerufen werden.
Da die Logik mehrere Repositories umfasst und komplex werden kann, erstellen Sie eine GetLatestNewsWithAuthorsUseCase
-Klasse, um die Logik aus ViewModel zu abstrahieren und sie lesbarer zu machen. Dies macht es auch einfacher, die Logik isoliert zu testen und in verschiedenen Teilen der App wiederverwendbar zu machen.
/**
* This use case fetches the latest news and the associated author.
*/
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend operator fun invoke(): List<ArticleWithAuthor> =
withContext(defaultDispatcher) {
val news = newsRepository.fetchLatestNews()
val result: MutableList<ArticleWithAuthor> = mutableListOf()
// This is not parallelized, the use case is linearly slow.
for (article in news) {
// The repository exposes suspend functions
val author = authorsRepository.getAuthor(article.authorId)
result.add(ArticleWithAuthor(article, author))
}
result
}
}
Die Logik ordnet alle Elemente in der news
-Liste zu. Obwohl die Datenschicht hauptsicher ist, sollte diese Arbeit den Hauptthread nicht blockieren, da Sie nicht wissen, wie viele Elemente verarbeitet werden. Deshalb wird die Arbeit im Anwendungsfall über den Standard-Dispatcher in einen Hintergrundthread verschoben.
Andere Nutzer
Neben der UI-Ebene kann die Domainebene auch von anderen Klassen wie Diensten und der Klasse Application
wiederverwendet werden. Wenn andere Plattformen wie TV oder Wear die Codebasis mit der mobilen App teilen, kann auch deren UI-Ebene Anwendungsfälle wiederverwenden, um alle oben genannten Vorteile der Domainebene zu nutzen.
Zugriffsbeschränkung für Datenschicht
Eine weitere Überlegung bei der Implementierung der Domainebene ist, ob Sie weiterhin den direkten Zugriff auf die Datenschicht von der UI-Ebene aus zulassen oder alles durch die Domainebene erzwingen sollten.
Diese Einschränkung hat den Vorteil, dass Ihre UI die Logik der Domainebene nicht mehr umgeht, z. B. wenn Sie für jede Zugriffsanfrage auf die Datenschicht ein Analyse-Logging durchführen.
Der potenziell erhebliche Nachteil besteht jedoch darin, dass Sie Anwendungsfälle zwingen müssen, selbst wenn es sich nur um einfache Funktionsaufrufe an die Datenschicht handelt. Dadurch wird die Komplexität geringfügig verbessert.
Ein guter Ansatz besteht darin, Anwendungsfälle nur bei Bedarf hinzuzufügen. Wenn Ihre UI-Ebene fast ausschließlich über Anwendungsfälle auf Daten zugreift, ist es möglicherweise sinnvoll, nur auf diese Weise auf Daten zuzugreifen.
Die Entscheidung, den Zugriff auf die Datenschicht einzuschränken, hängt letztendlich von Ihrer individuellen Codebasis ab und davon, ob Sie strenge Regeln oder einen flexibleren Ansatz bevorzugen.
Testen
Beim Testen der Domainebene gelten allgemeine Testrichtlinien. Bei anderen UI-Tests verwenden Entwickler in der Regel fiktive Repositories. Es empfiehlt sich, auch beim Testen der Domainebene fiktive Repositories zu nutzen.
Produktproben
Die folgenden Google-Beispiele veranschaulichen die Verwendung der Domain-Ebene. Sehen Sie sich diese Tipps in der Praxis an:
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Datenschicht
- Erstellung des UI-Zustands