Masalah pelapisan repositori dan konsistensi status
Yang benar-benar sulit untuk dikelola adalah cache lokal, status memori, pengembalian paket jarak jauh, dan status turunan UI semuanya diam-diam menulis "kebenaran"
Ketika banyak proyek Android berada dalam kebingungan, reaksi pertama adalah terus menambahkan lapisan.
ViewModel -> UseCase -> Repository -> LocalDataSource -> RemoteDataSource Saat string ini ditata, kodenya memang terlihat lebih rapi. Masalahnya, kerapian dan konsistensi bukanlah hal yang sama. Banyak tim yang menjadikan Repositori semakin seperti “portal terpadu”, namun pada akhirnya mereka menemukan bahwa status halaman lebih sulit untuk disimpulkan: daftar dan detailnya tidak konsisten, status pengumpulan melompat-lompat, UI tidak berubah setelah permintaan berhasil, dan sekumpulan data lama muncul setelah proses dibangun kembali.
Penilaian saya adalah: **Nilai pelapisan Repositori terletak pada sumber status yang jelas dan batasan penulisan. Selama cache memori, database lokal, pengembalian paket jarak jauh, dan status turunan UI semuanya dapat mengubah nilainya, semakin rapi lapisannya, semakin sulit menjaga konsistensi status. **
Masalah sebenarnya adalah ada lebih dari satu kebenaran
Situasi yang umum terjadi adalah Repositori dapat “menyatukan manajemen data”, dan hal ini hanya setengah benar.
Tentu saja, Repositori dapat mengemas jaringan, cache lokal, dan persistensi disk, tetapi jika Anda tidak terus bertanya “siapa sumber kebenarannya”, Repositori hanya menggabungkan beberapa status ke dalam nama kelas yang sama.
Jalur paling umum di luar kendali adalah ini:
- Halaman tersebut membaca database lokal terlebih dahulu dan segera menampilkan nilai lama;
- Memulai permintaan jarak jauh pada saat yang sama, dan memperbarui cache memori setelah mengembalikan paket;
- Untuk menjaga kelancaran interaksi, pertama-tama ubah status UI secara langsung dan lakukan pembaruan optimis;
- Halaman lain membaca nilai lain dari bidang tunggal Repositori;
- Akhirnya, pengunduhan database asinkron selesai, dan halaman lama didorong kembali.
Saat ini, apa yang Anda lihat di permukaan adalah bahwa “arsitektur sudah lengkap secara hierarki”. Faktanya, sudah ada empat negara dalam sistem yang bersaing untuk mendapatkan hak menafsirkan.
Mereka menjawab pertanyaan yang berbeda masing-masing:
- Basis data ingin menjawab “Dapatkah dipulihkan saat dijalankan berikutnya?”;
- Cache memori ingin menjawab “Apakah akses ini cepat?”;
- Remote end mengembalikan paket dan ingin menjawab “Apa yang baru saja dikatakan server?”;
- Status UI ingin menjawab “bagaimana seharusnya antarmuka dirender saat ini”.
Semua hal itu penting, namun menjadi penting bukan berarti semuanya bisa menjadi sumber kebenaran.
Jika tidak ada definisi yang jelas tentang “siapa yang bertanggung jawab atas nilai-nilai kebenaran yang terus-menerus, siapa yang hanya bertanggung jawab atas presentasi turunan, dan siapa yang hanya bisa membaca tetapi tidak bisa menulis”, Repositori perlahan-lahan akan merosot menjadi stasiun transfer negara. Ia menangkap semua kerumitan namun tidak menghilangkan satupun.
Repositori paling mudah disalahgunakan sebagai koordinator yang “dapat mengubah apa pun”
Masalah dengan banyak kode bukan karena Repositorinya terlalu tipis, namun terlalu kuat.
Repositori tipikal sering kali melakukan hal-hal berikut secara bersamaan:
- Membuat permintaan jaringan;
- Membaca dan menulis Kamar;
- Pertahankan peta memori; -Bidang diperlukan untuk merakit UI;
- Kembalikan pembaruan optimis jika terjadi kegagalan;
- Kirim acara dengan mudah untuk memberi tahu modul lain agar disegarkan.
Tampaknya sangat terkonsentrasi, tetapi sebenarnya itu adalah “lapisan akses data”, “lapisan koordinasi negara”, “lapisan strategi caching”, dan “lapisan aturan domain” yang digabung menjadi satu.
Setelah Repositori bertanggung jawab atas “agregasi baca” dan “koordinasi penulisan multi-sumber”, secara alami Repositori akan memasuki keadaan yang canggung: siapa pun dapat mengubah data melaluinya, namun tidak ada yang dapat dengan cepat mengetahui pengamat mana yang pada akhirnya akan terpengaruh oleh perubahan tersebut dan jalur tulis balik mana yang akan dipicu.
Misalnya, operasi pengumpulan, banyak implementasinya seperti ini:
suspend fun toggleFavorite(id: String) {
memory[id] = !(memory[id] ?: false)
dao.updateFavorite(id, memory[id]!!)
api.toggleFavorite(id)
}
Kode ini memang pendek, tetapi menggabungkan tiga tingkat semantik:
- UI ingin segera memberikan masukan, jadi ubah memorinya terlebih dahulu;
- Saya ingin menjaga konsistensi lokal, jadi saya segera menulis perpustakaannya;
- Server adalah wasit sebenarnya, namun hasilnya dikembalikan di akhir.
Masalahnya bukan bahwa “ubah lokal terlebih dahulu” harus salah, tetapi semantik kegagalan tidak ditentukan.
Bagaimana cara konvergen jika antarmuka habis tetapi server benar-benar berhasil? Jika penulisan lokal berhasil tetapi penulisan jarak jauh gagal, siapa yang akan melakukan rollback? Jika dua halaman diklik sebagai favorit secara bersamaan, halaman manakah yang akan menang pada akhirnya?
Ketika masalah ini tidak dirancang secara eksplisit, Repositori hanya menyembunyikan kondisi balapan dengan metode yang tampak bersih.
Aliran dapat menyebarkan keadaan, yang tidak berarti aliran tersebut secara otomatis menjamin konsistensi.
Dalam beberapa tahun terakhir, Android gemar menghubungkan Flow, StateFlow, dan SharedFlow ke Repositori, lalu mengekspos “sumber data responsif” ke upstream. Hal ini tentunya lebih baik daripada menelepon kembali ke mana pun, namun sering kali menimbulkan ilusi bahwa selama saya mengalirkan data, masalah konsistensi akan hilang.
Tidak akan.
Aliran responsif menentukan bagaimana perubahan disebarkan, bukan siapa yang menentukan perubahan.
Pola berikut ini sangat umum:
val userFlow = combine(
dao.observeUser(id),
memoryStateFlow,
remoteRefreshStateFlow
) { local, memory, remote ->
mergeUser(local, memory, remote)
}
Risiko terbesar dari kode ini bukanlah karena tulisannya jelek, tetapi mergeUser() sering kali secara diam-diam memperkenalkan keputusan bisnis:
- Nama ini didasarkan pada ujung jarak jauh;
- Apakah online atau tidak tergantung pada memori;
- Apakah sudah dibaca akan ditentukan secara lokal;
- Status pemuatan juga digantung di UI.
Yang dibutuhkan pada akhirnya adalah “hasil jahitan yang saat ini hampir tidak dapat merender halaman”.
Jenis penyambungan ini sangat nyaman pada jalur baca, namun dapat dengan mudah lepas kendali pada jalur tulis, karena sudah sulit untuk dijawab:
- Level manakah yang harus diubah pada bidang tertentu?
- Setelah satu lapisan berubah, apakah lapisan lain perlu disinkronkan?
- Setelah proses dibangun kembali, bidang mana yang masih dapat dibangun kembali;
- Bidang mana yang akan ditimpa dengan nilai baru selama pemulihan offline.
Jadi fenomena aneh di banyak proyek adalah: semakin indah aliran data ditulis, semakin metafisik bug statusnya. Akar permasalahannya adalah tidak ada satu pun sumber negara yang dapat dipertanggungjawabkan dalam sistem.
Yang benar-benar harus dikontrol adalah batas penulisannya
Batasan paling penting dalam desain Repositori adalah “jika ada izin menulis”.
Jika objek bisnis dapat dimodifikasi oleh pembaruan optimis UI, dimodifikasi oleh cache memori Repositori, didorong kembali oleh pengamat basis data, dan ditimpa oleh paket pengembalian antarmuka, cepat atau lambat objek tersebut akan mengalami masalah ketidakkonsistenan pesanan.
Daripada terus menambahkan abstraksi, saya sarankan untuk memperjelas batasan penulisan terlebih dahulu:
1. Pilih sumber kebenarannya terlebih dahulu
Tidak semua skenario memerlukan “database lokal adalah satu-satunya sumber kebenaran”, namun sumber utama harus dipilih.
- Dalam skenario di mana prioritas offline dan pemulihan daftar dimungkinkan, database lokal biasanya harus digunakan;
- Dalam skenario di mana waktu nyata kuat dan nilai-nilai lama tidak dapat diterima, hasil jarak jauh dapat digunakan;
- Status interaksi antarmuka murni, seperti perluasan, pemilihan, dan masukan, harus secara eksplisit dibiarkan dalam status UI dan tidak dituangkan kembali ke dalam Repositori.
Kuncinya adalah tidak bergantung pada database untuk menentukan separuh bidang, separuh bidang lainnya ditentukan di memori, lalu mengandalkan UI untuk mengisi lubang saat terjadi kesalahan.
2. Pisahkan “status turunan” dan “status persisten”
Banyak kebingungan yang timbul saat menulis kembali status tampilan sementara ke lapisan persistensi.
Misalnya:
isLoadingisRefreshingisExpandedpendingRetryCount
Status-status ini dapat menentukan bagaimana UI digambar, namun status-status tersebut tidak boleh dicampur dengan nilai-nilai kebenaran bisnis dalam entitas yang sama dan disebarkan ke mana-mana.
Setelah status turunan dimasukkan ke dalam model publik Repositori, status tersebut akan digunakan kembali secara keliru di antara halaman berbeda dan siklus hidup berbeda. Pada akhirnya, bahkan tidak jelas apakah “bidang ini masih mempertahankan nilai halaman terakhir”.
3. Buatlah jalur penulisan lebih kecil dari jalur membaca
Pembacaan dapat diagregasi dan penulisan dapat ditutup.
Anda dapat menyatukan database, memori, dan sinyal penyegaran jarak jauh saat membaca untuk memberikan model yang memadai pada halaman; tetapi saat menulis, yang terbaik adalah mengambil satu jalur terkontrol dan membiarkannya memutuskan:
- Apakah akan menulis secara lokal terlebih dahulu;
- Apakah kompensasi diperlukan; -Apakah diperbolehkan untuk menimpa versi lama;
- Apakah akan menyertakan nomor versi atau stempel waktu;
- Semantik apa yang harus dilihat UI setelah kegagalan.
Semakin banyak entri tulis yang diizinkan sistem, semakin banyak konsistensi yang bergantung pada “jangan membuat kesalahan”. Ini bukan desain, ini keberuntungan.
Contoh tandingan yang umum: Untuk “mengalami kehalusan sutra”, lakukan perubahan terlebih dahulu sebelum membicarakannya
Cara termudah untuk menulis konsistensi keadaan buruk adalah keputusan kecil “interaksi ini sangat sederhana, mari kita ubah secara lokal terlebih dahulu”.
Misalnya suka, koleksi, mengikuti, dan membaca terlalu mudah untuk dianggap sebagai “ubah UI dulu, lalu gagal”. Masalahnya adalah ketika mereka melintasi halaman, melintasi daftar, dan melintasi tingkatan caching, itu bukan lagi keputusan kecil.
Kasus kegagalan biasanya terlihat seperti ini:
- Klik Favorit di halaman detail, dan tombol akan langsung menyala;
- Halaman daftar juga memantau status memori Repositori yang sama, sehingga menyala secara serempak;
- Waktu antarmuka habis dan Repositori memicu rollback;
- Tetapi halaman daftar sudah mendapatkan nilai lama karena pengamat database, dan urutan rollback berbeda dari halaman detail;
- Pengguna kembali ke level sebelumnya dan melihat bahwa status kedua halaman tidak konsisten;
- Setelah proses mematikan dimulai kembali, hasilnya akan kembali ke hasil ketiga.
Hal yang paling menjengkelkan tentang masalah seperti ini adalah masalah ini tidak selalu terulang, sehingga tim dapat dengan mudah mengaitkannya dengan “Masalah waktu aliran”, “Masalah reorganisasi penulisan”, atau “fluktuasi jaringan sporadis”.
Faktanya, akar masalahnya lebih sederhana: ** memungkinkan beberapa lapisan memiliki kualifikasi untuk menulis hasil akhir pada saat yang bersamaan. **
Pelayanan ini berlapis untuk akuntabilitas, bukan kerapian formal
Saya tidak menentang pelapisan Repositori. Tanpa Repositori, banyak proyek Android akan menjadi lebih berantakan.
Namun apa yang sebenarnya harus disediakan oleh Repositori adalah:
- Bisakah Anda menjelaskan dari mana jalur membaca itu berasal? -Tuliskan keputusan apa saja yang dilalui dan apakah pertanggungjawaban dapat dilakukan;
- Siapa yang akan menang jika terjadi kesalahan dan apakah kesalahan tersebut dapat dipulihkan;
- Yang dibagikan antar halaman adalah nilai bisnis sebenarnya atau status tampilan sementara, dan apakah dapat dipisahkan.
Jika pertanyaan-pertanyaan ini tidak dapat dijawab, betapapun indahnya layeringnya, itu hanya akan menjadi tatanan visual.
Hal ini membuat kode terlihat lebih seperti diagram arsitektur, namun tidak selalu membuat keadaan terlihat lebih seperti sebuah sistem.
Batasan yang berlaku
Artikel ini terutama berfokus pada:
- Memiliki cache atau Kamar lokal;
- Status berbagi beberapa halaman;
- Secara bersamaan mengejar kecepatan layar pertama, pemulihan offline, dan umpan balik interaktif instan;
- Gunakan Repositori + Flow/StateFlow untuk mengatur pembacaan dan penulisan data.
Jika aplikasinya sangat ringan, datanya hampir selalu berupa permintaan satu kali, halaman siap digunakan, dan tidak diperlukan sinkronisasi lintas halaman, bahkan jika Repositori ditulis dengan sederhana dan kasar, masalah konsistensi status tidak akan terlalu menonjol.
Masalah sebenarnya adalah untuk proyek-proyek yang “menengah hingga besar tetapi belum cukup besar untuk sepenuhnya diplatformkan”: semakin banyak fungsi, dan sumber data menjadi semakin kompleks, namun tim masih menggunakan metode awal “paketkan lapisan Repositori terlebih dahulu dan kemudian bicarakan” untuk mendukungnya. Pada tahap ini kemungkinan besar akan muncul sistem yang strukturnya rapi dan perilakunya kacau.
Ringkasan
Kesalahpahaman paling umum mengenai pelapisan Repositori di Android adalah kesalahan “pintu masuk akses terpadu” dengan “keadaan alami terpadu”.
Pintu masuk terpadu hanya dapat mengurangi kebingungan di permukaan panggilan; hanya dengan membersihkan sumber kebenaran, menutup batas penulisan, dan mendefinisikan semantik kegagalan terlebih dahulu kita dapat benar-benar mengurangi perselisihan antar negara.
Jika tidak, apa yang dibutuhkan adalah serangkaian hal yang tersusun rapi dan lebih sulit untuk dimintai pertanggung jawaban.
What to read next
Want more posts about Android?
Posts in the same category are usually the best next step for reading more on this topic.
View same categoryWant to keep following #State Management?
Tags are useful for related tools, specific problems, and similar troubleshooting notes.
View same tagWant to explore another direction?
If you are not sure what to read next, return to the homepage and start from categories, topics, or latest updates.
Back home