Il livello dominio è un livello facoltativo che si trova tra il livello UI e il livello dati.
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
) { /* ... */ }
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
.
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 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:
Consigliato per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Livello dati
- Produzione stato UI