Lapisan Jaringan Modern di iOS Menggunakan Async/Tunggu

Pandangan baru mengenai topik jaringan yang memanfaatkan Model Konkurensi Swift

Saya harus membuat pengakuan: Membuat lapisan jaringan selalu menjadi topik yang menarik bagi saya. Sejak hari pertama pemrograman iOS, pada awal tahun 2007, setiap proyek baru mewakili peluang baru untuk menyempurnakan atau bahkan menghancurkan keseluruhan pendekatan yang telah saya gunakan sejauh ini. Upaya terakhir saya untuk menulis sesuatu tentang topik ini tertanggal 2017, dan saya menganggapnya sebagai tonggak sejarah setelah beralih ke bahasa Swift.

Sudah lama sekali sejak itu; bahasanya berkembang seperti kerangka sistem, dan baru-baru ini, dengan diperkenalkannya Model Konkurensi Swift yang baru, saya memutuskan untuk mengambil langkah lebih jauh ke depan dan memperbarui pendekatan saya terhadap lapisan jaringan. Versi baru ini mengalami desain ulang radikal yang memungkinkan Anda menulis permintaan hanya dalam satu baris kode:

Saat ini, Anda mungkin berpikir: mengapa saya harus menjadikan klien saya daripada mengandalkan Alamofire? Anda benar. Implementasi baru pasti belum matang dan akan menjadi sumber masalah untuk jangka waktu tertentu. Terlepas dari semua itu, Anda memiliki kesempatan untuk menciptakan integrasi yang lebih baik dengan perangkat lunak Anda dan menghindari ketergantungan pihak ketiga. Selain itu, Anda dapat memanfaatkan teknologi Apple baru seperti URLSession, Codable, Async/Await & Actors.
Anda dapat menemukan kodenya di GitHub; proyek ini disebut RealHTTP.

Klien

Mari kita mulai dengan mendefinisikan tipe untuk mewakili klien. Klien (sebelumnya HTTPClient) adalah struktur yang dilengkapi dengan cookie, header, opsi keamanan, aturan validator, batas waktu, dan semua pengaturan bersama lainnya yang mungkin Anda miliki bersama di antara sekelompok permintaan. Saat Anda menjalankan permintaan di klien, semua properti ini secara otomatis berasal dari klien kecuali Anda menyesuaikannya dalam satu permintaan.

Misalnya, saat Anda menjalankan panggilan autentikasi dan menerima token JWT, Anda mungkin ingin menyetel kredensial di tingkat klien, sehingga permintaan lainnya akan menyertakan data ini. Hal yang sama terjadi pada validator: untuk menghindari duplikasi logika validasi data, Anda mungkin ingin membuat validator baru dan membiarkan klien mengeksekusinya untuk setiap permintaan yang diambilnya. Klien juga merupakan kandidat yang tepat untuk menerapkan mekanisme percobaan ulang yang tidak tersedia pada implementasi URLSession dasar.

Permintaan

Seperti yang Anda bayangkan, permintaan (sebelumnya HTTPRequest) merangkum satu panggilan ke titik akhir.

Jika Anda telah membaca beberapa artikel lain tentang topik ini, Anda mungkin sering menemukan bahwa pilihan umum adalah menggunakan Swift's Generic untuk menangani keluaran permintaan.
Sesuatu seperti: struct HTTPRequest<Response>.

Ini memungkinkan Anda untuk menghubungkan dengan kuat jenis objek keluaran ke permintaan itu sendiri. Meskipun ini merupakan penggunaan cerdas dari konstruksi fantastis ini, menurut saya ini membuat permintaan sedikit membatasi. Dari sudut pandang praktis, Anda mungkin perlu menggunakan type erasure untuk menangani objek ini di luar konteksnya. Selain itu, secara konseptual, saya lebih memilih untuk memisahkan tahapan permintaan (mengambil ~› dapatkan data mentah ~› dekode objek) dan mudah diidentifikasi.

Karena alasan ini, saya memilih untuk menghindari obat generik dan mengembalikan respons mentah (HTTPResponse) dari permintaan; Oleh karena itu, objek tersebut akan menyertakan semua fungsi untuk memudahkan dekode (kita akan melihatnya di bawah).

Konfigurasikan Permintaan

Seperti yang kami katakan, permintaan harus memungkinkan kami dengan mudah mengatur semua atribut yang relevan untuk panggilan, terutama “Metode HTTP”, “Jalur”, “Variabel Kueri”, dan “Badan”. Apa yang paling disukai pengembang Swift dibandingkan apa pun? Keamanan tipe.

Saya menyelesaikannya dengan dua cara: menggunakan objek konfigurasi alih-alih literal dan protokol untuk menyediakan konfigurasi yang dapat diperluas bersama dengan serangkaian fungsi pembuat yang telah dibuat sebelumnya.

Ini adalah contoh konfigurasi permintaan:

Contoh umum dari tindakan keamanan tipe adalah Metode HTTP yang menjadi enum; tetapi juga header yang dikelola menggunakan objek HTTPHeader khusus, sehingga Anda dapat menulis sesuatu seperti berikut:

Ini mendukung deklarasi kunci aman tipe dan literal khusus.

Contoh terbaik penggunaan protokol adalah pengaturan isi permintaan. Meskipun pada akhirnya merupakan aliran biner, saya memutuskan untuk membuat struct untuk menampung konten data dan menambahkan serangkaian metode utilitas untuk membuat struktur tubuh yang paling umum (HTTPBody): formulir multi-bagian, objek yang dikodekan JSON, aliran input, URL yang dikodekan tubuh, dll.

Hasilnya adalah:

  • Antarmuka yang dapat diperluas: Anda dapat membuat wadah isi khusus untuk struktur data Anda sendiri dan mengaturnya secara langsung. Buat saja sesuai dengan protokol HTTPSerializableBody untuk memungkinkan serialisasi otomatis ke aliran data bila diperlukan.
  • Kumpulan API yang mudah digunakan: Anda dapat membuat semua container ini langsung dari metode statis yang ditawarkan oleh HTTPBody struct

Berikut ini contoh formulir multipart:

Membuat badan dengan objek yang dikodekan JSON juga hanya berjarak satu baris kode:

Ketika permintaan diteruskan ke klien, URLSessionTask terkait dibuat secara otomatis (di thread lain) dan aliran URLSession standar dieksekusi. Logika yang mendasarinya masih menggunakan URLSessionDelegate (dan delegasi keluarga lainnya); Anda dapat menemukan lebih banyak lagi di kelas HTTPDataLoader.

Jalankan Permintaan

HTTPClient memanfaatkan sepenuhnya async/menunggu, mengembalikan respons mentah dari server. Menjalankan permintaan itu mudah: cukup panggil fungsi fetch()-nya. Dibutuhkan argumen klien opsional; jika tidak disetel, instance HTTPClient tunggal default akan digunakan (artinya cookie, header, dan pengaturan konfigurasi lainnya terkait dengan instance bersama ini).

Oleh karena itu, permintaan ditambahkan ke klien tujuan dan, sesuai dengan konfigurasinya, akan dieksekusi secara asinkron. Serialisasi dan deserialisasi aliran data dibuat di Task lain (demi kesederhanaan, thread lain). Hal ini memungkinkan kami mengurangi jumlah pekerjaan yang dilakukan pada HTTPClient.

Responnya

Respons permintaannya bertipe HTTPResponse; objek ini merangkum semua hal tentang operasi, termasuk data mentah, kode status, kesalahan opsional (diterima dari server atau dihasilkan oleh validator respons), dan data metrik yang valid untuk tujuan debugging integrasi.

Langkah selanjutnya adalah mengubah respons mentah menjadi objek valid (dengan/tanpa DAO). Fungsi decode() memungkinkan Anda meneruskan kelas objek keluaran yang diharapkan. Biasanya, ini adalah objek Codable, namun penting juga untuk mengaktifkan decoding objek khusus, sehingga Anda juga dapat menggunakan objek apa pun yang sesuai dengan protokol HTTPDecodableResponse. Protokol ini hanya mendefinisikan fungsi statis: static func decode(_ response: HTTPResponse) throws -> Self?.

Dengan menerapkan fungsi decode() khusus, Anda dapat melakukan apa pun yang Anda inginkan untuk mendapatkan hasil yang diharapkan. Misalnya, saya penggemar berat SwiftyJSON. Pada awalnya mungkin tampak sedikit lebih bertele-tele dibandingkan 'Codable', namun ia juga menawarkan lebih banyak fleksibilitas dalam kasus-kasus edge, penanganan kegagalan yang lebih baik, dan proses transformasi yang tidak terlalu buram.

Karena sebagian besar waktu, Anda mungkin hanya ingin berakhir dengan objek keluaran yang didekodekan, operasi fetch() juga menyajikan parameter dekode opsional, sehingga Anda dapat melakukan pengambilan & dekode dalam sekali jalan tanpa meneruskan respons mentah.

Fungsi fetch() alternatif ini menggabungkan pengambilan dan dekode dalam satu fungsi; Anda mungkin merasa terbantu ketika Anda tidak perlu mendapatkan detail bagian dalam dari respons tetapi hanya objek yang didekodekan.

Validasi/Ubah Respons

Menggunakan klien khusus dan bukan klien bersama adalah untuk menyesuaikan logika di balik komunikasi dengan titik akhir Anda. Misalnya, kita akan berkomunikasi dengan dua titik akhir berbeda dengan logika berbeda (ya ampun, lingkungan lama…). Artinya, baik hasil maupun kesalahan ditangani secara berbeda.

Misalnya, sistem lama jauh dari sistem seperti REST dan menempatkan kesalahan di dalam isi permintaan; yang baru menggunakan kode status HTTP yang mengkilap.

Untuk menangani kasus ini dan kasus yang lebih kompleks, kami memperkenalkan konsep validator respon, yang sangat mirip dengan Validator Express. Pada dasarnya, validator ditentukan oleh protokol dan fungsi yang menyediakan permintaan dan respons mentahnya, sehingga Anda dapat memutuskan langkah berikutnya.

Anda dapat menolak respons dan membuat kesalahan, menerima respons atau memodifikasinya, segera mencoba ulang atau mencoba lagi setelah menjalankan permintaan alternatif (ini adalah contoh token JWT yang kedaluwarsa yang perlu disegarkan sebelum melakukan upaya lebih lanjut dengan permintaan asli).

Validator dieksekusi secara berurutan sebelum respons dikirim ke level aplikasi. Anda dapat menetapkan beberapa validator ke klien, dan semuanya dapat menyetujui hasil akhir. Ini adalah versi sederhana dari standar HTTPResponseValidator:

https://Gist.github.com/malcommac/decbd7a0c57218dae2c5b9af6b4af246

Anda dapat memperluas/mengonfigurasinya dengan perilaku berbeda. Selain itu, HTTPAltResponseValidator adalah validator yang tepat untuk mengimplementasikan logika coba lagi/setelah panggilan. Validator dapat mengembalikan salah satu tindakan berikut yang ditentukan oleh HTTPResponseValidatorResult:

  • nextValidator: cukup berikan pegangannya ke validator berikutnya
  • failChain: hentikan rantai dan kembalikan kesalahan untuk permintaan itu
  • retry: coba lagi permintaan asal dengan sebuah strategi

Strategi Coba Lagi

Salah satu keunggulan Alamofire adalah infrastruktur untuk mengadaptasi dan mencoba ulang permintaan. Mengimplementasikannya kembali dengan callback bukanlah hal yang mudah, tetapi dengan async/await, semuanya jauh lebih mudah. Kami ingin menerapkan dua jenis strategi percobaan ulang: percobaan ulang sederhana dengan penundaan dan strategi yang lebih kompleks untuk mengeksekusi panggilan alternatif yang diikuti dengan permintaan asal.

Strategi percobaan ulang ditangani di dalam URLSessionDelegate yang dikelola oleh objek internal khusus yang disebut HTTPDataLoader.

Berikut ini adalah versi logika yang terlalu disederhanakan yang Anda dapat temukan di sini (bersama dengan komentar):

Jika Anda berpikir untuk menggunakan percobaan ulang otomatis untuk masalah konektivitas, pertimbangkan untuk menggunakan waitsForConnectivity sebagai gantinya. Jika permintaan gagal karena masalah jaringan, biasanya yang terbaik adalah mengomunikasikan kesalahan tersebut kepada pengguna. Dengan NWPathMonitor Anda masih dapat memantau koneksi ke server Anda dan mencoba lagi secara otomatis.

Men-debug

Proses debug itu penting; cara standar untuk bertukar panggilan jaringan dengan tim backend adalah cURL. Itu tidak memerlukan perkenalan. Ada ekstensi untuk HTTPRequest dan HTTPResponse yang menghasilkan perintah cURL untuk URLRequest yang mendasarinya.

Idealnya, Anda harus menelepon cURLDescription berdasarkan permintaan/tanggapan dan Anda akan mendapatkan semua informasi secara otomatis, termasuk pengaturan HTTPClient orang tua.

Fitur lainnya

Artikel ini akan jauh lebih panjang. Kami tidak membahas topik seperti “SSL Pinning”, “Large File Download/Resume”, “Requests Mocking”, dan HTTP Caching. Semua fitur ini sedang diimplementasikan dan dikerjakan pada proyek GitHub, jadi jika tertarik Anda bisa melihat langsung di sumbernya. Omong-omong, saya telah menggunakan kembali pendekatan yang sama seperti yang Anda lihat di atas.

Merakit API

Saat ini, kami telah menciptakan infrastruktur jaringan ringan yang modern.

Tetapi bagaimana dengan implementasi API kami?

Untuk aplikasi yang lebih kecil, penggunaan HTTPClient secara langsung tanpa membuat definisi API dapat diterima. Namun secara umum merupakan ide bagus untuk menentukan API yang tersedia di suatu tempat untuk mengurangi kekacauan dalam kode Anda dan menghindari kemungkinan kesalahan akibat duplikasi.

Secara pribadi, saya tidak menyukai pendekatan Moya,di mana Anda memodelkan API sebagai enum, dan setiap properti memiliki saklar terpisah. Saya pikir ini umumnya membingungkan karena Anda memiliki semua properti yang mengonfigurasi permintaan tersebar dan tercampur dalam satu file. Pada akhirnya, sulit untuk membaca dan memodifikasi dan ketika Anda menambahkan endpoint baru, Anda harus berpindah ke atas dan ke bawah melalui potongan besar kode ini.

Pendekatan saya adalah memiliki objek yang mampu mengkonfigurasi HTTPRequest valid yang siap diteruskan ke HTTPClient. Untuk contoh ini, kami akan menggunakan MovieDB APIs 🍿 (Anda harus mendaftar untuk mendapatkan akun gratis untuk mendapatkan Kunci API yang valid).

Sekarang mari gunakan lapisan jaringan yang kita bangun sebagai contoh praktis. Demi kesederhanaan, kami akan mempertimbangkan dua API: satu untuk mendapatkan film mendatang/populer/peringkat teratas, satu lagi untuk penelusuran.

Pertama-tama, kami ingin menggunakan namespace melalui enum untuk membuat wadah tempat kami akan meletakkan semua sumber daya untuk konteks tertentu, dalam kasus kami Rankings dan Movies.

Sumber Daya menjelaskan layanan tertentu yang ditawarkan dari layanan jarak jauh; dibutuhkan beberapa parameter masukan dan menggunakannya untuk menghasilkan HTTPRequest valid yang siap dieksekusi. ProtokolAPIResourceConvertible menjelaskan proses ini:

Search adalah Sumber Daya untuk mencari film di dalam MovieDB. Ini dapat diinisialisasi dengan parameter yang diperlukan (querystring) dan dua parameter opsional lainnya, (rilis)year dan includeAdults filter.

Fungsi request() menghasilkan permintaan yang valid sesuai dengan dokumen API MovieDB. Kita dapat mengulangi langkah ini untuk masing-masing membuat Lists Sumber Daya untuk mendapatkan daftar peringkat film upcoming, popular dan topRated. Kami akan memasukkannya ke dalam namespace Rankings:

MoviesPage mewakili objek Codable yang mencerminkan hasil setiap panggilan MovieDB: Dengan pendekatan ini, kami mendapat tiga manfaat:

  • Panggilan API diatur dalam namespace berdasarkan konteksnya
  • Setiap Sumber Daya menjelaskan pendekatan tipe aman untuk membuat permintaan jarak jauh
  • Setiap Sumber Daya berisi semua logika yang menghasilkan Permintaan HTTP yang valid

Satu hal lagi: kita harus mengizinkan HTTPClient untuk menjalankan panggilan APIResourceConvertible dan mengembalikan objek yang aman untuk tipe seperti yang dijelaskan. Ini cukup mudah seperti yang Anda lihat di bawah:

Terakhir, kita akan membuat HTTPClient:

dan kami dapat menjalankan panggilan kami:

Anda dapat menemukan kode sumber lengkap untuk contoh di sini.

Kesimpulan

Sekarang, kami memiliki lapisan jaringan modern yang mudah digunakan berdasarkan async/await yang dapat kami sesuaikan. Kami memiliki kendali penuh atas fungsinya dan pemahaman lengkap tentang mekanismenya.

Pustaka lengkap untuk jaringan dirilis di bawah Lisensi MIT, dan disebut RealHTTP; kami mempertahankan dan mengembangkannya. Jika Anda menyukai artikel ini, mohon pertimbangkan untuk menambahkan bintang ke proyek atau berkontribusi pada pengembangannya.



Want to Connect?
Check out my offnotes newsletter here.