Antipattern repositori di Android

Terjemahan artikel disiapkan untuk mengantisipasi dimulainya kursus "Pengembang Android. Profesional " .








Panduan Arsitektur Aplikasi Android Resmi merekomendasikan penggunaan kelas Repositori untuk "menyediakan API yang bersih sehingga aplikasi lainnya dapat dengan mudah mengambil data". Namun, menurut saya, jika Anda menggunakan pola ini dalam proyek Anda, Anda dijamin akan terjebak dalam kode spaghetti yang berantakan.



Dalam artikel ini, saya akan memberi tahu Anda tentang "Pola repositori" dan menjelaskan mengapa itu sebenarnya merupakan anti-pola untuk aplikasi Android.



Gudang



Panduan Arsitektur Aplikasi yang disebutkan di atas merekomendasikan struktur berikut untuk mengatur logika tingkatan presentasi:







Peran objek repositori dalam struktur ini adalah sebagai berikut:



Modul repositori menangani operasi data. Mereka menyediakan API yang bersih sehingga aplikasi lainnya dapat mengambil data ini dengan mudah. Mereka tahu di mana mendapatkan data dan panggilan API apa yang harus dilakukan ketika diperbarui. Anda dapat menganggap repositori sebagai perantara antara berbagai sumber data seperti model persisten, layanan web, dan cache.



Pada dasarnya, panduan ini merekomendasikan penggunaan repositori untuk mengabstraksi sumber data di aplikasi Anda. Kedengarannya sangat masuk akal dan bahkan berguna, bukan?



Namun, jangan lupa bahwa mengobrol bukanlah membuang tas (dalam hal ini, menulis kode), tetapi untuk mengungkap topik arsitektur menggunakan diagram UML - terlebih lagi. Ujian sesungguhnya dari setiap pola arsitektur adalah implementasi dalam kode dan kemudian mengidentifikasi keuntungan dan kerugiannya. Jadi mari kita cari sesuatu yang kurang abstrak untuk ditinjau.



Repositori di Blueprints Arsitektur Android v2



Sekitar dua tahun lalu, saya meninjau "versi pertama" dari Cetak Biru Arsitektur Android. Secara teori, mereka seharusnya menerapkan contoh MVP yang bersih, tetapi dalam praktiknya, cetak biru ini menghasilkan basis kode yang agak kotor. Mereka memang berisi antarmuka bernama View dan Presenter, tetapi tidak menetapkan batas arsitektural apa pun, jadi itu pada dasarnya bukan MVP. Anda dapat melihat review kode yang diberikan di sini .



Sejak itu, Google telah memperbarui cetak biru arsitektur menggunakan Kotlin, ViewModel, dan praktik "modern" lainnya, termasuk repositori. Cetak biru yang diperbarui ini telah diawali dengan v2.



Mari kita lihat antarmuka TasksRepository dari cetak biru v2:



interface TasksRepository {
   fun observeTasks(): LiveData<Result<List<Task>>>
   suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>
   suspend fun refreshTasks()
   fun observeTask(taskId: String): LiveData<Result<Task>>
   suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>
   suspend fun refreshTask(taskId: String)
   suspend fun saveTask(task: Task)
   suspend fun completeTask(task: Task)
   suspend fun completeTask(taskId: String)
   suspend fun activateTask(task: Task)
   suspend fun activateTask(taskId: String)
   suspend fun clearCompletedTasks()
   suspend fun deleteAllTasks()
   suspend fun deleteTask(taskId: String)
}


Bahkan sebelum membaca kode, Anda dapat memperhatikan ukuran antarmuka ini - ini sudah merupakan panggilan bangun. Sejumlah metode dalam satu antarmuka akan menimbulkan pertanyaan bahkan dalam proyek Android besar, tetapi kita berbicara tentang aplikasi ToDo dengan hanya 2000 baris kode. Mengapa aplikasi yang agak sepele ini membutuhkan kelas dengan permukaan API yang begitu besar?



Repositori sebagai Objek Dewa



Jawaban atas pertanyaan dari bagian sebelumnya tercakup dalam nama metode TasksRepository. Saya secara kasar dapat membagi metode antarmuka ini menjadi tiga kelompok yang tidak tumpang tindih.



Grup 1:



fun observeTasks(): LiveData<Result<List<Task>>>
   fun observeTask(taskId: String): LiveData<Result<Task>>


Kelompok 2:



   suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>
   suspend fun refreshTasks()
   suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>
   suspend fun refreshTask(taskId: String)
   suspend fun saveTask(task: Task)
   suspend fun deleteAllTasks()
   suspend fun deleteTask(taskId: String)


Kelompok 3:



  suspend fun completeTask(task: Task)
   suspend fun completeTask(taskId: String)
   suspend fun clearCompletedTasks()
   suspend fun activateTask(task: Task)
   suspend fun activateTask(taskId: String)


Sekarang mari kita tentukan area tanggung jawab masing-masing grup di atas.



Grup 1 pada dasarnya adalah implementasi pola Observer menggunakan fasilitas LiveData. Grup 2 adalah gerbang ke datastore ditambah dua metode refreshyang diperlukan karena datastore jauh tersembunyi di balik repositori. Grup 3 berisi metode fungsional yang pada dasarnya mengimplementasikan dua bagian dari logika domain aplikasi (penyelesaian tugas dan aktivasi).



Jadi antarmuka yang satu ini memiliki tiga tanggung jawab yang berbeda. Tidak heran ini sangat besar. Dan meskipun dapat dikatakan bahwa keberadaan grup pertama dan kedua sebagai bagian dari satu antarmuka dapat diterima, menambahkan yang ketiga tidak dapat dibenarkan. Jika proyek ini perlu dikembangkan lebih lanjut dan menjadi aplikasi Android yang nyata, kelompok ketiga akan tumbuh secara proporsional dengan jumlah aliran domain dalam proyek tersebut. Hmm.



Kami memiliki istilah khusus untuk kelas yang berbagi begitu banyak tanggung jawab: Benda-benda ilahi. Ini adalah anti-pola yang tersebar luas di aplikasi Android. Activitie dan Fragment adalah tersangka standar dalam konteks ini, tetapi kelas lain dapat merosot menjadi objek Ilahi juga. Apalagi jika namanya diakhiri dengan "Manager", bukan?



Tunggu ... Saya rasa saya menemukan nama yang lebih baik untuk TasksRepository:



interface TasksManager {
   fun observeTasks(): LiveData<Result<List<Task>>>
   suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>
   suspend fun refreshTasks()
   fun observeTask(taskId: String): LiveData<Result<Task>>
   suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>
   suspend fun refreshTask(taskId: String)
   suspend fun saveTask(task: Task)
   suspend fun completeTask(task: Task)
   suspend fun completeTask(taskId: String)
   suspend fun activateTask(task: Task)
   suspend fun activateTask(taskId: String)
   suspend fun clearCompletedTasks()
   suspend fun deleteAllTasks()
   suspend fun deleteTask(taskId: String)
}


Sekarang nama antarmuka ini mencerminkan tanggung jawabnya dengan lebih baik!



Repositori anemia



Di sini Anda mungkin bertanya: "Jika saya menarik logika domain keluar dari repositori, apakah itu akan menyelesaikan masalah?" Nah, kembali ke "diagram arsitektur" dari manual Google.



Jika Anda completeTaskingin mengekstrak, katakanlah, metode dari TasksRepository, di mana Anda akan meletakkannya? Menurut "arsitektur" yang direkomendasikan Google, Anda perlu memindahkan logika ini ke salah satu ViewModels Anda. Tampaknya ini bukan keputusan yang buruk, tetapi sebenarnya memang demikian.



Misalnya, bayangkan Anda meletakkan logika ini ke dalam satu ViewModel. Kemudian, setelah sebulan, pengelola akun Anda ingin mengizinkan pengguna menyelesaikan tugas dari beberapa layar (ini relevan untuk semua pengelola Daftar Tugas yang pernah saya gunakan). Logika di dalam ViewModel tidak dapat digunakan kembali, jadi Anda perlu menduplikasinya atau mengembalikannya ke TasksRepository. Jelas, kedua pendekatan itu buruk.



Pendekatan yang lebih baik adalah mengekstrak aliran domain ini ke dalam objek khusus dan kemudian meletakkannya di antara ViewModel dan repositori. Kemudian ViewModels yang berbeda akan dapat menggunakan kembali objek itu untuk menjalankan utas tertentu itu. Objek ini dikenal sebagai "kasus penggunaan" atau "interaksi"... Namun, jika Anda menambahkan kasus penggunaan ke basis kode Anda, repositori pada dasarnya menjadi template yang tidak berguna. Apa pun yang mereka lakukan, itu akan lebih cocok dengan kasus penggunaan. Gabor Varadi telah membahas topik ini di artikel ini , jadi saya tidak akan membahasnya secara detail. Saya berlangganan hampir semua yang dia katakan tentang "repositori anemia."



Tetapi mengapa kasus penggunaan jauh lebih baik daripada repositori? Jawabannya sederhana: kasus penggunaan merangkum aliran terpisah. Karenanya, alih-alih satu repositori (untuk setiap konsep domain) yang secara bertahap tumbuh menjadi objek Ilahi, Anda akan memiliki beberapa kelas kasus penggunaan yang sangat ditargetkan. Jika streaming bergantung pada jaringan dan data yang disimpan, Anda dapat meneruskan abstraksi yang sesuai ke kelas use case dan itu akan "menengahi" di antara sumber-sumber ini.



Secara umum, sepertinya satu-satunya cara untuk mencegah degradasi repositori ke kelas Divine sambil menghindari abstraksi yang tidak perlu adalah dengan membuang repositori.



Repositori di luar Android.



Sekarang Anda mungkin bertanya-tanya apakah repositori adalah penemuan Google. Tidak, mereka bukan. Pola repositori telah dijelaskan jauh sebelum Google memutuskan untuk menggunakannya dalam panduan arsitekturnya.



Misalnya, Martin Fowler menggambarkan repositori dalam bukunya, Patterns of Enterprise Application Architecture. Blognya juga memiliki artikel tamu yang menjelaskan konsep yang sama. Menurut Fowler, repositori hanyalah pembungkus di sekitar tingkat penyimpanan yang menyediakan antarmuka kueri tingkat tinggi dan mungkin penyimpanan dalam cache dalam memori. Saya akan mengatakan bahwa dari sudut pandang Fowler, repositori berperilaku seperti ORM.



Eric Evans juga mendeskripsikan repositori dalam bukunya Domain Driven Design. Dia menulis:



, , , β€” . , . , , .


Perhatikan bahwa Anda dapat mengganti "repositori" dalam kutipan di atas dengan "ORM Ruangan" dan ini akan tetap masuk akal. Jadi, dalam konteks Desain Didorong Domain, repositori adalah ORM (diimplementasikan secara manual atau menggunakan kerangka kerja pihak ketiga).



Seperti yang Anda lihat, repositori tidak ditemukan di dunia Android. Ini adalah pola desain yang sangat wajar di mana semua kerangka kerja ORM dibangun. Perhatikan, bagaimanapun, apa yang bukan repositori: tidak ada "klasik" yang pernah berpendapat bahwa repositori harus mencoba mengabstraksikan perbedaan antara akses jaringan dan akses database.



Nyatanya, saya yakin mereka akan menganggap ide ini naif dan merugikan diri sendiri. Untuk memahami alasannya, Anda dapat membaca artikel lain, kali ini oleh Joel Spolsky (pendiri StackOverflow), berjudul"Hukum abstraksi bocor . " Sederhananya: jaringan terlalu berbeda dari akses database ke abstrak tanpa kebocoran yang signifikan.



Bagaimana repositori menjadi anti-pola di Android



Jadi, apakah Google salah menafsirkan pola repositori dan memperkenalkan ide naif untuk mengabstraksi akses jaringan ke dalamnya? Aku meragukan itu.



Saya menemukan tautan tertua ke antipattern ini di repositori GitHub ini , yang sayangnya merupakan sumber yang sangat populer. Saya tidak tahu apakah anti-pola ini ditemukan oleh penulis khusus ini, tetapi sepertinya repo inilah yang mempopulerkan gagasan umum dalam ekosistem Android. Pengembang Google mungkin mendapatkannya dari sana atau dari salah satu sumber sekunder.



Kesimpulan



Jadi, repositori di Android sudah menjadi anti-pola. Ini terlihat bagus di atas kertas, tetapi menjadi bermasalah bahkan dalam aplikasi sepele dan dapat menyebabkan masalah nyata dalam proyek yang lebih besar.



Misalnya, di cetak biru Google lainnya, kali ini untuk komponen arsitektur, penggunaan repositori pada akhirnya menghasilkan permata seperti NetworkBoundResource . Perlu diingat bahwa browser sampel GitHub masih merupakan aplikasi ~ 2 KLOC kecil.



Sejauh yang saya tahu, "pola repositori" seperti yang didefinisikan dalam dokumen resmi tidak kompatibel dengan kode yang bersih dan dapat dipelihara.



Terima kasih telah membaca dan seperti biasa Anda dapat meninggalkan komentar dan pertanyaan Anda di bawah ini.






All Articles