網域層

Stay organized with collections Save and categorize content based on your preferences.

網域層是使用者介面層和資料層之間的選用層。

如果有包括選用的網域層,則此網域層可以為
    使用者介面層提供依附元件,並取決於資料層。
圖 1. 網域層在應用程式架構中的角色。

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

網域層具備下列優點:

  • 避免程式碼重複的情形。
  • 提高使用網域層類別的類別可讀性。
  • 藉此提升應用程式的測試能力。
  • 可透過分擔責任以避免大型類別。

為保持這些類別的簡單與輕量化,每個用途應只負責處理單一功能,同時它們不應該包含可變動資料。相反,您應該在使用者介面或是資料層處理可變動資料。

本指南中的命名慣例

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

現在時態中的動詞 + noun/what (選用) + UseCase

例如:FormatDateUseCaseLogOutUserUseCaseGetLatestNewsWithAuthorsUseCaseMakeLoginRequestUseCase

依附元件

在典型的應用程式架構中,適用使用者介面層的 ViewModels 與資料層的存放區之間的用途類別。這表示用途類別 通常取決於存放區類別,而且這些類別與存放區的通訊方式 會與存放區相同,使用回呼 (Java) 或處理常式 (Kotlin) 的方式。若要瞭解詳情,請參閱資料層 頁面

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

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

由於用途包含可重複使用的邏輯,因此其他用途也可使用這些用途。網域層有多個用途等級是正常的情況。舉 例來說,如果使用者介面層的多個類別依賴時區, 以便在螢幕上顯示適當的訊息,下面範例定義的用途可以利用 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 文件

生命週期

用途本身沒有生命週期。相反,它們的範圍限定於使用該用途的類別。這表示您可以透過使用者介面層中的類別、透過服務或透過 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 清單中的所有項目;因此,即使資料層是安全的,但您不知道它會處理的項目會有多少,因此這項作業不應該封鎖主要執行緒。因此,用途會使用預設調度員將作業移至背景執行緒。

其他消費者

除了使用者介面層以外,網域層可供其他類別 (例如服務和 Application 類別) 重複使用。此外,如果平台 (例如電視或 Wear) 與行動應用程式共用程式碼集,它們的使用者介面層也能重複使用用途,藉此獲得網域層的所有上述優點。

測試

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