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 kho 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
) { /* ... */ }
GetLatestNewsWithAuthorsUseCase phụ thuộc vào lớp kho 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 thực thể 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 đối tượng 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 phụ thuộc vào hai lớp lưu trữ 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ẻ cơ sở mã 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.

Hạn chế quyền truy cập lớp dữ liệu

Một điều khác cần cân nhắc khi triển khai lớp miền là bạn vẫn nên cấp quyền truy cập trực tiếp vào lớp dữ liệu từ lớp giao diện người dùng hay buộc mọi thứ thông qua lớp miền.

Lớp giao diện người dùng không thể truy cập trực tiếp vào lớp dữ liệu, lớp này phải đi qua lớp Miền
Hình 4. Biểu đồ phần phụ thuộc cho thấy lớp giao diện người dùng đang bị từ chối quyền truy cập vào lớp dữ liệu.

Lợi thế của việc hạn chế này là ngăn giao diện người dùng bỏ qua logic lớp miền, chẳng hạn, nếu bạn đang thực hiện ghi nhật ký dữ liệu phân tích trên mỗi yêu cầu quyền truy cập vào lớp dữ liệu.

Tuy nhiên, điểm bất lợi đáng kể tiềm ẩn là thao tác này buộc bạn phải thêm các trường hợp sử dụng ngay cả khi đó chỉ là các lệnh gọi hàm đơn giản đến lớp dữ liệu. Điều này có thể làm tăng thêm độ phức tạp mà không mang lại nhiều lợi ích.

Một phương pháp hay là chỉ thêm các trường hợp sử dụng khi cần. Nếu nhận thấy lớp giao diện người dùng đang truy cập dữ liệu thông qua các trường hợp sử dụng gần như độc quyền, thì bạn chỉ nên truy cập vào dữ liệu theo cách này.

Cuối cùng, quyết định hạn chế quyền truy cập vào lớp dữ liệu sẽ phụ thuộc vào từng cơ sở mã của bạn và liệu bạn muốn áp dụng quy tắc nghiêm ngặt hơn hay phương pháp linh hoạt hơn.

Kiểm thử

Hướng dẫn kiểm thử chung áp dụng khi kiểm thử lớp miền. Đối với các hoạt động kiểm thử khác về giao diện người dùng, 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 kiểm thử lớp miền.

Mẫu

Các mẫu sau đây của Google minh hoạ cách sử dụng lớp miền. Hãy khám phá những mẫu đó để xem hướng dẫn này trong thực tế: