Warstwa domeny

Warstwa domeny to opcjonalna warstwa, która znajduje się między warstwą interfejsu a warstwą danych.

Jeśli jest włączona, opcjonalna warstwa domeny określa zależności od warstwy interfejsu i zależy od warstwy danych.
Rysunek 1. Rola warstwy domeny w architekturze aplikacji.

Warstwa domeny odpowiada za wykorzystywanie złożonej logiki biznesowej lub prostej logiki biznesowej, która jest wykorzystywana ponownie przez wiele modeli widoków. Ta warstwa jest opcjonalna, ponieważ nie wszystkie aplikacje będą spełniać te wymagania. Należy go używać tylko wtedy, gdy jest to konieczne, na przykład ze względu na złożoność lub możliwość wielokrotnego wykorzystania.

Warstwa domen zapewnia następujące korzyści:

  • Pozwala to uniknąć duplikowania kodu.
  • Poprawia to czytelność klas, które korzystają z klas warstwy domeny.
  • Poprawia możliwość testowania aplikacji.
  • Pozwala to uniknąć dużych klas, ponieważ umożliwia dzielenie obowiązków.

Aby te klasy były proste i zwięzłe, każdy przypadek użycia powinien być związany tylko z 1 funkcją i nie powinien zawierać zmiennych danych. Zamiast tego używaj zmiennych danych w interfejsie użytkownika lub warstwach danych.

Konwencje nazewnictwa w tym przewodniku

W tym przewodniku nazwy przypadków użycia pochodzą od pojedynczego działania, za które odpowiadają. Konwencja jest następująca:

czasownik w czasie rzeczywistym + rzeczownik/co (opcjonalnie) + UseCase.

na przykład: FormatDateUseCase, LogOutUserUseCase, GetLatestNewsWithAuthorsUseCase lub MakeLoginRequestUseCase.

Zależności

W typowej architekturze aplikacji klasy przypadków użycia pasują do modeli ViewModel z warstwy interfejsu użytkownika a repozytoriów z warstwy danych. Oznacza to, że klasy przypadków użycia zależą zwykle od klas repozytorium i komunikują się z warstwą interfejsu w taki sam sposób jak repozytoria – używają wywołań zwrotnych (w języku Java) lub współprogramów (w Kotlin). Więcej informacji na ten temat znajdziesz na stronie warstwy danych.

Na przykład w Twojej aplikacji możesz mieć klasę przypadku użycia, która pobiera dane z repozytorium wiadomości i repozytorium autora, a potem je łączy:

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

Ponieważ logiki wielokrotnego użytku znajdują się w pozostałych przypadkach, można ich używać również w innych. Zastosowanie w warstwie domeny wielu poziomów przypadków użycia jest normalnym zjawiskiem. Na przykład przypadek użycia zdefiniowany w poniższym przykładzie może wykorzystać przypadek użycia FormatDateUseCase, jeśli do wyświetlenia odpowiedniego komunikatu na ekranie różne klasy z warstwy interfejsu użytkownika korzystają ze stref czasowych:

class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository,
  private val formatDateUseCase: FormatDateUseCase
) { /* ... */ }
Metoda GetLastNewsWithAuthorsUseCase zależy od klas repozytorium z warstwy danych, ale także od klasy FormatDataUseCase, czyli innej klasy przypadku użycia, która również znajduje się w warstwie domeny.
Rysunek 2. Przykładowy wykres zależności dla przypadku użycia, który zależy od innych przypadków użycia.

Przypadki użycia wywołań w Kotlin

W Kotlin możesz sprawić, że instancje klas przypadków użycia będą wywoływane jako funkcje, definiując funkcję invoke() za pomocą modyfikatora operator. Zobacz ten przykład:

class FormatDateUseCase(userRepository: UserRepository) {

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

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

W tym przykładzie metoda invoke() w tabeli FormatDateUseCase umożliwia wywoływanie wystąpień klasy tak, jakby były one funkcjami. Metoda invoke() nie jest ograniczona do żadnego konkretnego podpisu – może przyjmować dowolną liczbę parametrów i zwracać dowolny typ. Możesz też przeciążyć invoke() różnymi podpisami w klasie. Przypadek użycia z przykładu powyżej możesz wywołać w ten sposób:

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

Więcej informacji o operatorze invoke() znajdziesz w dokumentacji Kotlin.

Cykl życia

Przypadki użycia nie mają własnego cyklu życia. Są one ograniczone do klas, które z nich korzystają. Oznacza to, że można wywoływać przypadki użycia z klas w warstwie interfejsu, z usług lub z poziomu samej klasy Application. Przypadki użycia nie powinny zawierać danych zmiennych, dlatego należy tworzyć nową instancję klasy przypadku użycia za każdym razem, gdy przekazujesz ją jako zależność.

Gwintowanie

Przypadki użycia z warstwy domeny muszą być typu main-safe, czyli bezpieczne podczas wywoływania z wątku głównego. Jeśli klasy przypadków użycia wykonują długotrwałe operacje blokujące, są odpowiedzialne za przeniesienie tej logiki do odpowiedniego wątku. Zanim to zrobisz, sprawdź, czy takie działania blokujące nie można lepiej umieścić w innych warstwach hierarchii. Zwykle złożone obliczenia odbywają się w warstwie danych, aby zachęcić do ponownego wykorzystania lub zapisywania w pamięci podręcznej. Na przykład operacje wymagające dużej ilości zasobów na dużej liście są lepiej umieszczone w warstwie danych niż w warstwie domeny, jeśli wynik trzeba zapisać w pamięci podręcznej w celu ponownego użycia na wielu ekranach aplikacji.

Poniższy przykład przedstawia przypadek użycia, który działa w wątku w tle:

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

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

Częste zadania

W tej sekcji opisano, jak wykonywać typowe zadania w warstwie domen.

Prosta logika biznesowa do wielokrotnego użytku

Należy ujmować powtarzalną logikę biznesową w warstwie interfejsu w klasie przypadku użycia. Ułatwia to wprowadzanie zmian wszędzie tam, gdzie jest używana logika. Pozwala też przetestować działanie logiki w odizolowaniu.

Przeanalizuj opisany wcześniej przykład FormatDateUseCase. Jeśli Twoje wymagania biznesowe dotyczące formatowania dat zmienią się w przyszłości, wystarczy zmienić kod w jednym miejscu.

Łączenie repozytoriów

W aplikacji z wiadomościami możesz mieć klasy NewsRepository i AuthorsRepository, które obsługują odpowiednio operacje na danych dotyczących wiadomości i autora. Klasa Article, którą wyświetla NewsRepository, zawiera tylko imię i nazwisko autora, ale chcesz wyświetlić na ekranie więcej informacji o nim. Informacje o autorze można znaleźć w AuthorsRepository.

Metoda GetLastNewsWithAuthorsUseCase zależy od 2 różnych klas repozytorium z warstwy danych: NewsRepository i AuthorsRepository.
Rysunek 3. Wykres zależności dla przypadku użycia, który łączy dane z wielu repozytoriów.

Logika oparta na wielu repozytoriach i może stać się złożona, dlatego tworzysz klasę GetLatestNewsWithAuthorsUseCase, aby wyodrębnić logikę z modelu ViewModel i zwiększyć jej czytelność. Dzięki temu łatwiej jest przetestować działanie logiki w odizolowanych miejscach i ponownie użyć jej w różnych częściach aplikacji.

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

Logika mapuje wszystkie elementy na liście news – mimo że warstwa danych jest bezpieczna, nie powinna blokować wątku głównego, ponieważ nie wiadomo, ile elementów przetworzy. Dlatego przykład zastosowania przenosi zadanie do wątku w tle z użyciem domyślnego dyspozytora.

Inni klienci

Oprócz warstwy interfejsu warstwa domeny może być wykorzystywana przez inne klasy, takie jak usługi i klasa Application. Co więcej, jeśli inne platformy, takie jak TV czy Wear, udostępniają bazę kodu aplikacji mobilnej, ich warstwa interfejsu również może wykorzystywać przypadki użycia, aby korzystać ze wszystkich wymienionych powyżej zalet warstwy domeny.

Ograniczenie dostępu do warstwy danych

Jedną z innych kwestii przy wdrażaniu warstwy domeny jest to, czy nadal chcesz zezwalać na bezpośredni dostęp do warstwy danych z poziomu warstwy interfejsu, czy wymuszać dostęp do niej przez całą warstwę domeny.

Warstwa interfejsu nie może uzyskać bezpośredniego dostępu do warstwy danych, musi przechodzić przez warstwę domeny
Rysunek 4. Wykres zależności pokazujący odmowę dostępu warstwy interfejsu do warstwy danych.

Zaletą tego ograniczenia jest to, że zapobiega ominięciu logiki warstwy domeny przez interfejs użytkownika, na przykład jeśli rejestrujesz dane analityczne przy każdym żądaniu dostępu do warstwy danych.

Potencjalnie znaczącą wadą jest jednak to, że zmusza do dodawania przypadków użycia nawet wtedy, gdy są to tylko proste wywołania funkcji w warstwie danych, które mogą komplikować działania bez niewielkich korzyści.

Warto dodawać przypadki użycia tylko wtedy, gdy jest to wymagane. Jeśli okaże się, że warstwa interfejsu użytkownika uzyskuje dostęp do danych niemal wyłącznie za pomocą przypadków użycia, ten sposób uzyskiwania dostępu do danych może mieć sens tylko.

Decyzja o ograniczeniu dostępu do warstwy danych sprowadza się do indywidualnej bazy kodu i tego, czy preferujesz bardziej rygorystyczne czy bardziej elastyczne podejście.

Testowanie

Podczas testowania warstwy domen obowiązują ogólne wskazówki dotyczące testowania. W przypadku innych testów interfejsu deweloperzy zwykle używają fałszywych repozytoriów. Dobrze jest też używać ich do testowania warstwy domeny.

Próbki

Poniższe przykłady Google ilustrują użycie warstwy domen. Zapoznaj się z nimi, aby zastosować te wskazówki w praktyce: