Ada banyak praktik terbaik yang terkenal tentang penanganan pengecualian secara terpisah. Saya tahu "harus dan tidak boleh dilakukan" dengan cukup baik, tetapi segala sesuatunya menjadi rumit ketika menyangkut praktik atau pola terbaik di lingkungan yang lebih besar. "Lempar lebih awal, terlambat" - Aku sudah berkali-kali mendengar dan itu masih membingungkanku.
Mengapa saya harus melempar lebih awal dan terlambat, jika pada level rendah pengecualian null pointer dilemparkan? Mengapa saya harus menangkapnya di lapisan yang lebih tinggi? Tidak masuk akal bagi saya untuk menangkap pengecualian tingkat rendah di tingkat yang lebih tinggi, seperti lapisan bisnis. Tampaknya melanggar kekhawatiran setiap lapisan.
Bayangkan situasi berikut:
Saya memiliki layanan yang menghitung angka. Untuk menghitung angka, layanan mengakses repositori untuk mendapatkan data mentah dan beberapa layanan lain untuk menyiapkan perhitungan. Jika terjadi kesalahan pada lapisan pengambilan data, mengapa saya harus melempar DataRetrievalException ke tingkat yang lebih tinggi? Sebaliknya saya lebih suka untuk membungkus pengecualian menjadi pengecualian yang bermakna, misalnya sebuah CalculationServiceException.
Mengapa melempar lebih awal, mengapa terlambat menangkap?
sumber
Jawaban:
Dalam pengalaman saya, yang terbaik untuk melempar pengecualian pada titik di mana kesalahan terjadi. Anda melakukan ini karena itu adalah titik di mana Anda paling tahu tentang mengapa pengecualian itu dipicu.
Karena pengecualian membuka kembali lapisan, menangkap dan memikirkan kembali adalah cara yang baik untuk menambahkan konteks tambahan ke pengecualian. Ini bisa berarti melempar jenis pengecualian yang berbeda, tetapi sertakan pengecualian asli saat Anda melakukan ini.
Akhirnya pengecualian akan mencapai lapisan di mana Anda dapat membuat keputusan tentang aliran kode (misalnya, permintaan pengguna untuk bertindak). Ini adalah titik di mana Anda akhirnya harus menangani pengecualian dan melanjutkan eksekusi normal.
Dengan latihan dan pengalaman dengan basis kode Anda, menjadi sangat mudah untuk menilai kapan harus menambahkan konteks tambahan ke kesalahan, dan di mana yang paling masuk akal sebenarnya, akhirnya menangani kesalahan.
Tangkap → Rethrow
Lakukan ini di mana Anda dapat dengan berguna menambahkan lebih banyak informasi yang akan menyelamatkan pengembang harus bekerja melalui semua lapisan untuk memahami masalah.
Tangkap → Tangani
Lakukan ini di mana Anda dapat membuat keputusan akhir tentang apa yang sesuai, tetapi alur eksekusi yang berbeda melalui perangkat lunak.
Tangkap → Kesalahan Kembali
Meskipun ada situasi di mana hal ini sesuai, menangkap pengecualian dan mengembalikan nilai kesalahan kepada pemanggil harus dipertimbangkan untuk refactoring ke dalam implementasi Catch → Rethrow.
sumber
NullPointerException
? Mengapa tidak memeriksanull
dan melempar pengecualian (mungkin sebuahIllegalArgumentException
) lebih cepat sehingga penelepon tahu persis di mana yang buruknull
dilewatkan?" Saya percaya itu akan menjadi apa yang disarankan oleh bagian "lempar lebih awal" dari perkataan itu.Anda ingin melempar pengecualian sesegera mungkin karena itu membuatnya lebih mudah untuk menemukan penyebabnya. Misalnya, pertimbangkan metode yang bisa gagal dengan argumen tertentu. Jika Anda memvalidasi argumen dan gagal di awal metode, Anda segera tahu kesalahan dalam kode panggilan. Jika Anda menunggu hingga argumen diperlukan sebelum gagal, Anda harus mengikuti eksekusi dan mencari tahu apakah bug ada dalam kode panggilan (argumen buruk) atau metode memiliki bug. Semakin awal Anda melempar pengecualian, semakin dekat dengan penyebab dasarnya dan semakin mudah untuk mencari tahu di mana ada yang salah.
Alasan pengecualian ditangani pada tingkat yang lebih tinggi adalah karena tingkat yang lebih rendah tidak tahu apa tindakan yang sesuai untuk menangani kesalahan. Bahkan, mungkin ada beberapa cara yang tepat untuk menangani kesalahan yang sama tergantung pada apa kode panggilan itu. Ambil contoh membuka file. Jika Anda mencoba membuka file konfigurasi dan tidak ada di sana, mengabaikan pengecualian dan melanjutkan dengan konfigurasi default dapat menjadi respons yang sesuai. Jika Anda membuka file pribadi yang penting untuk eksekusi program dan entah bagaimana hilang, satu-satunya pilihan Anda mungkin untuk menutup program.
Membungkus pengecualian dalam jenis yang tepat adalah masalah yang murni ortogonal.
sumber
Yang lain telah merangkum dengan baik mengapa harus melempar lebih awal . Biarkan saya berkonsentrasi pada alasan mengapa harus terlambat , yang mana saya belum melihat penjelasan yang memuaskan untuk selera saya.
JADI MENGAPA PENGECUALIAN?
Tampaknya ada kebingungan di sekitar mengapa pengecualian ada di tempat pertama. Biarkan saya berbagi rahasia besar di sini: alasan untuk pengecualian, dan penanganan pengecualian adalah ... ABSTRAKSI .
Pernahkah Anda melihat kode seperti ini:
Bukan itu cara pengecualian harus digunakan. Kode seperti di atas memang ada dalam kehidupan nyata, tetapi mereka lebih merupakan penyimpangan, dan benar-benar pengecualian (pun). Definisi pembagian misalnya, bahkan dalam matematika murni, adalah bersyarat: selalu "kode penelepon" yang harus menangani kasus nol yang luar biasa untuk membatasi domain input. Itu jelek. Itu selalu menyakitkan bagi penelepon. Namun, untuk situasi seperti itu pola check-then-do adalah cara alami untuk pergi:
Atau, Anda dapat menggunakan komando penuh pada gaya OOP seperti ini:
Seperti yang Anda lihat, kode penelepon memiliki beban pemeriksaan awal, tetapi tidak melakukan penanganan pengecualian setelahnya. Jika
ArithmeticException
ada yang datang dari menelepondivide
ataueval
, maka ANDA yang harus melakukan pengecualian menangani dan memperbaiki kode Anda, karena Anda lupacheck()
. Untuk alasan yang sama menangkap aNullPointerException
hampir selalu merupakan hal yang salah untuk dilakukan.Sekarang ada beberapa orang yang mengatakan mereka ingin melihat kasus luar biasa dalam metode / fungsi tanda tangan, yaitu untuk secara eksplisit memperluas domain output . Mereka adalah orang-orang yang menyukai pengecualian yang diperiksa . Tentu saja, mengubah domain output harus memaksa kode penelepon langsung untuk beradaptasi, dan itu memang akan dicapai dengan pengecualian yang diperiksa. Tetapi Anda tidak perlu pengecualian untuk itu! Itu sebabnya Anda memiliki
Nullable<T>
kelas generik , kelas kasus , tipe data aljabar , dan tipe gabungan . Beberapa orang OO bahkan mungkin lebih suka kembalinull
untuk kasus kesalahan sederhana seperti ini:Secara teknis pengecualian dapat digunakan untuk tujuan seperti di atas, tapi di sini adalah titik: pengecualian tidak ada untuk penggunaan tersebut . Pengecualian adalah pro abstraksi. Pengecualian adalah tentang tipuan. Pengecualian memungkinkan untuk memperpanjang domain "hasil" tanpa memutus kontrak klien langsung , dan menunda penanganan kesalahan ke "tempat lain". Jika kode Anda melempar pengecualian yang ditangani oleh penelepon langsung dari kode yang sama, tanpa lapisan abstraksi di antaranya, maka Anda melakukannya SALAH
BAGAIMANA CARA MENCAPAI KEMUDIAN?
Jadi inilah kita. Saya telah berargumen untuk menunjukkan bahwa menggunakan pengecualian dalam skenario di atas bukanlah bagaimana pengecualian dimaksudkan untuk digunakan. Namun ada kasus penggunaan asli, di mana abstraksi dan tipuan yang ditawarkan oleh penanganan pengecualian sangat diperlukan. Memahami penggunaan tersebut akan membantu memahami rekomendasi keterlambatan juga.
Kasus penggunaan itu adalah: Pemrograman Terhadap Abstraksi Sumberdaya ...
Ya, logika bisnis harus diprogram terhadap abstraksi , bukan implementasi konkret. Kode "perkabelan" kabel IOC tingkat atas akan membuat implementasi nyata dari abstraksi sumber daya, dan meneruskannya ke logika bisnis. Tidak ada yang baru di sini. Tetapi implementasi konkret dari abstraksi sumber daya tersebut berpotensi membuang pengecualian spesifik implementasi mereka sendiri , bukan?
Jadi siapa yang dapat menangani pengecualian khusus implementasi tersebut? Apakah mungkin untuk menangani pengecualian khusus sumber daya sama sekali dalam logika bisnis? Tidak, tidak. Logika bisnis diprogram melawan abstraksi, yang mengecualikan pengetahuan tentang rincian pengecualian khusus implementasi tersebut.
"Aha!", Anda mungkin berkata: "tapi itu sebabnya kami bisa mensubklasifikasikan pengecualian dan membuat hierarki pengecualian" (lihat Mr. Spring !). Biarkan saya memberitahu Anda, itu salah. Pertama, setiap buku yang masuk akal tentang OOP mengatakan warisan konkret itu buruk, namun entah bagaimana komponen inti JVM ini, penanganan perkecualian, terkait erat dengan warisan konkret. Ironisnya, Joshua Bloch tidak mungkin menulis buku Java yang efektif sebelum dia bisa mendapatkan pengalaman dengan JVM yang berfungsi, bukan? Ini lebih merupakan buku "pelajaran yang dipelajari" untuk generasi berikutnya. Kedua, dan yang lebih penting, jika Anda menangkap pengecualian tingkat tinggi, bagaimana Anda akan MENANGANInya?
PatientNeedsImmediateAttentionException
: apakah kita harus memberinya pil atau mengamputasi kakinya !? Bagaimana dengan pernyataan switch atas semua subclass yang mungkin? Ini polimorfisme Anda, abstraksi. Anda mengerti maksudnya.Jadi siapa yang bisa menangani pengecualian khusus sumber daya? Pasti orang yang tahu konkretnya! Orang yang instantiated sumber daya! Kode "kabel" tentu saja! Lihat ini:
Logika bisnis dikodekan melawan abstraksi ... TANPA PENANGANAN KESALAHAN SUMBER BETON!
Sementara itu di tempat lain implementasi konkret ...
Dan akhirnya kode pengkabelan ... Siapa yang menangani pengecualian sumber daya konkret? Orang yang tahu tentang mereka!
Sekarang bersamaku. Kode di atas adalah sederhana. Anda mungkin mengatakan Anda memiliki aplikasi perusahaan / wadah web dengan beberapa lingkup sumber daya yang dikelola wadah IOC, dan Anda perlu mencoba ulang secara otomatis dan menginisialisasi ulang sumber daya sesi atau permintaan lingkup, dll. Logika pengkabelan pada lingkup tingkat yang lebih rendah mungkin diberikan pabrik abstrak kepada membuat sumber daya, oleh karena itu tidak menyadari implementasi yang tepat. Hanya lingkup tingkat yang lebih tinggi yang benar-benar akan tahu pengecualian apa yang dapat dilemparkan oleh sumber daya tingkat rendah itu. Sekarang tunggu!
Sayangnya, pengecualian hanya memungkinkan tipuan atas tumpukan panggilan, dan cakupan yang berbeda dengan kardinalitas yang berbeda biasanya berjalan pada beberapa utas berbeda. Tidak ada cara untuk berkomunikasi melalui itu dengan pengecualian. Kami membutuhkan sesuatu yang lebih kuat di sini. Jawaban: meneruskan pesan async . Tangkap setiap pengecualian pada akar lingkup tingkat bawah. Jangan abaikan apa pun, jangan biarkan apa pun lolos. Ini akan menutup dan membuang semua sumber daya yang dibuat pada tumpukan panggilan dari ruang lingkup saat ini. Kemudian menyebarkan pesan kesalahan ke ruang lingkup lebih tinggi dengan menggunakan antrian / saluran pesan dalam penanganan rutin pengecualian, sampai Anda mencapai tingkat di mana konkresi diketahui. Itulah pria yang tahu cara menanganinya.
SUMMA SUMMARUM
Jadi menurut interpretasi saya, catch late (terlambat menangkap) berarti menangkap pengecualian di tempat yang paling nyaman DI MANA ANDA TIDAK MELANGGAR ABSTRAKSI LAGI . Jangan sampai terlalu cepat! Tangkap pengecualian pada layer tempat Anda membuat pengecualian konkrit yang melontarkan abstraksi sumber daya, layer yang mengetahui konkresi abstraksi. Lapisan "kabel".
HTH. Selamat coding!
sumber
WrappedFirstResourceException
atauWrappedSecondResourceException
dan membutuhkan lapisan "kabel" untuk melihat ke dalam pengecualian itu untuk melihat akar penyebab masalah ...FailingInputResource
pengecualian apa pun akan merupakan hasil dari operasi denganin1
. Sebenarnya, saya pikir dalam banyak kasus pendekatan yang tepat adalah memiliki lapisan kabel melewati objek penanganan pengecualian, dan meminta lapisan bisnis menyertakancatch
yang kemudian memanggilhandleException
metode objek itu . Metode itu dapat melakukan rethrow, atau menyediakan data default, atau memasang prompt "Abort / Retry / Fail" dan biarkan operator memutuskan apa yang harus dilakukan, dll. Tergantung pada apa yang diperlukan aplikasi.UnrecoverableInternalException
, mirip dengan kode kesalahan HTTP 500.doMyBusiness
metode statis . Ini demi singkatnya, dan sangat mungkin untuk membuatnya lebih dinamis.Handler
Kelas semacam itu akan dipakai dengan beberapa sumber input / output, dan memilikihandle
metode yang menerima kelas yang mengimplementasikan aReusableBusinessLogicInterface
. Anda kemudian dapat menggabungkan / mengkonfigurasi untuk menggunakan implementasi penangan, sumber daya, dan logika bisnis yang berbeda di lapisan kabel di suatu tempat di atasnya.Untuk menjawab pertanyaan ini dengan benar, mari kita mundur dan mengajukan pertanyaan yang bahkan lebih mendasar.
Mengapa kita memiliki pengecualian?
Kami memberikan pengecualian agar penelepon metode kami tahu bahwa kami tidak dapat melakukan apa yang diminta. Jenis pengecualian menjelaskan mengapa kami tidak bisa melakukan apa yang ingin kami lakukan.
Mari kita lihat beberapa kode:
Kode ini jelas dapat membuang pengecualian referensi nol jika
PropertyB
nol. Ada dua hal yang bisa kita lakukan dalam kasus ini untuk "memperbaiki" situasi ini. Kita bisa:Membuat PropertyB di sini bisa sangat berbahaya. Apa alasan metode ini untuk membuat PropertyB? Tentunya ini akan melanggar prinsip tanggung jawab tunggal. Dalam semua kemungkinan, jika PropertyB tidak ada di sini, itu menunjukkan bahwa ada sesuatu yang salah. Metode ini dipanggil pada objek yang dibangun sebagian atau PropertyB diatur ke nol salah. Dengan membuat PropertyB di sini, kita bisa menyembunyikan bug yang jauh lebih besar yang bisa menggigit kita nanti, seperti bug yang menyebabkan korupsi data.
Sebaliknya, jika kami membiarkan referensi nol muncul, kami membiarkan pengembang yang memanggil metode ini tahu, sesegera mungkin, bahwa ada yang tidak beres. Prasyarat penting dalam memanggil metode ini telah terlewatkan.
Jadi sebenarnya, kita melempar lebih awal karena itu memisahkan keprihatinan kita jauh lebih baik. Segera setelah kesalahan terjadi, kami membiarkan pengembang hulu tahu tentang itu.
Mengapa kita "terlambat menangkap" adalah cerita yang berbeda. Kami tidak benar-benar ingin menangkap terlambat, kami benar-benar ingin menangkap sedini mungkin kami tahu bagaimana menangani masalah dengan benar. Beberapa saat ini akan menjadi lima belas lapisan abstraksi nanti dan beberapa waktu ini akan berada pada titik penciptaan.
Intinya adalah bahwa kita ingin menangkap pengecualian pada lapisan abstraksi yang memungkinkan kita untuk menangani pengecualian pada titik di mana kita memiliki semua informasi yang kita butuhkan untuk menangani pengecualian dengan benar.
sumber
if(PropertyB == null) return 0;
Lempar segera setelah Anda melihat sesuatu yang layak dibuang untuk menghindari meletakkan benda dalam keadaan tidak valid. Berarti bahwa jika sebuah pointer nol dilewatkan, Anda memeriksanya lebih awal dan melempar NPE sebelum memiliki kesempatan untuk mengalir ke level rendah.
Tangkap segera setelah Anda tahu apa yang harus dilakukan untuk memperbaiki kesalahan (ini biasanya bukan tempat Anda melempar jika tidak, Anda hanya dapat menggunakan if-else), jika parameter yang tidak valid dilewati maka layer yang menyediakan parameter harus berurusan dengan konsekuensi .
sumber
Aturan bisnis yang valid adalah 'jika perangkat lunak tingkat bawah gagal menghitung nilai, maka ...'
Ini hanya dapat diekspresikan pada level yang lebih tinggi, jika perangkat lunak level bawah mencoba mengubah perilakunya berdasarkan kebenarannya sendiri, yang hanya akan berakhir dengan simpul.
sumber
Pertama-tama, pengecualian untuk situasi luar biasa. Dalam contoh Anda, tidak ada angka yang dapat dihitung jika data mentah tidak ada karena tidak dapat dimuat.
Dari pengalaman saya, praktik yang baik untuk pengecualian abstrak sambil berjalan tumpukan. Biasanya poin di mana Anda ingin melakukan ini adalah setiap kali pengecualian melewati batas antara dua lapisan.
Jika ada kesalahan dengan mengumpulkan data mentah Anda di lapisan data, berikan pengecualian untuk memberi tahu siapa pun yang meminta data. Jangan mencoba untuk mengatasi masalah ini di sini. Kompleksitas kode penanganan bisa sangat tinggi. Lapisan data juga hanya bertanggung jawab untuk meminta data, bukan untuk menangani kesalahan yang terjadi saat melakukan ini. Inilah yang dimaksud dengan "melempar lebih awal" .
Dalam contoh Anda layer penangkap adalah lapisan layanan. Layanan itu sendiri adalah lapisan baru, duduk di atas lapisan akses data. Jadi, Anda ingin menangkap pengecualian di sana. Mungkin layanan Anda memiliki beberapa infrastruktur failover dan mencoba meminta data dari repositori lain. Jika ini juga gagal, bungkus pengecualian di dalam sesuatu yang dipahami oleh penelepon layanan (jika ini adalah layanan web, ini mungkin kesalahan SOAP). Tetapkan pengecualian asli sebagai pengecualian dalam sehingga lapisan selanjutnya dapat mencatat apa yang salah.
Kesalahan layanan dapat ditangkap oleh lapisan memanggil layanan (misalnya UI). Dan inilah yang dimaksud dengan "terlambat menangkap" . Jika Anda tidak dapat menangani pengecualian di lapisan bawah, rethrow itu. Jika lapisan paling atas tidak dapat menangani pengecualian, tangani itu! Ini mungkin termasuk mencatat atau mempresentasikannya.
Alasan mengapa Anda harus mengubah kembali pengecualian (seperti dijelaskan di atas dengan membungkusnya dalam pengecualian yang lebih umum) adalah, bahwa pengguna sangat mungkin tidak dapat memahami bahwa ada kesalahan karena, misalnya, penunjuk yang menunjuk ke memori yang tidak valid. Dan dia tidak peduli. Dia hanya peduli bahwa angka itu tidak dapat dihitung oleh layanan dan ini adalah informasi yang harus ditampilkan kepadanya.
Pergi lebih jauh Anda dapat (di dunia yang ideal) sepenuhnya meninggalkan
try
/catch
kode dari UI. Alih-alih menggunakan pengendali pengecualian global yang mampu memahami pengecualian yang mungkin dilemparkan oleh lapisan bawah, tulis mereka ke dalam beberapa log dan bungkus mereka menjadi objek kesalahan yang berisi informasi kesalahan yang bermakna (dan mungkin dilokalkan). Objek-objek itu dapat dengan mudah disajikan kepada pengguna dalam bentuk apa pun yang Anda inginkan (kotak pesan, pemberitahuan, pesan bersulang, dan sebagainya).sumber
Melempar pengecualian pada awal umumnya adalah praktik yang baik karena Anda tidak ingin kontrak yang rusak mengalir melalui kode lebih jauh dari yang diperlukan. Misalnya, jika Anda mengharapkan parameter fungsi tertentu menjadi bilangan bulat positif maka Anda harus menerapkan batasan itu pada titik pemanggilan fungsi alih-alih menunggu sampai variabel itu digunakan di tempat lain dalam tumpukan kode.
Mengejar terlambat Saya tidak bisa mengomentari karena saya punya aturan sendiri dan itu berubah dari proyek ke proyek. Satu hal yang saya coba lakukan adalah memisahkan pengecualian menjadi dua kelompok. Satu untuk penggunaan internal saja dan yang lainnya untuk penggunaan eksternal. Pengecualian internal ditangkap dan ditangani oleh kode saya sendiri dan pengecualian eksternal dimaksudkan untuk ditangani oleh kode apa pun yang memanggil saya. Ini pada dasarnya adalah bentuk menangkap hal-hal kemudian tetapi tidak cukup karena itu menawarkan saya fleksibilitas untuk menyimpang dari aturan ketika diperlukan dalam kode internal.
sumber