Domainebene

Die Domainebene ist eine optionale Ebene, die sich zwischen der UI-Ebene und der Datenschicht befindet.

Die optionale Domainebene stellt Abhängigkeiten für die UI-Ebene bereit und hängt von der Datenschicht ab.
Abbildung 1. Die Rolle der Domainebene in der Anwendungsarchitektur.

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
) { /* ... */ }
GetNeuesteNewsWithAuthorsUseCase hängt von Repository-Klassen aus der Datenschicht, aber auch von FormatDataUseCase ab, einer weiteren Anwendungsfallklasse, die sich ebenfalls in der Domainebene befindet.
Abbildung 2. Beispiel für ein Abhängigkeitsdiagramm für einen Anwendungsfall, der von anderen Anwendungsfällen abhängt.

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.

GetNeuesteNewsWithAuthorsUseCase hängt von zwei verschiedenen Repository-Klassen aus der Datenschicht ab: NewsRepository und AuthorsRepository.
Abbildung 3. Abhängigkeitsdiagramm für einen Anwendungsfall, in dem Daten aus mehreren Repositories kombiniert 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.

Die UI-Ebene kann nicht direkt auf die Datenschicht zugreifen, sie muss über die Domain-Ebene erfolgen
Abbildung 4: Abhängigkeitsdiagramm, das zeigt, dass der Zugriff auf die UI-Ebene verweigert wird.

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: