RESS - Arsitektur Baru untuk Aplikasi Seluler



Berlawanan dengan judul provokatif, ini bukan arsitektur baru, tetapi upaya untuk menerjemahkan praktik yang sederhana dan telah teruji ke dalam bahasa Newspeak, yang dituturkan oleh komunitas Android modern

Pendahuluan


Baru-baru ini, menjadi menyakitkan untuk melihat apa yang terjadi di dunia pengembangan platform mobile. Astronotika arsitektur berkembang pesat, setiap hipster menganggapnya sebagai tugas untuk menciptakan arsitektur baru, dan untuk menyelesaikan tugas sederhana, alih-alih dua baris, masukkan beberapa kerangka kerja yang modis.

Situs-situs ini telah mengisi tutorial tentang kerangka kerja trendi dan arsitektur canggih, tetapi bahkan tidak ada praktik terbaik untuk klien Android REST. Meskipun ini adalah salah satu kasus aplikasi yang paling sering. Saya ingin pendekatan yang normal untuk pengembangan pergi ke massa juga. Karena itu, saya menulis artikel ini

Mengapa solusi yang ada buruk?


Secara umum, masalah MVP bermodel baru, VIPER dan sejenisnya persis sama, penulis mereka tidak tahu cara mendesain. Dan pengikut mereka - bahkan lebih. Dan karena itu mereka tidak mengerti hal-hal penting dan jelas. Dan mereka terlibat dalam teknik-over konvensional.

1. Arsitekturnya harus sederhana


Semakin sederhana semakin baik. Ini membuatnya lebih mudah untuk dipahami, lebih dapat diandalkan dan lebih fleksibel. Orang bodoh mana pun dapat menyulitkan dan membuat banyak abstraksi, tetapi untuk melakukannya secara sederhana, Anda harus berpikir dengan hati-hati.

2. Over-engineering itu buruk


Anda perlu menambahkan level abstraksi baru hanya ketika yang lama tidak menyelesaikan masalah. Setelah menambahkan level baru, sistem seharusnya menjadi lebih mudah dipahami, dan lebih sedikit kode. Jika, misalnya, setelah itu Anda memiliki tiga bukannya satu file, dan sistem menjadi lebih rumit, maka Anda membuat kesalahan, dan jika dengan cara sederhana, Anda menulis sampah .

Penggemar MVP, misalnya, dalam artikel mereka sendiri menulis dalam teks biasa bahwa MVP bodoh menyebabkan komplikasi yang signifikan dari sistem. Dan mereka membenarkannya dengan fakta bahwa itu sangat fleksibel dan lebih mudah dirawat . Tetapi, seperti yang kita ketahui dari paragraf 1, ini adalah hal-hal yang saling eksklusif.

Sekarang tentang VIPER, lihat saja, misalnya, pada diagram dari artikel ini .

Skema
gambar

Dan ini untuk setiap layar! Mata saya sakit. Saya terutama bersimpati dengan mereka yang di tempat kerja harus berurusan dengan ini di luar kehendak mereka. Bagi mereka yang memperkenalkannya sendiri, saya bersimpati dengan alasan yang sedikit berbeda .

Pendekatan baru


Hei, aku juga ingin nama yang modis . Oleh karena itu, arsitektur yang diusulkan disebut RESS - R equest, E vent, S creen, S torage. Surat-surat dan nama-nama itu dirinci begitu bodoh untuk mendapatkan kata yang bisa dibaca. Nah, agar tidak membuat kebingungan dengan nama-nama yang sudah digunakan. Nah, dengan REST yang selaras.

Segera melakukan pemesanan, arsitektur ini untuk klien REST. Untuk jenis aplikasi lain, mungkin tidak akan berfungsi.



1. Penyimpanan


Gudang Data (dalam istilah lain Model, Repositori). Kelas ini menyimpan data dan memprosesnya (menyimpan, memuat, menambahkan ke database, dll.), Serta semua data dari layanan REST pertama kali sampai di sini, diuraikan dan disimpan di sini.

2. Layar


Layar aplikasi, dalam kasus Android, ini adalah Aktivitas Anda. Dalam istilah lain, ini adalah ViewController biasa seperti Apple MVC.

3. Permintaan


Kelas yang bertanggung jawab untuk mengirim permintaan ke layanan REST, serta menerima tanggapan dan memberi tahu tentang tanggapan komponen lain dari sistem.

4. Acara


Tautan antar komponen lainnya. Misalnya, Permintaan mengirim acara tentang respons server kepada mereka yang berlangganan. Dan Storage mengirim acara tentang perubahan data.

Berikut ini adalah contoh implementasi yang disederhanakan. Kode ditulis dengan asumsi dan tidak dicentang, sehingga mungkin ada kesalahan sintaks dan kesalahan ketik

Minta
public class Request { public interface RequestListener { default void onApiMethod1(Json answer) {} default void onApiMethod2(Json answer) {} } private static class RequestTask extends AsyncTask<Void, Void, String> { public RequestTask(String methodName) { this.methodName = methodName; } private String methodName; @Override protected String doInBackground(Void ... params) { URL url = new URL(Request.serverUrl + "/" + methodName); HttpURLConnection httpConnection = (HttpURLConnection)url.openConnection(); // ... //      // ... return result; } @Override protected void onPostExecute(String result) { // ... //  JSON  result // ... Requestr.onHandleAnswer(methodName, json); } } private static String serverUrl = "myserver.com"; private static List<OnCompleteListener> listeners = new ArrayList<>(); private static void onHandleAnswer(String methodName, Json json) { for(RequestListener listener : listeners) { if(methodName.equals("api/method1")) listener.onApiMethod1(json); else if(methodName.equals("api/method2")) listener.onApiMethod2(json); } } private static void makeRequest(String methodName) { new RequestTask(methodName).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } public static void registerListener(RequestListener listener) { listeners.add(listener); } public static void unregisterListener(RequestListener listener) { listeners.remove(listener); } public static void apiMethod1() { makeRequest("api/method1"); } public static void onApiMethod2() { makeRequest("api/method2"); } } 


Penyimpanan
 public class DataStorage { public interface DataListener { default void onData1Changed() {} default void onData2Changed() {} } private static MyObject1 myObject1 = null; private static List<MyObject2> myObjects2 = new ArrayList<>(); public static void registerListener(DataListener listener) { listeners.add(listener); } public static void unregisterListener(DataListener listener) { listeners.remove(listener); } public static User getMyObject1() { return myObject1; } public static List<MyObject2> getMyObjects2() { return myObjects2; } public static Request.RequestListener listener = new Request.RequestListener() { private T fromJson<T>(Json answer) { // ... //    JSON // ... return objectT; } @Override public void onApiMethod1(Json answer) { myObject1 = fromJson(answer); for(RequestListener listener : listeners) listener.data1Changed(); } @Override public void onApiMethod2(Json answer) { myObject2 = fromJson(myObjects2); for(RequestListener listener : listeners) listener.data2Changed(); } }; } 


Layar
 public class MyActivity extends Activity implements DataStorage.DataListener { private Button button1; private Button button2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); button1.setOnClickListener((View) -> { Request.apiMethod1(); }); button2.setOnClickListener((View) -> { Request.apiMethod2(); }); updateViews(); } @Override protected void onPause() { super.onPause(); DataStorage.unregisterListener(this); } @Override protected void onResume() { super.onResume(); DataStorage.registerListener(this); updateViews(); } private void updateViews() { updateView1(); updateView2(); } private void updateView1() { Object1 data = DataStorage.getObject1(); // ... //     // ... } private void updateView2() { List<Object2> data = DataStorage.getObjects2(); // ... //     // ... } @Override public void onData1Changed() { updateView1(); } @Override public void onData2Changed() { updateView2(); } } 


Aplikasi
 public class MyApp extends Application { @Override public void onCreate() { super.onCreate(); Request.registerListener(DataStorage.listener); } } 


Semata yang sama, tetapi dalam hal RESS, untuk pemahaman


Ini berfungsi seperti ini: Ketika Anda mengklik tombol, metode yang diinginkan untuk Permintaan berkedut, Permintaan mengirim permintaan ke server, memproses respons dan memberi tahu DataStorage terlebih dahulu. DataStorage mem-parsing respons dan menyimpan data di rumah. Permintaan kemudian memberitahukan Layar yang sedang aktif, Layar mengambil data dari DataStorage dan memperbarui UI.

Tanda layar dan berhenti berlangganan dari biasa-biasa saja di onResume dan onPause, masing-masing. Ini juga memperbarui UI selain onResume. Apa yang diberikannya? Pemberitahuan datang hanya di Aktivitas aktif saat ini, tidak ada masalah dengan memproses permintaan di latar belakang atau memutar balik Kegiatan. Aktivitas akan selalu terbarui. Pemberitahuan tidak akan mencapai aktivitas latar belakang, dan ketika kembali ke status aktif, data akan diambil dari DataStorage. Akibatnya, tidak ada masalah ketika Anda memutar layar dan membuat kembali Activity.

Dan untuk semua ini, api default dari Android SDK sudah cukup.

Pertanyaan dan Jawaban untuk Kritik Masa Depan


1. Apa untungnya?


Kesederhanaan yang nyata, fleksibilitas, kemampuan pemeliharaan, skalabilitas, dan ketergantungan minimum. Anda selalu dapat menyulitkan bagian tertentu dari sistem jika perlu. Terlalu banyak data? Pisahkan DataStorage menjadi beberapa. API REST layanan besar? Buat beberapa Permintaan. Apakah mendengarkan terlalu sederhana, canggung dan tidak modis? Ambil EventBus. Melihat curiga di sebuah salon di HttpConnection? Nah, ambil Retrofit. Berani Kegiatan dengan banyak fragmen? Anggap saja setiap fragmen adalah Layar, atau pilah menjadi subkelas.

2. AsyncTask adalah orang jahat, ambil setidaknya Retrofit!


Hah? Dan masalah apa yang ditimbulkannya dalam kode ini? Kebocoran memori? Tidak, di sini AsyncTask tidak menyimpan tautan ke aktivasi, tetapi hanya tautan ke metode statis. Jawabannya hilang? Tidak, jawabannya selalu datang ke DataStorage statis sampai aplikasi dimatikan. Mencoba menyegarkan aktivitas saat jeda? Tidak, notifikasi hanya datang dalam Aktivitas aktif.

Dan bagaimana Retrofit dapat membantu di sini? Lihat saja di sini . Penulis mengambil RxJava, Retrofit dan masih memahat kruk untuk memecahkan masalah yang tidak dimiliki RESS.

3. Layar adalah ViewController yang sama! Perlu memisahkan logika dan presentasi, arrr!


Jatuhkan mantra ini. Klien tipikal untuk layanan REST adalah satu tampilan besar untuk sisi server. Semua logika bisnis Anda adalah mengatur keadaan yang tepat untuk bidang tombol atau teks. Apa yang akan Anda bagikan di sana? Katakanlah akan lebih mudah untuk mempertahankan? Pertahankan 3 file dengan 3 ton kode, bukannya 1 file dengan 1 ton lebih mudah? Ok Dan jika kita memiliki aktivitas dengan 5 fragmen? Ini sudah 3 x (5 +1) = 18 file.

Pemisahan ke Controller dan View dalam kasus seperti itu hanya menghasilkan banyak kode yang tidak berarti, saatnya untuk mengakuinya. Menambahkan fungsionalitas ke proyek dengan MVP sangat menyenangkan: apakah Anda ingin menambahkan penangan tombol? Oke, perbaiki Presenter, Activity, dan View-interface. Dalam RESS, untuk ini saya akan menulis beberapa baris kode dalam satu file.

Tetapi dalam proyek-proyek besar, ViewController tumbuh dengan sangat buruk? Jadi, Anda belum melihat proyek besar. Klien REST Anda untuk situs berikutnya dengan 5 ribu baris adalah proyek kecil, dan 5 ribu baris di sana hanya karena ada 5 kelas di setiap layar. Proyek yang benar-benar besar di RESS dengan 100+ layar dan beberapa tim yang terdiri dari 10 orang merasa hebat. Cukup buat beberapa Permintaan dan Penyimpanan. Dan Layar untuk layar tebal mengandung Layar tambahan untuk elemen UI besar, misalnya, fragmen yang sama. Sebuah proyek pada MVP dengan skala yang sama hanya akan menenggelamkan banyak presenter, antarmuka, aktivasi, fragmen dan koneksi yang tidak jelas. Dan transisi ke VIPER umumnya akan membuat seluruh tim berhenti suatu hari.

Kesimpulan


Saya harap artikel ini akan mendorong banyak pengembang untuk mempertimbangkan kembali pandangan mereka tentang arsitektur, bukan untuk menghasilkan abstraksi dan melihat solusi yang lebih sederhana dan lebih teruji waktu.

Source: https://habr.com/ru/post/id424081/


All Articles