Penanganan kesalahan fungsional di Kotlin menggunakan Panah

gambar

Halo, Habr!

Semua orang menyukai pengecualian runtime. Tidak ada cara yang lebih baik untuk mengetahui bahwa ada sesuatu yang tidak diperhitungkan saat menulis kode. Terutama - jika pengecualian menjatuhkan aplikasi di antara jutaan pengguna, dan berita ini datang dalam email panik dari portal analytics. Sabtu pagi. Ketika Anda sedang dalam perjalanan negara.

Setelah ini, Anda serius memikirkan penanganan kesalahan - dan apa saja kemungkinan yang disediakan Kotlin untuk kami?

Yang pertama kali muncul di benak saya adalah try-catch. Bagi saya - pilihan yang bagus, tetapi memiliki dua masalah:

  1. Ini adalah kode tambahan (pembungkus paksa di sekitar kode tidak mempengaruhi keterbacaan dengan cara terbaik).
  2. Itu tidak selalu (terutama ketika menggunakan perpustakaan pihak ketiga) dari blok catch adalah mungkin untuk mendapatkan pesan informatif tentang apa yang sebenarnya menyebabkan kesalahan.

Mari kita lihat bagaimana try-catch mengubah kode ketika mencoba menyelesaikan masalah di atas.

Misalnya, fungsi eksekusi permintaan jaringan yang paling sederhana

fun makeRequest(request: RequestBody): List<ResponseData>? { val response = httpClient.newCall(request).execute() return if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { null } } 

menjadi seperti

 fun makeRequest(request: RequestBody): List<ResponseData>? { try { val response = httpClient.newCall(request).execute() return if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { null } } catch (e: Exception) { log.error("SON YOU DISSAPOINT: ", e.message) return null } } 

"Ini tidak terlalu buruk," seseorang mungkin berkata, "Anda dan kotlin Anda menginginkan semua kode gula," tambahnya (ini kutipan) - dan ia akan ... dua kali benar. Tidak, tidak akan ada holivars hari ini - semua orang memutuskan untuk dirinya sendiri. Saya pribadi memutuskan kode parser json yang ditulis sendiri, di mana parsing masing-masing bidang dibungkus dengan try-catch, sementara masing-masing blok catch kosong. Jika seseorang puas dengan keadaan ini - sebuah bendera di tangannya. Saya ingin menawarkan cara yang lebih baik.

Sebagian besar bahasa pemrograman fungsional yang diketik menawarkan dua kelas untuk menangani kesalahan dan pengecualian: Coba dan Baik . Cobalah untuk menangani pengecualian, dan Entah untuk menangani kesalahan logika bisnis.

Perpustakaan Arrow memungkinkan Anda untuk menggunakan abstraksi ini dengan Kotlin. Dengan demikian, Anda dapat menulis ulang permintaan di atas sebagai berikut:

 fun makeRequest(request: RequestBody): Try<List<ResponseData>> = Try { val response = httpClient.newCall(request).execute() if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { emptyList() } } 

Bagaimana pendekatan ini berbeda dari menggunakan try-catch?

Pertama, siapa pun yang membaca kode ini setelah Anda (dan kemungkinan besar akan) akan sudah dapat memahami dengan tanda tangan bahwa mengeksekusi kode dapat menyebabkan kesalahan - dan menulis kode untuk memprosesnya. Selain itu, kompiler akan bersumpah jika ini tidak dilakukan.

Kedua, ada fleksibilitas dalam bagaimana kesalahan dapat ditangani.

Di dalam Mencoba, kesalahan atau keberhasilan eksekusi masing-masing diwakili sebagai kelas Kegagalan dan Keberhasilan. Jika kami ingin fungsi selalu mengembalikan sesuatu karena kesalahan, kami dapat menetapkan nilai default:

 makeRequest(request).getOrElse { emptyList() } 

Jika penanganan kesalahan yang lebih kompleks diperlukan, lipatan datang untuk menyelamatkan:

 makeRequest(request).fold( {ex -> //  -       emptyList() }, { data -> /*    */ } ) 

Anda dapat menggunakan fungsi pemulihan - isinya akan sepenuhnya diabaikan jika Coba kembali Sukses.

 makeRequest(request).recover { emptyList() } 

Anda dapat menggunakan untuk pemahaman (dipinjam oleh pembuat Arrow dari Scala) jika Anda perlu memproses hasil Sukses menggunakan urutan perintah dengan memanggil pabrik .monad () di Coba:

 Try.monad().binding { val r = httpclient.makeRequest(request) val data = r.recoverWith { Try.pure(emptyList()) }.bind() val result: MutableList<Data> = data.toMutableList() result.add(Data()) yields(result) } 

Opsi di atas dapat ditulis tanpa menggunakan binding, tetapi kemudian akan dibaca dengan cara yang berbeda:

 httpcilent.makeRequest(request) .recoverWith { Try.pure(emptyList()) } .flatMap { data -> val result: MutableList<Data> = data.toMutableList() result.add(Data()) Try.pure(result) } 

Pada akhirnya, hasil dari fungsi dapat diproses menggunakan ketika:

 when(response) { is Try.Success -> response.data.toString() is Try.Failure -> response.exception.message } 

Dengan demikian, dengan menggunakan Arrow, Anda dapat mengganti konstruksi try-catch yang ideal dengan sesuatu yang fleksibel dan sangat nyaman. Keuntungan tambahan menggunakan Arrow adalah bahwa terlepas dari kenyataan bahwa perpustakaan memposisikan dirinya sebagai fungsional, Anda dapat menggunakan abstraksi individual dari sana (misalnya, Coba yang sama) sambil terus menulis kode OOP lama yang baik. Tetapi saya memperingatkan Anda - Anda mungkin menyukainya dan terlibat, dalam beberapa minggu Anda akan mulai mempelajari Haskell, dan kolega Anda akan segera berhenti memahami alasan Anda tentang struktur kode.

PS: Layak :)

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


All Articles