Livello di dominio

Il livello dominio è un livello facoltativo che si trova tra il livello UI e il livello dati.

Se incluso, il livello di dominio facoltativo fornisce dipendenze al livello UI e dipende dal livello dati.
Figura 1. Il ruolo del livello di dominio nell'architettura dell'app.

Il livello dominio è responsabile dell'incapsulamento di una logica di business complessa, o semplice, di business riutilizzata da più ViewModel. Questo livello è facoltativo perché non tutte le app avranno questi requisiti. Da utilizzare solo quando necessario, ad esempio per gestire la complessità o favorire la riusabilità.

Un livello di dominio offre i seguenti vantaggi:

  • Evita la duplicazione del codice.
  • Migliora la leggibilità nelle classi che utilizzano le classi di livello del dominio.
  • Migliora la testabilità dell'app.
  • Consente di evitare classi molto numerose consentendo di suddividere le responsabilità.

Per mantenere queste classi semplici e leggere, ogni caso d'uso deve avere la responsabilità solo su una singola funzionalità e non deve contenere dati modificabili. Dovresti invece gestire i dati modificabili nell'interfaccia utente o nei livelli dati.

Convenzioni di denominazione in questa guida

In questa guida, i casi d'uso sono denominati in base alla singola azione di cui sono responsabili. La convenzione è la seguente:

verbo al presente + nome/cosa (facoltativo) + Caso d'uso.

Ad esempio: FormatDateUseCase, LogOutUserUseCase, GetLatestNewsWithAuthorsUseCase o MakeLoginRequestUseCase.

Dipendenze

In una tipica architettura delle app, le classi dei casi d'uso si adattano ai ViewModel del livello UI e ai repository del livello dati. Ciò significa che le classi dei casi d'uso di solito dipendono dalle classi dei repository e comunicano con il livello UI allo stesso modo dei repository, utilizzando callback (per Java) o coroutine (per Kotlin). Per ulteriori informazioni, consulta la pagina del livello dati.

Ad esempio, nella tua app potresti avere una classe di casi d'uso che recupera i dati da un repository di notizie e da un repository di autori e li combina:

class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository
) { /* ... */ }

Poiché i casi d'uso contengono logiche riutilizzabili, possono essere utilizzati anche da altri casi d'uso. È normale avere più livelli di casi d'uso nel livello dominio. Ad esempio, il caso d'uso definito nell'esempio seguente può utilizzare il caso d'uso FormatDateUseCase se più classi del livello UI si basano sui fusi orari per visualizzare il messaggio corretto sullo schermo:

class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository,
  private val formatDateUseCase: FormatDateUseCase
) { /* ... */ }
GetLatestNewsWithAutorisUseCase dipende dalle classi del repository del livello dati, ma anche da FormatDataUseCase, un'altra classe di casi d'uso che fa parte anche del livello dominio.
Figura 2. Esempio di grafico delle dipendenze per un caso d'uso che dipende da altri casi d'uso.

Casi d'uso delle chiamate in Kotlin

In Kotlin, puoi rendere le istanze delle classi di casi d'uso richiamabili come funzioni definendo la funzione invoke() con il modificatore operator. Vedi l'esempio seguente:

class FormatDateUseCase(userRepository: UserRepository) {

    private val formatter = SimpleDateFormat(
        userRepository.getPreferredDateFormat(),
        userRepository.getPreferredLocale()
    )

    operator fun invoke(date: Date): String {
        return formatter.format(date)
    }
}

In questo esempio, il metodo invoke() in FormatDateUseCase consente di chiamare istanze della classe come se fossero funzioni. Il metodo invoke() non è limitato a una firma specifica: può assumere un numero illimitato di parametri e restituire qualsiasi tipo. Puoi anche sovraccaricare invoke() con firme diverse nel tuo corso. Dovresti chiamare il caso d'uso dell'esempio precedente come segue:

class MyViewModel(formatDateUseCase: FormatDateUseCase) : ViewModel() {
    init {
        val today = Calendar.getInstance()
        val todaysDate = formatDateUseCase(today)
        /* ... */
    }
}

Per scoprire di più sull'operatore invoke(), consulta la documentazione di Kotlin.

Ciclo di vita

I casi d'uso non hanno un proprio ciclo di vita. L'ambito è invece limitato alla classe che li utilizza. Ciò significa che puoi chiamare i casi d'uso dalle classi nel livello dell'interfaccia utente, dai servizi o dalla classe Application stessa. Poiché i casi d'uso non devono contenere dati modificabili, devi creare una nuova istanza di una classe di casi d'uso ogni volta che la passi come dipendenza.

Threading

I casi d'uso del livello dominio devono essere main-safe; in altre parole, devono poter chiamare in sicurezza dal thread principale. Se le classi di casi d'uso eseguono operazioni di blocco a lunga esecuzione, sono responsabili dello spostamento di tale logica nel thread appropriato. Tuttavia, prima di farlo, controlla se queste operazioni di blocco sono posizionate meglio in altri livelli della gerarchia. In genere, i calcoli complessi vengono eseguiti nel livello dati per favorire la riusabilità o la memorizzazione nella cache. Ad esempio, un'operazione che richiede molte risorse su un elenco di grandi dimensioni è meglio posizionata nel livello dati che nel livello del dominio se il risultato deve essere memorizzato nella cache per riutilizzarlo su più schermate dell'app.

L'esempio seguente mostra un caso d'uso che esegue le proprie operazioni su un thread in background:

class MyUseCase(
    private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {

    suspend operator fun invoke(...) = withContext(defaultDispatcher) {
        // Long-running blocking operations happen on a background thread.
    }
}

Attività comuni

Questa sezione descrive come eseguire le attività più comuni a livello di dominio.

Logica di business semplice riutilizzabile

Dovresti incapsulare la logica di business ripetibile presente nel livello UI in una classe di casi d'uso. In questo modo è più facile applicare qualsiasi modifica ovunque venga usata la logica. Inoltre, ti permette di testare la logica in modo isolato.

Considera l'esempio FormatDateUseCase descritto in precedenza. Se i requisiti della tua attività riguardano la modifica della formattazione della data in futuro, dovrai modificare il codice solo in una posizione centralizzata.

Combina i repository

In un'app di notizie, potresti avere classi NewsRepository e AuthorsRepository che gestiscono rispettivamente le operazioni sui dati delle notizie e degli autori. La classe Article mostrata da NewsRepository contiene solo il nome dell'autore, ma vuoi visualizzare sullo schermo ulteriori informazioni sull'autore. È possibile ottenere informazioni sull'autore dal AuthorsRepository.

GetLatestNewsWithAuthorsUseCase dipende da due diverse classi di repository dal livello dati: NewsRepository e AuthorsRepository.
Figura 3. Grafico delle dipendenze per un caso d'uso che combina dati di più repository.

Poiché la logica coinvolge più repository e può diventare complessa, devi creare una classe GetLatestNewsWithAuthorsUseCase per astrarre la logica dal ViewModel e renderla più leggibile. Inoltre, la logica è più facile da testare singolarmente e riutilizzabile in diverse parti dell'app.

/**
 * 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
        }
}

La logica mappa tutti gli elementi nell'elenco news. Di conseguenza, anche se il livello dati è al sicuro dal principale, questa operazione non dovrebbe bloccare il thread principale perché non conosci il numero di elementi che verrà elaborato. Ecco perché il caso d'uso sposta il lavoro in un thread in background utilizzando il supervisore predefinito.

Altri consumatori

Oltre al livello UI, il livello dominio può essere riutilizzato da altre classi come i servizi e la classe Application. Inoltre, se altre piattaforme come TV o Wear condividono il codebase con l'app mobile, anche il livello UI può riutilizzare i casi d'uso per ottenere tutti i vantaggi del livello dominio menzionati sopra.

Limitazione di accesso al livello dati

Un'altra considerazione da considerare durante l'implementazione del livello di dominio è se è ancora necessario consentire l'accesso diretto al livello dati dal livello UI o forzare tutto nel livello di dominio.

Il livello UI non può accedere direttamente al livello dati, deve passare attraverso il livello Dominio
Figura 4. Grafico delle dipendenze che mostra al livello UI l'accesso negato al livello dati.

Il vantaggio di questa limitazione è che impedisce alla tua UI di bypassare la logica del livello di dominio, ad esempio se esegui il logging di analisi per ogni richiesta di accesso al livello dati.

Tuttavia, lo svantaggio potenzialmente significativo è che ti costringe ad aggiungere casi d'uso anche quando si tratta solo di semplici chiamate di funzione al livello dati, il che può aggiungere complessità senza alcun vantaggio.

Un buon approccio è aggiungere casi d'uso solo quando necessario. Se il tuo livello UI accede ai dati quasi esclusivamente attraverso casi d'uso, potrebbe avere senso accedere ai dati solo in questo modo.

In definitiva, la decisione di limitare l'accesso al livello dati dipende dal tuo singolo codebase e se preferisci regole rigide o un approccio più flessibile.

Test

Le linee guida generali per i test si applicano durante il test del livello dominio. Per altri test dell'interfaccia utente, gli sviluppatori in genere usano repository falsi ed è buona norma usare repository falsi anche per i test del livello di dominio.

Samples

I seguenti esempi di Google mostrano l'utilizzo del livello dominio. Esplorale per vedere concretamente queste indicazioni: