Saya telah merenungkan masalah ini untuk sementara waktu sekarang dan menemukan diri saya terus menemukan peringatan dan kontradiksi, jadi saya berharap seseorang dapat menghasilkan kesimpulan sebagai berikut:
Mendukung pengecualian atas kode kesalahan
Sejauh yang saya ketahui, dari bekerja di industri selama empat tahun, membaca buku dan blog, dll. Praktik terbaik saat ini untuk menangani kesalahan adalah dengan melemparkan pengecualian, daripada mengembalikan kode kesalahan (tidak harus kode kesalahan, tetapi tipe mewakili kesalahan).
Tapi - bagi saya ini tampaknya bertentangan ...
Pengodean ke antarmuka, bukan implementasi
Kami mengkode ke antarmuka atau abstraksi untuk mengurangi kopling. Kami tidak tahu, atau ingin tahu, tipe spesifik dan implementasi antarmuka. Jadi bagaimana kita bisa tahu pengecualian apa yang harus kita tangkap? Implementasinya bisa melempar 10 pengecualian berbeda, atau bisa melempar tidak ada. Ketika kita menangkap pengecualian tentu kita membuat asumsi tentang implementasi?
Kecuali - antarmuka memiliki ...
Spesifikasi pengecualian
Beberapa bahasa memungkinkan pengembang untuk menyatakan bahwa metode tertentu melempar pengecualian tertentu (Java misalnya, menggunakan throws
kata kunci.) Dari sudut pandang kode panggilan ini tampak baik - kita tahu secara eksplisit pengecualian mana yang mungkin perlu kita tangkap.
Tapi - ini sepertinya menyarankan ...
Abstraksi bocor
Mengapa antarmuka harus menentukan pengecualian yang dapat dilemparkan? Bagaimana jika implementasinya tidak perlu melempar pengecualian, atau perlu melempar pengecualian lain? Tidak ada cara, pada tingkat antarmuka, untuk mengetahui pengecualian mana yang mungkin ingin diterapkan oleh suatu implementasi.
Begitu...
Untuk menyimpulkan
Mengapa pengecualian lebih disukai ketika mereka (di mata saya) bertentangan dengan praktik terbaik perangkat lunak? Dan, jika kode kesalahan sangat buruk (dan saya tidak perlu dijual berdasarkan sifat buruk dari kode kesalahan), apakah ada alternatif lain? Bagaimana keadaan saat ini (atau segera menjadi) seni untuk penanganan kesalahan yang memenuhi persyaratan praktik terbaik seperti diuraikan di atas, tetapi tidak bergantung pada kode panggilan memeriksa nilai kembali kode kesalahan?
Jawaban:
Pertama-tama, saya tidak setuju dengan pernyataan ini:
Ini tidak selalu terjadi: misalnya, lihat Objective-C (dengan kerangka kerja Foundation). Di sana NSError adalah cara yang lebih disukai untuk menangani kesalahan, meskipun ada apa yang oleh pengembang Java akan disebut pengecualian sejati: @try, @catch, @throw, kelas NSException, dll.
Namun memang benar bahwa banyak antarmuka bocor abstraksi mereka dengan pengecualian yang dilemparkan. Ini adalah keyakinan saya bahwa ini bukan kesalahan "pengecualian" - gaya penyebaran / penanganan kesalahan. Secara umum saya percaya saran terbaik tentang penanganan kesalahan adalah ini:
Menangani kesalahan / pengecualian pada level serendah mungkin, titik
Saya pikir jika seseorang berpegang pada aturan praktis itu, jumlah "kebocoran" dari abstraksi bisa sangat terbatas dan terkandung.
Tentang apakah pengecualian yang dilemparkan oleh metode harus menjadi bagian dari deklarasi, saya percaya mereka harus: mereka adalah bagian dari kontrak yang didefinisikan oleh antarmuka ini: Metode ini melakukan A, atau gagal dengan B atau C.
Sebagai contoh, jika sebuah kelas adalah Parser XML, bagian dari desainnya harus menunjukkan bahwa file XML yang disediakan benar-benar salah. Di Jawa, Anda biasanya melakukannya dengan mendeklarasikan pengecualian yang ingin Anda temui dan menambahkannya ke
throws
bagian deklarasi metode. Di sisi lain, jika salah satu algoritma penguraian gagal, tidak ada alasan untuk melewatkan pengecualian di atas tanpa penanganan.Semuanya bermuara pada satu hal: Desain antarmuka yang baik. Jika Anda mendesain antarmuka dengan cukup baik, tidak ada pengecualian yang dapat menghantui Anda. Kalau tidak, bukan hanya pengecualian yang akan mengganggu Anda.
Juga, saya pikir pencipta Jawa memiliki alasan keamanan yang sangat kuat untuk memasukkan pengecualian ke metode deklarasi / definisi.
Satu hal terakhir: Beberapa bahasa, Eiffel misalnya, memiliki mekanisme lain untuk penanganan kesalahan dan tidak termasuk kemampuan melempar. Di sana, 'pengecualian' jenis secara otomatis dinaikkan ketika kondisi akhir untuk rutin tidak puas.
sumber
goto
sangat berbeda. Sebagai contoh, pengecualian selalu mengarah ke arah yang sama - ke bawah tumpukan panggilan. Dan kedua, melindungi diri Anda dari pengecualian mendadak adalah praktik yang sama persis dengan KERING - misalnya, dalam C ++, jika Anda menggunakan RAII untuk memastikan pembersihan, maka memastikan pembersihan dalam semua kasus, tidak hanya pengecualian tetapi juga semua aliran kontrol normal. Ini jauh lebih dapat diandalkan.try/finally
mencapai sesuatu yang agak mirip. Saat Anda menjamin pembersihan dengan benar, Anda tidak perlu mempertimbangkan pengecualian sebagai kasus khusus.Saya hanya ingin mencatat bahwa pengecualian dan kode kesalahan bukan satu-satunya cara untuk menangani kesalahan dan jalur kode alternatif.
Keluar dari pikiran, Anda dapat memiliki pendekatan seperti yang diambil oleh Haskell, di mana kesalahan dapat diisyaratkan melalui tipe data abstrak dengan beberapa konstruktor (mis. Enum yang dibedakan, atau null pointer, tetapi typesafe dan dengan kemungkinan menambahkan sintaksis fungsi gula atau penolong untuk membuat aliran kode terlihat bagus).
operationThatMightfail adalah fungsi yang mengembalikan nilai yang dibungkus dengan Maybe. Ia bekerja seperti pointer yang dapat dibatalkan, tetapi notasi menjamin bahwa semuanya bernilai nol jika ada a, b, atau c gagal. (dan kompiler melindungi Anda dari membuat NullPointerException yang tidak disengaja)
Kemungkinan lain adalah melewatkan objek penangan kesalahan sebagai argumen tambahan untuk setiap fungsi yang Anda panggil. Penangan kesalahan ini memiliki metode untuk setiap "pengecualian" yang memungkinkan yang dapat ditandai oleh fungsi tempat Anda meneruskannya, dan dapat digunakan oleh fungsi itu untuk menangani pengecualian di tempat terjadinya, tanpa harus memundurkan tumpukan melalui pengecualian.
LISP umum melakukan ini, dan membuatnya layak dengan memiliki dukungan sintaksis (argumen implisit) dan memiliki fungsi bawaan mengikuti protokol ini.
sumber
Maybe
itu.Ya, pengecualian dapat menyebabkan abstraksi bocor. Tetapi apakah kode kesalahan bahkan tidak lebih buruk dalam hal ini?
Salah satu cara untuk mengatasi masalah ini adalah membuat antarmuka menentukan dengan tepat pengecualian mana yang dapat dilemparkan dalam keadaan apa dan menyatakan bahwa implementasi harus memetakan model pengecualian internal mereka untuk spesifikasi ini, dengan menangkap, mengubah, dan melemparkan kembali pengecualian jika perlu. Jika Anda menginginkan antarmuka "prefek", itulah caranya.
Dalam praktiknya, biasanya cukup untuk menentukan pengecualian yang secara logis merupakan bagian dari antarmuka dan yang mungkin ingin ditangkap dan dilakukan klien. Secara umum dipahami bahwa dapat ada pengecualian lain ketika kesalahan tingkat rendah terjadi atau bug bermanifestasi, dan yang hanya dapat ditangani oleh klien dengan menunjukkan pesan kesalahan dan / atau mematikan aplikasi. Setidaknya pengecualian masih dapat berisi informasi yang membantu mendiagnosis masalah.
Bahkan, dengan kode kesalahan, hal yang hampir sama akhirnya terjadi, hanya dengan cara yang lebih implisit, dan dengan kemungkinan informasi yang hilang lebih banyak dan aplikasi berakhir dalam keadaan yang tidak konsisten.
sumber
getSQLState
(generik) dangetErrorCode
(khusus vendor). Sekarang seandainya memiliki subclass yang tepat ...Banyak hal bagus di sini, saya hanya ingin menambahkan bahwa kita semua harus waspada terhadap kode yang menggunakan pengecualian sebagai bagian dari aliran kontrol normal. Kadang-kadang orang masuk ke dalam perangkap itu di mana segala sesuatu yang bukan kasus biasa menjadi pengecualian. Saya bahkan telah melihat pengecualian yang digunakan sebagai kondisi terminasi loop.
Pengecualian berarti "sesuatu yang tidak bisa saya tangani di sini terjadi, perlu pergi ke orang lain untuk mencari tahu apa yang harus dilakukan." Pengguna mengetik input yang tidak valid bukanlah pengecualian (yang harus ditangani secara lokal oleh input dengan bertanya lagi, dll.).
Kasus degenerasi lain dari penggunaan pengecualian yang pernah saya lihat adalah orang-orang yang respons pertamanya adalah "melempar pengecualian." Ini hampir selalu dilakukan tanpa menulis tangkapan (rule of thumb: tulis tangkapan pertama, kemudian pernyataan melempar). Dalam aplikasi besar ini menjadi bermasalah ketika pengecualian yang tidak tertangkap muncul dari bagian bawah dan meledakkan program.
Saya bukan anti-pengecualian, tetapi mereka tampak seperti lajang beberapa tahun yang lalu: terlalu sering digunakan dan tidak tepat. Mereka sempurna untuk penggunaan yang dimaksudkan, tetapi kasus itu tidak seluas yang dipikirkan beberapa orang.
sumber
Nggak. Spesifikasi pengecualian berada di keranjang yang sama dengan tipe pengembalian dan argumen - mereka adalah bagian dari antarmuka. Jika Anda tidak dapat memenuhi spesifikasi itu, maka jangan mengimplementasikan antarmuka. Jika Anda tidak pernah melempar, maka itu tidak masalah. Tidak ada yang bocor dalam menentukan pengecualian dalam antarmuka.
Kode kesalahan sangat buruk. Mereka mengerikan. Anda harus ingat secara manual untuk memeriksa dan menyebarkannya, setiap waktu, untuk setiap panggilan. Ini melanggar KERING, untuk memulai, dan secara besar-besaran meledakkan kode penanganan kesalahan Anda. Pengulangan ini adalah masalah yang jauh lebih besar daripada yang dihadapi oleh pengecualian. Anda tidak akan pernah dapat mengabaikan pengecualian, tetapi orang dapat dan secara diam-diam mengabaikan kode kembali - jelas merupakan hal yang buruk.
sumber
Well Exception handling dapat memiliki implementasi antarmuka sendiri. Tergantung pada jenis pengecualian yang dilemparkan, lakukan langkah-langkah yang diinginkan.
Solusi untuk masalah desain Anda adalah memiliki dua implementasi antarmuka / abstraksi. Satu untuk fungsi dan lainnya untuk penanganan pengecualian. Dan tergantung pada jenis Pengecualian yang ditangkap, panggil kelas jenis pengecualian yang sesuai.
Implementasi kode Kesalahan adalah cara ortodoks dalam menangani pengecualian. Ini seperti penggunaan string vs string builder.
sumber
Pengecualian IM-HO sangat harus dinilai berdasarkan kasus per kasus, karena dengan memutus aliran kontrol mereka akan meningkatkan kompleksitas aktual dan yang dirasakan dari kode Anda, dalam banyak kasus tidak perlu begitu. Mengesampingkan diskusi terkait dengan melempar pengecualian di dalam fungsi Anda - yang sebenarnya dapat meningkatkan aliran kontrol Anda, jika seseorang ingin melihat melempar pengecualian melalui batas panggilan, pertimbangkan hal berikut:
Mengizinkan callee untuk memutus aliran kontrol Anda mungkin tidak memberikan manfaat nyata, dan mungkin tidak ada cara yang berarti untuk menangani pengecualian. Sebagai contoh langsung, jika seseorang menerapkan pola yang Dapat Diobservasi (dalam bahasa seperti C # di mana Anda memiliki acara di mana-mana dan tidak ada yang eksplisit
throws
dalam definisi), tidak ada alasan aktual untuk membiarkan Pengamat memutus aliran kontrol Anda jika crash, dan tidak ada cara yang berarti untuk menangani barang-barang mereka (tentu saja, tetangga yang baik tidak boleh membuang ketika mengamati, tetapi tidak ada yang sempurna).Pengamatan di atas dapat diperluas ke antarmuka yang digabungkan secara longgar (seperti yang Anda tunjukkan); Saya pikir itu sebenarnya norma bahwa setelah merangkak 3-6 frame stack, pengecualian yang tidak tertangkap mungkin berakhir di bagian kode yang:
Mempertimbangkan hal di atas, mendekorasi antarmuka dengan
throws
semantik hanya merupakan keuntungan fungsional marjinal, karena banyak penelepon melalui kontrak antarmuka hanya akan peduli jika Anda gagal, bukan mengapa.Saya akan mengatakan itu kemudian menjadi masalah selera dan kenyamanan: fokus utama Anda dengan anggun memulihkan keadaan Anda baik di penelepon dan callee setelah "pengecualian", oleh karena itu, jika Anda memiliki banyak pengalaman dalam memindahkan kode kesalahan sekitar (datang dari latar belakang C), atau jika Anda bekerja di lingkungan di mana pengecualian dapat mengubah kejahatan (C ++), saya tidak percaya bahwa melemparkan barang-barang sangat penting untuk OOP yang bagus dan bersih sehingga Anda tidak dapat mengandalkan yang lama pola jika Anda tidak nyaman dengan itu. Terutama jika itu mengarah pada pemecahan SoC.
Dari perspektif teoretis, saya pikir cara SoC-halal dalam menangani pengecualian dapat diturunkan langsung dari pengamatan bahwa sebagian besar penelepon langsung hanya peduli bahwa Anda gagal, bukan mengapa. Melemparkan callee, seseorang yang sangat dekat di atas (2-3 frame) menangkap versi upcasted, dan pengecualian sebenarnya selalu tenggelam ke penangan kesalahan khusus (bahkan jika hanya melacak) - ini adalah tempat AOP akan berguna, karena penangan ini berguna cenderung horisontal.
sumber
Keduanya harus hidup berdampingan.
Kembalikan kode kesalahan saat Anda mengantisipasi perilaku tertentu.
Kembalikan pengecualian saat Anda tidak mengantisipasi beberapa perilaku.
Kode kesalahan biasanya dikaitkan dengan satu pesan, ketika jenis pengecualian tetap ada, tetapi pesan dapat bervariasi
Pengecualian memiliki jejak tumpukan, ketika kode kesalahan tidak. Saya tidak menggunakan kode kesalahan untuk men-debug sistem yang rusak.
Ini mungkin khusus untuk JAWA, tetapi ketika saya mendeklarasikan antarmuka saya, saya tidak menentukan pengecualian apa yang mungkin dilemparkan oleh implementasi antarmuka itu, itu hanya tidak masuk akal.
Ini sepenuhnya terserah Anda. Anda dapat mencoba dan menangkap jenis pengecualian yang sangat spesifik dan kemudian menangkap yang lebih umum
Exception
. Mengapa tidak membiarkan pengecualian menyebarkan tumpukan dan kemudian menanganinya? Atau Anda dapat melihat pemrograman aspek di mana penanganan pengecualian menjadi aspek "pluggable".Saya tidak mengerti mengapa ini menjadi masalah bagi Anda. Ya, Anda mungkin memiliki satu implementasi yang tidak pernah gagal atau melempar pengecualian dan Anda mungkin memiliki implementasi yang berbeda yang selalu gagal dan melempar pengecualian. Jika itu masalahnya, maka jangan tentukan pengecualian pada antarmuka dan masalah Anda telah terpecahkan.
Apakah akan mengubah apa pun jika bukan pengecualian, implementasi Anda mengembalikan objek hasil? Objek ini akan berisi hasil dari tindakan Anda bersama dengan kesalahan / kegagalan jika ada. Anda kemudian dapat menginterogasi objek itu.
sumber
Dalam pengalaman saya, kode yang menerima kesalahan (baik itu melalui pengecualian, kode kesalahan atau apa pun) biasanya tidak peduli untuk penyebab pasti kesalahan - itu akan bereaksi dengan cara yang sama untuk setiap kegagalan, kecuali untuk kemungkinan pelaporan error (baik itu dialog kesalahan atau semacam log); dan pelaporan ini akan dilakukan secara ortogonal terhadap kode yang disebut prosedur gagal. Misalnya, kode ini dapat meneruskan kesalahan ke beberapa kode lain yang tahu cara melaporkan kesalahan tertentu (misalnya memformat string pesan), mungkin melampirkan beberapa informasi konteks.
Tentu saja, dalam beberapa kasus yang diperlukan untuk melampirkan semantik khusus untuk kesalahan dan bereaksi secara berbeda berdasarkan mana kesalahan terjadi. Kasing seperti itu harus didokumentasikan dalam spesifikasi antarmuka. Namun, antarmuka mungkin masih berhak untuk melempar pengecualian lain tanpa arti khusus.
sumber
Saya menemukan bahwa pengecualian memungkinkan untuk menulis kode yang lebih terstruktur dan ringkas untuk pelaporan dan penanganan kesalahan: menggunakan kode kesalahan memerlukan memeriksa nilai kembali setelah setiap panggilan dan memutuskan apa yang harus dilakukan jika terjadi hasil yang tidak terduga.
Di sisi lain saya setuju bahwa pengecualian mengungkapkan detail implementasi yang harus disembunyikan ke kode yang meminta antarmuka. Karena tidak mungkin untuk mengetahui apriori mana potongan kode dapat melempar pengecualian mana (kecuali mereka dinyatakan dalam metode tanda tangan seperti di Jawa), dengan menggunakan pengecualian kami memperkenalkan dependensi implisit yang sangat kompleks antara berbagai bagian kode, yang merupakan bertentangan dengan prinsip meminimalkan ketergantungan.
Meringkas:
sumber
catch Exception
atau bahkanThrowable
, atau setara).Bias Benar.
Itu tidak bisa diabaikan, harus ditangani, itu benar-benar transparan. Dan JIKA Anda menggunakan jenis kesalahan tangan kiri yang benar itu menyampaikan semua informasi yang sama sebagai pengecualian java.
Kelemahan? Kode dengan penanganan kesalahan yang tepat terlihat menjijikkan (benar dari semua mekanisme).
sumber