Lapisan domain

Lapisan domain adalah lapisan opsional yang berada di antara lapisan UI dan lapisan data.

Jika disertakan, lapisan domain opsional memberikan dependensi ke
    lapisan UI dan bergantung pada lapisan data.
Gambar 1. Peran lapisan domain dalam arsitektur aplikasi.

Lapisan domain bertanggung jawab untuk mengenkapsulasi logika bisnis yang kompleks, atau logika bisnis sederhana yang digunakan kembali oleh beberapa ViewModels. Lapisan ini bersifat opsional karena tidak semua aplikasi akan memiliki persyaratan ini. Anda hanya boleh menggunakannya jika diperlukan, misalnya, untuk menangani kompleksitas atau mendukung penggunaan kembali.

Lapisan domain memberikan manfaat berikut:

  • Menghindari duplikasi kode.
  • Meningkatkan keterbacaan di class yang menggunakan class lapisan domain.
  • Meningkatkan kemudahan pengujian aplikasi.
  • Menghindari class yang besar dengan mengizinkan Anda memisahkan tanggung jawab.

Agar class ini tetap sederhana dan ringan, setiap kasus penggunaan hanya memiliki tanggung jawab atas satu fungsi, dan tidak boleh berisi data yang dapat berubah. Sebagai gantinya, Anda harus menangani data yang dapat berubah di lapisan data atau UI.

Konvensi penamaan dalam panduan ini

Dalam panduan ini, kasus penggunaan dinamai menurut tindakan tunggal yang menjadi tanggung jawabnya. Konvensinya adalah sebagai berikut:

kata kerja dalam bentuk masa kini + kata benda/apa (opsional) + UseCase.

Misalnya: FormatDateUseCase, LogOutUserUseCase, GetLatestNewsWithAuthorsUseCase, atau MakeLoginRequestUseCase.

Dependensi

Dalam arsitektur aplikasi standar, class kasus penggunaan berada di antara ViewModel dari lapisan UI dan repositori dari lapisan data. Ini berarti bahwa class kasus penggunaan biasanya bergantung pada class repositori, dan berkomunikasi dengan lapisan UI sama seperti yang dilakukan repositori, yaitu menggunakan callback (untuk Java) atau coroutine (untuk Kotlin). Untuk mempelajari hal ini lebih lanjut, lihat halaman lapisan data.

Misalnya, di aplikasi, Anda mungkin memiliki class kasus penggunaan yang mengambil data dari repositori berita dan repositori penulis, lalu menggabungkannya:

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

Karena kasus penggunaan berisi logika yang dapat digunakan kembali, kasus tersebut juga dapat digunakan oleh kasus penggunaan lainnya. Memiliki beberapa tingkat kasus penggunaan di lapisan domain adalah hal yang wajar. Misalnya, kasus penggunaan yang ditentukan dalam contoh di bawah dapat memanfaatkan kasus penggunaan FormatDateUseCase jika beberapa class dari lapisan UI bergantung pada zona waktu untuk menampilkan pesan yang tepat pada layar:

class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository,
  private val formatDateUseCase: FormatDateUseCase
) { /* ... */ }
GetLatestNewsWithAuthorsUseCase bergantung pada class repositori dari
    lapisan data, tetapi juga bergantung pada FormatDataUseCase yang merupakan class kasus penggunaan
    lain yang juga berada di lapisan domain.
Gambar 2. Contoh grafik dependensi untuk kasus penggunaan yang bergantung pada kasus penggunaan lainnya.

Kasus penggunaan panggilan di Kotlin

Di Kotlin, Anda dapat membuat instance class kasus penggunaan yang dapat dipanggil sebagai fungsi dengan menentukan fungsi invoke() dengan pengubah operator. Lihat contoh berikut:

class FormatDateUseCase(userRepository: UserRepository) {

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

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

Dalam contoh ini, metode invoke() di FormatDateUseCase memungkinkan Anda untuk memanggil instance class seolah-olah itu adalah fungsi. Metode invoke() tidak dibatasi untuk tanda tangan tertentu. Metode ini dapat mengambil sejumlah parameter dan menampilkan jenis apa pun. Anda juga dapat melebihi beban invoke() dengan tanda tangan yang berbeda di class Anda. Anda akan memanggil kasus penggunaan dari contoh di atas sebagai berikut:

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

Untuk mempelajari operator invoke() lebih lanjut, lihat dokumen Kotlin.

Siklus Proses

Kasus penggunaan tidak memiliki siklus prosesnya sendiri. Sebagai gantinya, kasus penggunaan disertakan ke class yang menggunakannya. Ini berarti Anda dapat memanggil kasus penggunaan dari class di lapisan UI, dari layanan, atau dari class Application itu sendiri. Karena kasus penggunaan tidak boleh berisi data yang dapat berubah, Anda harus membuat instance baru dari class kasus penggunaan setiap kali Anda meneruskannya sebagai dependensi.

Threading

Kasus penggunaan dari lapisan domain harus berupa main-safe; dengan kata lain, kasus penggunaan harus aman untuk dipanggil dari thread utama. Jika class kasus penggunaan menjalankan operasi pemblokiran yang berjalan lama, class tersebut bertanggung jawab memindahkan logika itu ke thread yang sesuai. Namun, sebelum melakukannya, periksa apakah operasi pemblokiran tersebut akan lebih baik jika ditempatkan di lapisan hierarki lainnya. Biasanya, komputasi kompleks terjadi di lapisan data untuk mendorong penggunaan kembali atau caching. Misalnya, operasi yang menggunakan banyak resource pada daftar besar lebih baik ditempatkan di lapisan data daripada di lapisan domain jika hasilnya harus di-cache untuk digunakan kembali di beberapa layar aplikasi.

Contoh berikut menunjukkan kasus penggunaan yang bekerja di thread latar belakang:

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

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

Tugas umum

Bagian ini menjelaskan cara melakukan tugas lapisan domain umum.

Logika bisnis sederhana yang dapat digunakan kembali

Anda harus mengenkapsulasi logika bisnis berulang yang ada di lapisan UI dalam class kasus penggunaan. Hal ini mempermudah penerapan setiap perubahan di mana pun logika tersebut digunakan. Alat ini juga memungkinkan Anda menguji logika secara terpisah.

Perhatikan contoh FormatDateUseCase yang dijelaskan sebelumnya. Jika persyaratan bisnis Anda terkait dengan perubahan pemformatan tanggal di masa mendatang, Anda hanya perlu mengubah kode di satu tempat terpusat.

Menggabungkan repositori

Di aplikasi berita, Anda mungkin memiliki class NewsRepository dan AuthorsRepository yang menangani operasi data berita dan penulis. Class Article yang ditampilkan oleh NewsRepository hanya berisi nama penulis, tetapi Anda ingin menampilkan informasi lebih lanjut tentang penulis di layar. Informasi penulis dapat diperoleh dari AuthorsRepository.

GetRecentNewsWithAuthorsUseCase bergantung pada dua class repositori
    yang berbeda dari lapisan data: NewsRepository dan AuthorsRepository.
Gambar 3. Grafik dependensi untuk kasus penggunaan yang menggabungkan data dari beberapa repositori.

Karena logika melibatkan beberapa repositori dan dapat menjadi kompleks, Anda membuat class GetLatestNewsWithAuthorsUseCase untuk memisahkan logika dari ViewModel dan membuatnya lebih mudah dibaca. Hal ini juga membuat logika lebih mudah untuk diuji secara terpisah dan dapat digunakan kembali di berbagai bagian aplikasi.

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

Logika memetakan semua item dalam daftar news; jadi, meskipun lapisan data berupa main-safe, operasi ini tidak akan memblokir thread utama karena Anda tidak tahu jumlah item yang akan diproses. Itulah mengapa kasus penggunaan memindahkan pekerjaan ke thread latar belakang menggunakan dispatcher default.

Konsumen lainnya

Selain lapisan UI, lapisan domain dapat digunakan kembali oleh class lain seperti layanan dan class Application. Selain itu, jika platform lain seperti TV atau Wear berbagi codebase dengan aplikasi seluler, lapisan UI-nya juga dapat menggunakan kembali kasus penggunaan untuk mendapatkan semua manfaat lapisan domain yang disebutkan di atas.

Pembatasan akses lapisan data

Salah satu pertimbangan lain saat menerapkan lapisan domain adalah apakah Anda masih harus mengizinkan akses langsung ke lapisan data dari lapisan UI, atau memaksa semuanya melalui lapisan domain.

Lapisan UI tidak dapat mengakses lapisan data secara langsung, dan harus melalui lapisan Domain
Gambar 4. Grafik dependensi yang menunjukkan lapisan UI ditolak aksesnya ke lapisan data.

Keuntungan dari membuat batasan ini adalah mencegah UI Anda mengabaikan logika lapisan domain, misalnya, jika Anda melakukan logging analisis pada setiap permintaan akses ke lapisan data.

Namun, terdapat kemungkinan kerugian yang signifikan karena ini memaksa Anda untuk menambahkan kasus penggunaan. Penambahan kecil sekalipun, seperti panggilan fungsi sederhana ke lapisan data, dapat menambah kompleksitas dengan sedikit manfaat.

Pendekatan yang baik adalah menambahkan kasus penggunaan hanya saat diperlukan. Jika Anda menemukan bahwa lapisan UI hampir selalu mengakses data melalui kasus penggunaan, akses ke data mungkin hanya dapat dilakukan dengan cara ini.

Pada akhirnya, keputusan untuk membatasi akses ke lapisan data bergantung pada codebase Anda, dan apakah Anda lebih memilih aturan yang ketat atau pendekatan yang lebih fleksibel.

Pengujian

Panduan pengujian umum berlaku saat Anda menguji lapisan domain. Untuk pengujian UI lainnya, developer biasanya menggunakan repositori palsu, dan praktik yang baik adalah menggunakan repositori palsu saat menguji lapisan domain.

Contoh

Contoh Google berikut menunjukkan penggunaan lapisan domain. Jelajahi untuk melihat panduan ini dalam praktik: