網域層

網域層是 UI 層和 API 層之間的「選用」層 資料層

包含時,選用的網域層會提供依附元件
    並依附於資料層
圖 1. 網域層在應用程式架構中的角色。

網域層負責封裝複雜的商業邏輯,或 由多個 ViewModel 重複使用的簡單商業邏輯。此層為選用性質,因為並非所有應用程式都有上述要求。建議只在有需要時才使用,例如處理複雜操作或需要能夠重複使用時。

網域層具有下列優點:

  • 可避免程式碼發生重複的情形。
  • 提高使用網域層類別的類別可讀性。
  • 提高應用程式的測試能力。
  • 透過允許分散責任的方式避免出現大型類別。

為簡化這些類別並減少其資料量,建議每個用途只負責處理單一功能,而且也不應包含可變動資料。建議您改為在 UI 或是資料層處理可變動資料。

本指南中的命名慣例

在本指南中,用途是以使用者負責的單一操作而命名。慣例如下:

「<現在式的動詞>」+「<名詞/動作 (選用)>」+「UseCase」

例如:FormatDateUseCaseLogOutUserUseCaseGetLatestNewsWithAuthorsUseCaseMakeLoginRequestUseCase

依附元件

在典型的應用程式架構中,用途類別會介於 資料層和存放區這表示用途類別通常取決於存放區類別,且這些類別與存放區的通訊方式與存放區相同,都會使用回呼 (Java) 或協同程式 (Kotlin) 的方式。如要進一步瞭解,請參閱資料層頁面

舉例來說,您的應用程式可能有一個用途類別,可從新聞存放區和作者存放區擷取資料,並且整合這些資料:

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

由於用途包含可重複使用的邏輯,因此其他用途也可使用。網域層常會有多個用途等級。適用對象 舉例來說,以下範例中定義的用途可使用 FormatDateUseCase 用途,如果 UI 層中的多個類別需要時間 區域,以便在螢幕上顯示適當的訊息:

class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository,
  private val formatDateUseCase: FormatDateUseCase
) { /* ... */ }
GetLatestNewsWithAuthorsUseCase 依附於資料層的存放區類別,但也依附於 FormatDataUseCase,這是屬於網域層的另一個用途類別。
圖 2.依附其他用途的用途範例依附元件圖。

在 Kotlin 中呼叫用途

在 Kotlin 中,您可以使用 operator 修飾符定義 invoke() 函式,這樣就能以函式形式呼叫用途類別例項。請參閱以下範例:

class FormatDateUseCase(userRepository: UserRepository) {

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

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

在這個範例中,FormatDateUseCase 中的 invoke() 方法可讓您呼叫該類別的執行個體,就如同它們是函式一樣。invoke() 方法不限於任何特定簽名,而是可以使用任意數量的參數並傳回任何類型。您也可以在類別中使用不同的簽名,使 invoke() 超載。您可以按照下列方法,從上述範例中呼叫用途:

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

如要進一步瞭解 invoke() 運算子,請參閱 Kotlin 文件

生命週期

用途沒有自己的生命週期。相反的,其範圍受使用該用途的類別限制。也就是說,您可以從 UI 層中的類別呼叫用途,也可從服務或 Application 類別本身呼叫用途。由於用途不應包含可變動的資料,因此每次您將用途類別例項當做依附元件傳送時,都應建立新的用途類別例項。

執行緒

網域層的用途必須是「main-safe」;也就是必須 能夠安全地從主執行緒呼叫如果用途類別執行長時間的封鎖作業,就必須負責將該邏輯移至適當的執行緒。不過,在進行轉移作業前,請先檢查這些封鎖作業是否放置在階層的其他層進行會更好。一般來說,複雜的運算會在資料層中完成以便重複使用或快取。舉例來說,如果需要快取結果以便在應用程式的多個螢幕上重複使用,將大型清單中資源密集的作業放置在資料層會比放置在網域層來得好。

以下範例顯示在背景執行緒上執行作業的用途:

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

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

一般工作

本節說明如何執行常見的網域層工作。

可重複使用的簡單商業邏輯

您應該將使用者介面層中出現的可重複商業邏輯封裝在 用途類別如此一來,使用邏輯的所有位置都能更輕鬆地套用變更。您也可以在隔離中測試邏輯。

請參照前述的 FormatDateUseCase 範例。如果貴企業日後在有關日期格式要求方面會有所變動,只需要在一個集中的位置變更程式碼即可。

合併存放區

在新聞應用程式中,您可將 NewsRepositoryAuthorsRepository 類別分別設為處理新聞和作者資料作業。NewsRepository 顯示的 Article 類別只包含作者的姓名,但您想要在螢幕上顯示作者的更多相關資訊。您可以從 AuthorsRepository 取得作者資訊。

GetLatestNewsWithAuthorsUseCase 取決於資料層的兩個不同存放區類別:NewsRepository 和 AuthorsRepository。
圖 3. 用途的依附元件圖,合併多個存放區的資料。

由於邏輯涉及多個存放區,且可能變得較為複雜,因此您可以建立 GetLatestNewsWithAuthorsUseCase 類別將邏輯從 ViewModel 中摘錄出來,使其更具可讀性。這也使得邏輯更容易在隔離狀態下進行測試,進而在應用程式的不同部分中重複使用。

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

這個邏輯會對應 news 清單中的所有項目;因此,即使資料層是安全的,但您不知道它要處理的項目數有多少,因此這項作業不應該封鎖主要執行緒。因此,用途會使用預設調度工具將作業移至背景執行緒。

其他取用者

除了 UI 層以外,網域層可供其他類別重複使用,例如 服務和 Application 類別。此外,如果其他平台 或 Wear 與行動應用程式分享程式碼集,則其 UI 層也可重複使用 以便享有網域層的所有上述優勢。

資料層存取權限制

實作網域層時,還有一個考量重點: 仍允許直接從 UI 層存取資料層,或強制 都屬於網域層

UI 層無法直接存取資料層,必須通過網域層存取
圖 4. 顯示存取遭拒 UI 層的依附元件圖表 資料層

設定這項限制的好處在於,使用者介面不會略過 網域層邏輯,例如,如果您要個別記錄 資料層的存取權要求

不過,可能明顯的缺點是強制您 加入用途,即使只是對資料層的簡單函式呼叫 這種做法只會增加複雜度

建議您只在必要時新增用途。如果您發現使用者介面 幾乎只透過用途存取資料 以這種方式存取資料。

最後,限制資料層存取權的決定,取決於 以及您希望使用嚴格的規則或更有彈性 。

測試

一般測試指南會在測試網域層時套用。對於其他 UI 測試,開發人員通常會使用假存放區, 測試網域層時,也建議您使用假存放區。

範例

以下 Google 範例示範如何使用網域層。 歡迎查看這些範例,瞭解實務做法: