網域層

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

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

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

網域層具有下列優點:

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

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

本指南中的命名慣例

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

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

例如:FormatDateUseCaseLogOutUserUseCaseGetLatestNewsWithAuthorsUseCaseMakeLoginRequestUseCase

依附元件

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

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

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

由於用途包含可重複使用的邏輯,因此其他用途也可使用。網域層常會有多個用途等級。舉例來說,如果 UI 層有多個類別仰賴時區以在畫面中顯示正確訊息,以下範例中定義的用途就可以利用 FormatDateUseCase 用途:

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.
    }
}

一般工作

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

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

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

請參照前述的 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 層幾乎完全透過用途存取資料,最好還是透過這種方式存取資料。

最後,限制資料層存取的決定取決於個別程式碼集,以及您希望使用嚴格的規則,還是更靈活的方法。

測試

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

範例

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