Lớp miền

Lớp miền là lớp không bắt buộc nằm giữa lớp giao diện người dùng và lớp dữ liệu.

Khi được đưa vào, lớp miền không bắt buộc sẽ cung cấp các phần phụ thuộc
    cho lớp giao diện người dùng và phụ thuộc vào lớp dữ liệu.
Hình 1. Vai trò của lớp miền trong cấu trúc ứng dụng.

Lớp miền chịu trách nhiệm về việc tổng hợp các logic nghiệp vụ phức tạp, hoặc logic nghiệp vụ đơn giản được sử dụng lại trong nhiều ViewModel. Lớp này là không bắt buộc vì không phải ứng dụng nào cũng có những yêu cầu này. Bạn chỉ nên sử dụng thuộc tính này khi cần, ví dụ: để xử lý độ phức tạp hoặc ưa chuộng khả năng tái sử dụng.

Lớp miền mang lại các lợi ích sau:

  • Lớp miền giúp tránh việc trùng lặp mã.
  • Lớp miền cải thiện khả năng đọc trong các lớp sử dụng loại đối tượng lớp miền.
  • Lớp miền cải thiện khả năng thử nghiệm của ứng dụng.
  • Lớp miền giúp bạn tránh được các lớp lớn bằng cách cho phép bạn phân chia trách nhiệm.

Để giúp các lớp này đơn giản và nhẹ, mỗi trường hợp sử dụng chỉ nên có trách nhiệm đối với một chức năng duy nhất và không nên chứa dữ liệu có thể thay đổi. Bạn nên xử lý dữ liệu có thể thay đổi trong giao diện người dùng hoặc các lớp dữ liệu.

Quy ước đặt tên trong hướng dẫn này

Trong hướng dẫn này, các trường hợp sử dụng được đặt tên theo từng hành động mà chúng chịu trách nhiệm. Quy ước như sau:

động từ ở thì hiện tại + danh từ/cái gì (không bắt buộc) + UseCase.

Ví dụ: FormatDateUseCase, LogOutUserUseCase, GetLatestNewsWithAuthorsUseCase hoặc MakeLoginRequestUseCase.

Phần phụ thuộc

Trong một cấu trúc ứng dụng thông thường, các loại trường hợp sử dụng phù hợp giữa các ViewModel từ lớp giao diện người dùng và đối tượng lưu trữ từ lớp dữ liệu. Điều này có nghĩa là các lớp trường hợp sử dụng thường phụ thuộc vào lớp lưu trữ và chúng giao tiếp với lớp giao diện người dùng theo cách tương tự như các đối tượng lưu trữ—bằng cách sử dụng các lệnh gọi lại (đối với Java) hoặc coroutine (đối với Kotlin). Để tìm hiểu thêm về điều này, hãy xem trang về lớp dữ liệu.

Ví dụ: trong ứng dụng của mình, bạn có thể có các lớp trường hợp sử dụng tìm nạp dữ liệu từ kho lưu trữ tin tức và kho lưu trữ tác giả, rồi kết hợp các dữ liệu này:

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

Vì các trường hợp sử dụng chứa logic có thể tái sử dụng, nên bạn cũng có thể sử dụng những trường hợp này trong các trường hợp sử dụng khác. Việc có nhiều cấp độ trường hợp sử dụng trong lớp miền là điều bình thường. Ví dụ: trường hợp sử dụng được xác định trong ví dụ dưới đây có thể sử dụng trường hợp sử dụng FormatDateUseCase nếu nhiều lớp từ lớp giao diện người dùng dựa vào múi giờ để hiển thị thông báo thích hợp trên màn hình:

class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository,
  private val formatDateUseCase: FormatDateUseCase
) { /* ... */ }
GetNewNewsWithauthorsUseCase phụ thuộc vào lớp lưu trữ từ
    lớp dữ liệu, nhưng cũng phụ thuộc vào FormatDataUseCase, một lớp trường hợp sử dụng
    khác cũng có trong lớp miền.
Hình 2. Biểu đồ phần phụ thuộc mẫu cho một trường hợp sử dụng phụ thuộc vào các trường hợp sử dụng khác.

Các trường hợp sử dụng lệnh gọi trong Kotlin

Trong Kotlin, bạn có thể gọi các phiên bản lớp trường hợp sử dụng dưới dạng các hàm bằng cách xác định hàm invoke() bằng công cụ sửa đổi operator. Hãy xem ví dụ sau đây:

class FormatDateUseCase(userRepository: UserRepository) {

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

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

Trong ví dụ này, phương thức invoke() trong FormatDateUseCase cho phép bạn gọi các phiên bản lớp như thể chúng là các hàm. Phương thức invoke() không bị giới hạn ở bất kỳ chữ ký cụ thể nào. Phương thức này có thể lấy số lượng thông số bất kỳ và trả về bất kỳ loại nào. Bạn cũng có thể gây quá tải cho invoke() bằng các chữ ký khác nhau trong lớp. Bạn sẽ gọi trường hợp sử dụng từ ví dụ trên như sau:

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

Để tìm hiểu thêm về toán tử invoke(), hãy xem tài liệu Kotlin.

Vòng đời

Các trường hợp sử dụng không có vòng đời riêng. Thay vào đó, các trường hợp này thuộc phạm vi của lớp sử dụng chúng. Điều này có nghĩa là bạn có thể gọi các trường hợp sử dụng từ các lớp trong lớp giao diện người dùng, từ các dịch vụ hoặc từ chính lớp Application. Vì các trường hợp sử dụng không được chứa dữ liệu có thể thay đổi, nên bạn phải tạo một bản sao của trường hợp sử dụng mới mỗi khi bạn chuyển trường hợp đó dưới dạng một phần phụ thuộc.

Luồng

Các trường hợp sử dụng từ lớp miền phải an toàn chính; nói cách khác, những trường hợp sử dụng này phải an toàn để gọi từ luồng chính. Nếu các lớp trường hợp sử dụng thực hiện các thao tác chặn dài hạn, thì chúng có trách nhiệm di chuyển logic đó sang luồng phù hợp. Tuy nhiên, trước khi làm việc đó, hãy kiểm tra xem các thao tác chặn đó có được đặt ở các lớp khác trong hệ thống phân cấp hay không. Thông thường, các phép tính phức tạp diễn ra trong lớp dữ liệu để khuyến khích việc sử dụng lại hoặc lưu vào bộ nhớ đệm. Ví dụ: một thao tác cần nhiều tài nguyên trên một danh sách lớn tốt hơn nên được đặt trong lớp dữ liệu so với trong lớp miền nếu kết quả cần được lưu vào bộ nhớ đệm để sử dụng lại trên nhiều màn hình của ứng dụng.

Ví dụ sau đây cho thấy một trường hợp sử dụng thực hiện tác vụ trên một luồng nền:

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

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

Tác vụ chung

Phần này mô tả cách thực hiện các tác vụ phổ biến trên lớp miền.

Logic kinh doanh đơn giản có thể tái sử dụng

Bạn nên gói gọn logic kinh doanh lặp lại có trong lớp giao diện người dùng trong một lớp trường hợp sử dụng. Thao tác này giúp việc áp dụng mọi thay đổi ở mọi nơi sử dụng logic trở nên dễ dàng hơn. Việc này cũng cho phép bạn thử nghiệm tính riêng biệt của logic.

Hãy xem xét ví dụ về FormatDateUseCase được mô tả ở trên. Nếu sau này, các yêu cầu kinh doanh của bạn liên quan đến cách thay đổi định dạng ngày, thì bạn chỉ cần thay đổi mã ở một nơi tập trung.

Kết hợp các kho lưu trữ

Trong một ứng dụng tin tức, bạn có thể có các lớp NewsRepositoryAuthorsRepository. Các lớp đó xử lý thao tác tin tức và tác giả tương ứng. Lớp ArticleNewsRepository hiển thị chỉ chứa tên của tác giả, nhưng bạn nên hiển thị thêm thông tin về tác giả trên màn hình. Bạn có thể lấy thông tin tác giả từ AuthorsRepository.

GetLatestNewsWithAuthorsUseCase tuỳ thuộc vào hai lớp
    lưu trữ khác nhau từ lớp dữ liệu: NewsRepository và AuthorsRepository.
Hình 3. Biểu đồ phần phụ thuộc cho một trường hợp sử dụng kết hợp dữ liệu từ nhiều kho lưu trữ.

Vì logic liên quan đến nhiều kho lưu trữ và có thể trở nên phức tạp, bạn nên tạo một lớp GetLatestNewsWithAuthorsUseCase để tóm tắt logic từ ViewModel và làm cho logic dễ đọc hơn. Điều này cũng giúp việc thử kiểm tính riêng biệt của logic trở nên dễ dàng hơn và có thể tái sử dụng trong các phần khác nhau của ứng dụng.

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

Logic sẽ ánh xạ tất cả các mục trong danh sách news; do đó, mặc dù lớp dữ liệu là an toàn chính, nhưng công việc này không nên chặn luồng chính vì bạn không biết hệ thống sẽ xử lý bao nhiêu mục. Đó là lý do trường hợp sử dụng sẽ chuyển công việc sang luồng nền bằng cách sử dụng trình điều phối mặc định.

Các trường hợp sử dụng khác

Ngoài lớp giao diện người dùng, các lớp khác có thể tái sử dụng lớp miền, chẳng hạn như dịch vụ và lớp Application. Hơn nữa, nếu các nền tảng khác như TV hoặc Wear chia sẻ mã cơ sở với ứng dụng dành cho thiết bị di động, thì lớp giao diện người dùng của các nền tảng đó cũng có thể tái sử dụng các trường hợp sử dụng để có được tất cả lợi ích nói trên của lớp miền.

Thử nghiệm

Hướng dẫn thử nghiệm chung áp dụng khi thử nghiệm lớp miền. Đối với các thử nghiệm giao diện người dùng khác, nhà phát triển thường sử dụng kho lưu trữ giả, và cũng nên sử dụng kho lưu trữ giả khi thử nghiệm lớp miền.