Penanganan kesalahan Kotlin / Java: bagaimana melakukannya dengan benar?


Sumber


Penanganan kesalahan dalam pengembangan apa pun memainkan peran penting. Hampir semuanya bisa salah dalam program: pengguna akan memasukkan data yang salah, atau mereka dapat datang melalui http, atau kami membuat kesalahan saat menulis serialisasi / deserialisasi dan selama pemrosesan program macet dengan kesalahan. Ya, ini mungkin kehabisan ruang disk.


spoiler

Β―_ (ツ) _ / Β―, tidak ada satu cara, dan dalam setiap situasi tertentu Anda harus memilih opsi yang paling sesuai, tetapi ada rekomendasi tentang bagaimana melakukannya dengan lebih baik.


Kata Pengantar


Sayangnya (atau hanya kehidupan seperti itu?), Daftar ini terus berlanjut. Pengembang terus-menerus perlu memikirkan fakta bahwa di suatu tempat suatu kesalahan dapat terjadi, dan ada 2 situasi:


  • ketika kesalahan yang diharapkan terjadi dalam memanggil fungsi yang telah kami sediakan dan dapat mencoba memproses;
  • ketika kesalahan tak terduga terjadi selama operasi yang tidak kami perkirakan.

Dan jika kesalahan yang diharapkan setidaknya dilokalkan, maka sisanya dapat terjadi hampir di mana-mana. Jika kami tidak memproses sesuatu yang penting, maka kami bisa saja crash dengan kesalahan (meskipun perilaku ini tidak cukup dan Anda perlu setidaknya menambahkan pesan ke log kesalahan). Tetapi jika saat ini pembayaran sedang diproses dan Anda tidak bisa jatuh, tetapi setidaknya Anda perlu mengembalikan respons tentang operasi yang gagal?


Sebelum kita melihat cara untuk menangani kesalahan, beberapa kata tentang Pengecualian (pengecualian):


Pengecualian



Sumber


Hierarki pengecualian dijelaskan dengan baik dan Anda dapat menemukan banyak informasi tentangnya, sehingga tidak masuk akal untuk melukisnya di sini. Apa yang kadang-kadang masih menyebabkan diskusi panas checked dan unchecked kesalahan. Dan meskipun mayoritas menerima pengecualian yang unchecked sebagai pilihan (di Kotlin tidak ada pengecualian yang checked sama sekali), tidak semua orang setuju dengan ini.


Pengecualian yang checked benar-benar memiliki niat baik untuk menjadikannya mekanisme penanganan kesalahan yang mudah, tetapi kenyataannya membuat penyesuaiannya, meskipun gagasan untuk memasukkan semua pengecualian yang dapat dilemparkan dari fungsi ini ke dalam tanda tangan dapat dipahami dan logis.


Mari kita lihat sebuah contoh. Misalkan kita memiliki fungsi method yang dapat membuang PanicException diperiksa. Fungsi seperti itu akan terlihat seperti ini:


 public void method() throws PanicException { } 

Dari deskripsinya, jelas bahwa dia bisa melempar pengecualian dan hanya ada satu pengecualian. Apakah terlihat cukup nyaman? Dan sementara kami memiliki program kecil, itu saja. Tetapi jika program sedikit lebih besar dan ada lebih banyak fungsi seperti itu, maka beberapa masalah muncul.


Pengecualian yang diperiksa membutuhkan, dengan spesifikasi, bahwa semua pengecualian yang mungkin diperiksa (atau nenek moyang yang sama untuk mereka) terdaftar dalam tanda tangan fungsi. Oleh karena itu, jika kita memiliki rantai panggilan a -> b -> c dan fungsi yang paling bersarang melempar semacam pengecualian, maka itu harus diletakkan untuk semua orang dalam rantai. Dan jika ada beberapa pengecualian, maka fungsi paling atas dalam tanda tangan harus memiliki deskripsi semuanya.


Jadi, ketika program menjadi lebih kompleks, pendekatan ini mengarah pada fakta bahwa pengecualian pada fungsi teratas secara bertahap runtuh ke leluhur yang sama dan akhirnya turun ke Exception . Apa yang ada dalam formulir ini menjadi mirip dengan pengecualian yang unchecked dan meniadakan semua keuntungan dari pengecualian yang diperiksa.


Dan mengingat bahwa program, sebagai organisme hidup, terus berubah dan berevolusi, hampir tidak mungkin untuk meramalkan sebelumnya apa pengecualian yang mungkin muncul di dalamnya. Dan akibatnya, situasinya adalah ketika kita menambahkan fungsi baru dengan pengecualian baru, kita harus melalui seluruh rantai penggunaannya dan mengubah tanda tangan semua fungsi. Setuju, ini bukan tugas yang paling menyenangkan (bahkan mengingat bahwa IDE modern melakukan ini untuk kita).


Tapi yang terakhir, dan mungkin paku terbesar dalam pengecualian diperiksa "mengusir" lambdas dari Jawa 8. Tidak ada pengecualian diperiksa Β―_ (ツ) _ / Β― dalam tanda tangan mereka (karena fungsi apa pun dapat disebut dalam lambda, dengan signature), jadi setiap panggilan fungsi dengan pengecualian yang dicentang dari lambda memaksanya untuk dibungkus dengan penerusan pengecualian sebagai tidak dicentang:


 Stream.of(1,2,3).forEach(item -> { try { functionWithCheckedException(); } catch (Exception e) { throw new RuntimeException("rethrow", e); } }); 

Untungnya, dalam spesifikasi JVM tidak ada pengecualian yang diperiksa sama sekali, jadi di Kotlin Anda tidak dapat membungkus apa pun dalam lambda yang sama, tetapi cukup memanggil fungsi yang diinginkan.


meski terkadang ...

Meskipun ini kadang-kadang menyebabkan konsekuensi yang tidak terduga, seperti, misalnya, operasi @Transactional dalam Spring Framework , yang "mengharapkan" hanya pengecualian yang tidak unckecked . Tapi ini lebih merupakan fitur kerangka kerja, dan mungkin perilaku ini di Spring akan berubah dalam masalah github dalam waktu dekat.


Pengecualian itu sendiri adalah objek khusus. Selain fakta bahwa mereka dapat "dilempar" melalui metode, mereka juga mengumpulkan stacktrace saat pembuatan. Fitur ini kemudian membantu dengan analisis masalah dan mencari kesalahan, tetapi juga dapat menyebabkan beberapa masalah kinerja jika logika aplikasi menjadi sangat terikat dengan pengecualian yang dilemparkan. Seperti yang ditunjukkan dalam artikel , menonaktifkan rakitan stacktrace dapat secara signifikan meningkatkan kinerja mereka dalam kasus ini, tetapi Anda harus menggunakan itu hanya dalam kasus luar biasa ketika benar-benar diperlukan!


Menangani kesalahan


Hal utama yang harus dilakukan dengan kesalahan "tak terduga" adalah menemukan tempat di mana Anda dapat mencegatnya. Dalam bahasa JVM, ini bisa berupa titik pembuatan aliran atau titik filter / entri ke metode http, di mana Anda dapat menempatkan try-catch dengan menangani kesalahan yang unchecked . Jika Anda menggunakan kerangka kerja apa pun, maka kemungkinan besar kerangka kerja itu sudah memiliki kemampuan untuk membuat penangan kesalahan umum, seperti, misalnya, dalam Kerangka Kerja Spring, Anda dapat menggunakan metode dengan penjelasan @ExceptionHandler .


Anda dapat "meningkatkan" pengecualian ke titik pemrosesan pusat yang tidak ingin kami tangani di tempat tertentu dengan melemparkan pengecualian yang tidak unckecked sama (ketika, misalnya, kami tidak tahu apa yang harus dilakukan di tempat tertentu dan bagaimana menangani kesalahan). Tetapi metode ini tidak selalu cocok, karena kadang-kadang mungkin diperlukan untuk menangani kesalahan di tempat, dan Anda perlu memeriksa bahwa semua tempat panggilan fungsi diproses dengan benar. Pertimbangkan cara untuk melakukan ini.


  1. Masih menggunakan pengecualian dan try-catch yang sama:


      int a = 10; int b = 20; int sum; try { sum = calculateSum(a,b); } catch (Exception e) { sum = -1; } 

    Kelemahan utama adalah bahwa kita dapat "lupa" untuk membungkusnya dalam try-catch di tempat panggilan dan melewatkan upaya untuk memprosesnya di tempat, karena pengecualian akan melempar ke titik umum pemrosesan kesalahan. Di sini kita bisa pergi ke pengecualian checked (untuk Jawa), tetapi kemudian kita akan mendapatkan semua kerugian yang disebutkan di atas. Pendekatan ini mudah digunakan jika penanganan kesalahan tidak selalu diperlukan, tetapi dalam kasus yang jarang diperlukan.


  2. Gunakan kelas yang dimeteraikan sebagai hasil dari panggilan (Kotlin).
    Di Kotlin, Anda dapat membatasi jumlah ahli waris kelas, menjadikannya dapat dihitung pada tahap kompilasi - ini memungkinkan kompiler untuk memverifikasi bahwa semua opsi yang mungkin diuraikan dalam kode. Di Jawa, Anda bisa membuat antarmuka umum dan beberapa keturunan, namun, kehilangan pemeriksaan tingkat kompilasi.


     sealed class Result data class SuccessResult(val value: Int): Result() data class ExceptionResult(val exception: Exception): Result() val a = 10 val b = 20 val sum = when (val result = calculateSum(a,b)) { is SuccessResult -> result.value is ExceptionResult -> { result.exception.printStackTrace() -1 } } 

    Di sini kita mendapatkan sesuatu seperti pendekatan kesalahan golang ketika Anda perlu secara eksplisit memeriksa nilai yang dihasilkan (atau mengabaikan secara eksplisit). Pendekatannya cukup praktis dan terutama nyaman ketika Anda perlu membuang banyak parameter di setiap situasi. Kelas Result dapat diperluas dengan berbagai metode yang membuatnya lebih mudah untuk mendapatkan hasilnya dengan pengecualian melempar di atas, jika ada (mis. Kita tidak perlu menangani kesalahan di tempat panggilan). Kelemahan utama hanya akan membuat benda-benda tak berguna menengah (dan entri yang sedikit lebih verbose), tetapi juga dapat dihapus menggunakan kelas inline (jika satu argumen cukup untuk kita). dan, sebagai contoh khusus, ada kelas Result dari Kotlin. Benar, ini hanya untuk penggunaan internal, seperti di masa depan, implementasinya mungkin sedikit berubah, tetapi jika Anda ingin menggunakannya, Anda dapat menambahkan flag kompilasi -Xallow-result-return-type .


  3. Sebagai salah satu dari kemungkinan tipe klaim 2, penggunaan tipe dari pemrograman fungsional Either , yang dapat berupa hasil atau kesalahan. Tipe itu sendiri dapat berupa kelas sealed atau kelas inline . Di bawah ini adalah contoh penggunaan implementasi dari perpustakaan arrow :


     val a = 10 val b = 20 val value = when(val result = calculateSum(a,b)) { is Either.Left -> { result.a.printStackTrace() -1 } is Either.Right -> result.b } 

    Either cocok untuk mereka yang menyukai pendekatan fungsional dan yang suka membangun rantai panggilan.


  4. Gunakan Option atau jenis yang dapat dibatalkan dari Kotlin:


     fun testFun() { val a = 10 val b = 20 val sum = calculateSum(a,b) ?: throw RuntimeException("some exception") } fun calculateSum(a: Int, b: Int): Int? 

    Pendekatan ini cocok jika penyebab kesalahan tidak terlalu penting dan ketika itu hanya satu. Jawaban kosong dianggap sebagai kesalahan dan dilempar lebih tinggi. Catatan terpendek, tanpa membuat objek tambahan, tetapi pendekatan ini tidak selalu dapat diterapkan.


  5. Mirip dengan item 4, hanya menggunakan nilai hardcode sebagai penanda kesalahan:


     fun testFun() { val a = 10 val b = 20 val sum = calculateSum(a,b) if (sum == -1) { throw RuntimeException(β€œerror”) } } fun calculateSum(a: Int, b: Int): Int 

    Ini mungkin merupakan pendekatan penanganan kesalahan tertua yang kembali dari C (atau bahkan dari Algol). Tidak ada overhead, hanya kode yang tidak sepenuhnya jelas (bersama dengan pembatasan pada pilihan hasil), tetapi, tidak seperti paragraf 4, dimungkinkan untuk membuat berbagai kode kesalahan jika diperlukan lebih dari satu pengecualian.



Kesimpulan


Semua pendekatan dapat digabungkan tergantung pada situasinya, dan tidak ada satu pun di antara mereka yang cocok dalam semua kasus.


Jadi, misalnya, Anda dapat mencapai pendekatan golang untuk kesalahan menggunakan kelas sealed , dan jika tidak nyaman, beralihlah ke kesalahan yang unchecked .


Atau, di sebagian besar tempat, nullable jenis nullable sebagai penanda bahwa tidak mungkin untuk menghitung nilai atau mendapatkannya dari suatu tempat (misalnya, sebagai indikator bahwa nilai tersebut tidak ditemukan dalam database).


Dan jika Anda memiliki kode yang berfungsi penuh bersama dengan arrow atau pustaka sejenis lainnya, maka kemungkinan besar yang terbaik adalah menggunakan.


Adapun http-server, paling mudah untuk meningkatkan semua kesalahan ke titik pusat dan hanya di beberapa tempat menggabungkan pendekatan nullable dengan kelas sealed .


Saya akan senang melihat di komentar bahwa Anda menggunakan ini, atau mungkin ada metode penanganan kesalahan yang mudah?


Dan terima kasih kepada semua orang yang membaca sampai akhir!

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


All Articles