Mengapa metode tidak seharusnya melempar beberapa jenis pengecualian yang diperiksa?

47

Kami menggunakan SonarQube untuk menganalisis kode Java kami dan memiliki aturan ini (set ke kritis):

Metode publik harus membuang paling banyak satu pengecualian yang diperiksa

Menggunakan pengecualian yang diperiksa memaksa pemanggil metode untuk menangani kesalahan, baik dengan menyebarkannya atau dengan menanganinya. Ini membuat pengecualian tersebut sepenuhnya menjadi bagian dari API metode ini.

Agar kompleksitas penelepon masuk akal, metode tidak boleh melempar lebih dari satu jenis pengecualian yang diperiksa. "

Bit lain di Sonar memiliki ini :

Metode publik harus membuang paling banyak satu pengecualian yang diperiksa

Menggunakan pengecualian yang diperiksa memaksa pemanggil metode untuk menangani kesalahan, baik dengan menyebarkannya atau dengan menanganinya. Ini membuat pengecualian tersebut sepenuhnya menjadi bagian dari API metode ini.

Agar kompleksitas penelepon masuk akal, metode tidak boleh melempar lebih dari satu jenis pengecualian yang diperiksa.

Kode berikut:

public void delete() throws IOException, SQLException {      // Non-Compliant
  /* ... */
}

harus di refactored menjadi:

public void delete() throws SomeApplicationLevelException {  // Compliant
    /* ... */
}

Metode utama tidak dicentang oleh aturan ini dan diizinkan untuk melemparkan beberapa pengecualian.

Saya tidak pernah menemukan aturan / rekomendasi ini dalam bacaan saya tentang penanganan pengecualian dan telah mencoba menemukan beberapa standar, diskusi, dll. Tentang topik tersebut. Satu-satunya hal yang saya temukan adalah ini dari CodeRach: Berapa banyak pengecualian yang harus dilontarkan metode paling banyak?

Apakah ini standar yang diterima dengan baik?

sdoca
sumber
7
Apa yang kamu pikirkan Penjelasan yang Anda kutip dari SonarQube tampaknya masuk akal; apakah Anda punya alasan untuk meragukannya?
Robert Harvey
3
Saya telah menulis banyak kode yang membuang lebih dari satu pengecualian dan telah menggunakan banyak perpustakaan yang juga melemparkan lebih dari satu pengecualian. Juga, dalam buku / artikel tentang penanganan pengecualian, topik membatasi jumlah pengecualian yang dilontarkan biasanya tidak diangkat. Tetapi banyak dari contoh menunjukkan melempar / menangkap banyak memberikan persetujuan implisit untuk latihan. Jadi, saya menemukan aturan yang mengejutkan dan ingin melakukan penelitian lebih lanjut tentang praktik terbaik / filosofi penanganan pengecualian versus hanya contoh cara dasar.
sdoca

Jawaban:

32

Mari kita pertimbangkan situasi di mana Anda memiliki kode yang disediakan:

public void delete() throws IOException, SQLException {      // Non-Compliant
  /* ... */
}

Bahayanya di sini adalah bahwa kode yang Anda tulis untuk dihubungi delete()akan terlihat seperti:

try {
  foo.delete()
} catch (Exception e) {
  /* ... */
}

Ini juga buruk. Dan itu akan ditangkap dengan aturan lain bahwa bendera menangkap kelas Exception dasar.

Kuncinya adalah untuk tidak menulis kode yang membuat Anda ingin menulis kode yang buruk di tempat lain.

Aturan yang Anda temui adalah aturan yang agak umum. Checkstyle memilikinya dalam aturan desainnya:

JumlahKuningan

Membatasi melempar pernyataan ke hitungan yang ditentukan (1 secara default).

Dasar Pemikiran: Pengecualian merupakan bagian dari antarmuka metode. Mendeklarasikan metode untuk melempar terlalu banyak pengecualian yang berakar berbeda membuat penanganan perkecualian berat dan mengarah pada praktik pemrograman yang buruk seperti menulis kode seperti catch (Exception ex). Pemeriksaan ini memaksa pengembang untuk menempatkan pengecualian ke dalam hierarki sehingga dalam kasus yang paling sederhana, hanya satu jenis pengecualian yang perlu diperiksa oleh penelepon tetapi setiap subclass dapat ditangkap secara khusus jika perlu.

Ini persis menggambarkan masalah dan apa masalahnya dan mengapa Anda tidak harus melakukannya. Ini adalah standar yang diterima dengan baik bahwa banyak alat analisis statis akan mengidentifikasi dan menandai.

Dan sementara Anda dapat melakukannya sesuai dengan desain bahasa, dan mungkin ada saat-saat itu adalah hal yang benar untuk dilakukan, itu adalah sesuatu yang harus Anda lihat dan segera lanjutkan "um, mengapa saya melakukan ini?" Mungkin dapat diterima untuk kode internal di mana setiap orang cukup disiplin untuk tidak pernah catch (Exception e) {}, tetapi lebih sering daripada tidak saya pernah melihat orang mengambil jalan pintas terutama dalam situasi internal.

Jangan membuat orang menggunakan kelas Anda ingin menulis kode yang buruk.


Saya harus menunjukkan bahwa pentingnya ini dikurangi dengan Java SE 7 dan kemudian karena pernyataan tangkap tunggal dapat menangkap beberapa pengecualian ( Menangkap Beberapa Jenis Pengecualian dan Memotong Kembali Pengecualian dengan Memeriksa Jenis yang Ditingkatkan dari Oracle).

Dengan Java 6 dan sebelumnya, Anda akan memiliki kode yang terlihat seperti:

public void delete() throws IOException, SQLException {
  /* ... */
}

dan

try {
  foo.delete()
} catch (IOException ex) {
     logger.log(ex);
     throw ex;
} catch (SQLException ex) {
     logger.log(ex);
     throw ex;
}

atau

try {
    foo.delete()
} catch (Exception ex) {
    logger.log(ex);
    throw ex;
}

Tak satu pun dari opsi ini dengan Java 6 yang ideal. Pendekatan pertama melanggar KERING . Beberapa blok melakukan hal yang sama, berulang-ulang - satu kali untuk setiap pengecualian. Anda ingin mencatat pengecualian dan melakukan rethrow? Baik. Baris kode yang sama untuk setiap pengecualian.

Opsi kedua lebih buruk karena beberapa alasan. Pertama, itu berarti Anda menangkap semua pengecualian. Null pointer tertangkap di sana (dan seharusnya tidak). Selain itu, Anda rethrowing sebuah Exceptionyang berarti bahwa metode tanda tangan akan deleteSomething() throws Exceptionyang hanya membuat berantakan lebih jauh stack sebagai orang yang menggunakan kode Anda sekarang dipaksa untuk catch(Exception e).

Dengan Java 7, ini tidak seperti yang penting karena Anda malah dapat melakukan:

catch (IOException|SQLException ex) {
    logger.log(ex);
    throw ex;
}

Selanjutnya, jenis memeriksa jika salah satu tidak menangkap jenis pengecualian yang dilemparkan:

public void rethrowException(String exceptionName)
throws IOException, SQLException {
    try {
        foo.delete();
    } catch (Exception e) {
        throw e;
    }
}

Pemeriksa tipe akan mengenali bahwa emungkin hanya tipe IOExceptionatau SQLException. Saya masih tidak terlalu antusias tentang penggunaan gaya ini, tapi itu tidak menyebabkan kode seburuk di bawah Java 6 (di mana itu akan memaksa Anda untuk memiliki metode tanda tangan menjadi superclass yang pengecualiannya diperluas).

Terlepas dari semua perubahan ini, banyak alat analisis statis (Sonar, PMD, Checkstyle) masih menerapkan panduan gaya Java 6. Itu bukan hal yang buruk. Saya cenderung setuju dengan peringatan ini agar tetap diberlakukan, tetapi Anda dapat mengubah prioritasnya menjadi mayor atau minor sesuai dengan cara tim Anda memprioritaskannya.

Jika pengecualian harus diperiksa atau dicentang ... yang adalah masalah g r e a t debat yang satu dapat dengan mudah menemukan posting blog yang tak terhitung jumlahnya mengambil setiap sisi argumen. Namun, jika Anda bekerja dengan pengecualian yang diperiksa, Anda mungkin harus menghindari melempar beberapa jenis, setidaknya di bawah Java 6.

other_paul
sumber
Menerima ini sebagai jawaban ketika menjawab pertanyaan saya tentang hal itu menjadi standar yang diterima dengan baik. Namun, saya masih membaca berbagai diskusi pro / kontra yang dimulai dengan tautan yang diberikan Panzercrisis dalam jawabannya untuk menentukan standar saya nantinya.
sdoca
"Pemeriksa tipe akan mengenali bahwa e mungkin hanya tipe IOException atau SQLException.": Apa artinya ini? Apa yang terjadi ketika pengecualian tipe lain dilempar foo.delete()? Apakah masih ketahuan dan di pasang kembali?
Giorgio
@ Giorgio Ini akan menjadi kesalahan waktu kompilasi jika deletemelempar pengecualian yang diperiksa selain IOException atau SQLException dalam contoh itu. Poin kunci yang saya coba buat adalah bahwa metode yang memanggil rethrowException masih akan mendapatkan tipe Exception di Java 7. Di Java 6, semua itu akan digabung menjadi Exceptiontipe umum yang membuat analisis statis dan coders lain sedih.
Saya melihat. Sepertinya agak berbelit-belit bagi saya. Saya akan menemukan itu lebih intuitif untuk melarang catch (Exception e)dan memaksanya untuk menjadi catch (IOException e)atau catch (SQLException e)sebaliknya.
Giorgio
@Giorgio Ini adalah langkah maju bertahap dari Java 6 untuk mencoba membuatnya lebih mudah untuk menulis kode yang baik. Sayangnya, opsi untuk menulis kode yang buruk akan bersama kami untuk waktu yang lama. Ingatlah bahwa Java 7 dapat melakukannya catch(IOException|SQLException ex). Tetapi, jika Anda hanya akan memikirkan kembali pengecualian, memungkinkan pemeriksa tipe untuk menyebarkan tipe aktual pengecualian akan menyederhanakan kode bukanlah hal yang buruk.
22

Alasan bahwa Anda, idealnya, hanya ingin melemparkan satu jenis pengecualian adalah karena melakukan sebaliknya kemungkinan melanggar prinsip Pertanggungjawaban Tunggal dan Ketergantungan . Mari kita gunakan contoh untuk menunjukkan.

Katakanlah kita memiliki metode yang mengambil data dari kegigihan, dan kegigihan itu adalah sekumpulan file. Karena kita berurusan dengan file, kita dapat memiliki FileNotFoundException:

public String getData(int id) throws FileNotFoundException

Sekarang, kami memiliki perubahan dalam persyaratan, dan data kami berasal dari basis data. Alih-alih a FileNotFoundException(karena kita tidak berurusan dengan file), kita sekarang melempar SQLException:

public String getData(int id) throws SQLException

Kita sekarang harus melalui semua kode yang menggunakan metode kita dan mengubah pengecualian yang harus kita periksa, kalau tidak kode tidak akan dikompilasi. Jika metode kita dipanggil jauh dan luas, itu bisa berarti banyak untuk mengubah / meminta orang lain berubah. Butuh banyak waktu, dan orang tidak akan bahagia.

Inversi ketergantungan mengatakan bahwa kita benar-benar tidak boleh membuang salah satu dari pengecualian ini karena mereka mengekspos detail implementasi internal yang sedang kami coba enkapsulasi. Kode panggilan perlu tahu jenis kegigihan apa yang kita gunakan, padahal seharusnya hanya khawatir jika catatan itu dapat diambil. Alih-alih, kita harus melempar pengecualian yang menyampaikan kesalahan pada tingkat abstraksi yang sama seperti yang kami paparkan melalui API kami:

public String getData(int id) throws InvalidRecordException

Sekarang, jika kita mengubah implementasi internal, kita bisa membungkus pengecualian itu dalam InvalidRecordExceptiondan meneruskannya (atau tidak membungkusnya, dan hanya membuang yang baru InvalidRecordException). Kode eksternal tidak tahu atau peduli jenis kegigihan apa yang sedang digunakan. Semuanya dienkapsulasi.


Adapun Tanggung Jawab Tunggal, kita perlu berpikir tentang kode yang melempar beberapa, pengecualian yang tidak terkait. Katakanlah kita memiliki metode berikut:

public Record parseFile(String filename) throws IOException, ParseException

Apa yang bisa kita katakan tentang metode ini? Kita bisa tahu hanya dari tanda tangan bahwa itu membuka file dan mem - parsingnya. Ketika kita melihat konjungsi, seperti "dan" atau "atau" dalam deskripsi metode, kita tahu bahwa itu melakukan lebih dari satu hal; ia memiliki lebih dari satu tanggung jawab . Metode dengan lebih dari satu tanggung jawab sulit untuk dikelola karena dapat berubah jika ada yang berubah. Sebagai gantinya, kita harus memecah metode sehingga mereka memiliki satu tanggung jawab:

public String readFile(String filename) throws IOException
public Record parse(String data) throws ParseException

Kami telah mengekstrak tanggung jawab membaca file dari tanggung jawab mengurai data. Salah satu efek sampingnya adalah sekarang kita dapat meneruskan data String apa pun ke data parse dari sumber apa pun: dalam memori, file, jaringan, dll. Kita juga dapat menguji parselebih mudah sekarang karena kita tidak memerlukan file pada disk untuk menjalankan tes terhadap.


Kadang-kadang benar-benar ada dua (atau lebih) pengecualian yang bisa kita lemparkan dari suatu metode, tetapi jika kita berpegang teguh pada SRP dan DIP, saat-saat kita menghadapi situasi ini menjadi lebih jarang.

cbojar
sumber
Saya sepenuhnya setuju dengan membungkus pengecualian tingkat bawah sesuai contoh Anda. Kami melakukannya secara teratur dan melempar varian MyAppExceptions. Salah satu contoh di mana saya melempar beberapa pengecualian adalah ketika mencoba memperbarui catatan dalam database. Metode ini melempar RecordNotFoundException. Namun, catatan hanya dapat diperbarui jika dalam kondisi tertentu, jadi metode ini juga melempar InvalidRecordStateException. Saya pikir ini valid dan memberi pemanggil informasi yang berharga.
sdoca
@ sdoca Jika updatemetode Anda adalah atomik seperti yang Anda bisa lakukan, dan pengecualian berada pada tingkat abstraksi yang tepat, maka ya, sepertinya Anda memang perlu melempar dua jenis pengecualian, karena ada dua kasus luar biasa. Itu harus menjadi ukuran berapa banyak pengecualian yang dapat dilemparkan, daripada aturan-aturan tipis (kadang-kadang sewenang-wenang) ini.
cbojar
2
Tetapi jika saya memiliki metode yang membaca data dari aliran dan mem-parsingnya seiring berjalannya waktu, saya tidak bisa memecah dua fungsi tersebut tanpa membaca seluruh aliran menjadi buffer, yang bisa jadi berat. Selanjutnya kode yang memutuskan bagaimana menangani dengan benar pengecualian dapat terpisah dari kode yang melakukan pembacaan dan penguraian. Ketika saya menulis kode pembacaan dan penguraian, saya tidak tahu bagaimana kode yang memanggil kode saya mungkin ingin menangani kedua jenis Pengecualian, jadi saya harus membiarkan keduanya lewat.
user3294068
+1: Saya sangat suka jawaban ini, terutama karena menangani masalah dari sudut pandang pemodelan. Seringkali tidak perlu menggunakan idiom lain (seperti catch (IOException | SQLException ex)) karena masalah sebenarnya ada dalam model / desain program.
Giorgio
3

Saya ingat bermain-main dengan ini sedikit ketika bermain dengan Java beberapa waktu lalu, tapi saya tidak benar-benar menyadari perbedaan antara diperiksa dan tidak dicentang sampai saya membaca pertanyaan Anda. Saya menemukan artikel ini di Google cukup cepat, dan masuk ke beberapa kontroversi yang jelas:

http://tutorials.jenkov.com/java-exception-handling/checked-or-unchecked-exceptions.html

Yang sedang berkata, salah satu masalah yang orang ini sebutkan dengan pengecualian diperiksa adalah bahwa (dan saya pribadi mengalami ini sejak awal dengan Jawa) jika Anda terus menambahkan banyak pengecualian diperiksa untuk throwsklausa dalam deklarasi metode Anda, tidak hanya apakah Anda harus memasukkan kode boilerplate lebih banyak untuk mendukungnya saat Anda pindah ke metode tingkat yang lebih tinggi, tetapi itu juga hanya membuat kompatibilitas yang lebih besar dan merusak istirahat ketika Anda mencoba memperkenalkan lebih banyak jenis pengecualian untuk metode tingkat yang lebih rendah. Jika Anda menambahkan tipe pengecualian yang dicentang ke metode level yang lebih rendah, maka Anda harus menjalankan kembali kode Anda dan menyesuaikan beberapa deklarasi metode lainnya juga.

Salah satu poin mitigasi yang disebutkan dalam artikel - dan penulis tidak suka ini secara pribadi - adalah membuat pengecualian kelas dasar, membatasi throwsklausa Anda untuk hanya menggunakannya, dan kemudian hanya menaikkan subkelasnya secara internal. Dengan begitu Anda bisa membuat tipe pengecualian yang baru diperiksa tanpa harus menjalankan kembali semua kode Anda.

Penulis artikel ini mungkin tidak terlalu menyukai ini, tetapi masuk akal dalam pengalaman pribadi saya (terutama jika Anda dapat melihat apa semua subclass itu), dan saya yakin itulah sebabnya saran yang Anda berikan adalah untuk tutup semua untuk satu jenis pengecualian yang diperiksa masing-masing. Terlebih lagi adalah bahwa saran yang Anda sebutkan sebenarnya memungkinkan untuk beberapa jenis pengecualian diperiksa dalam metode non-publik, yang masuk akal jika ini adalah motif mereka (atau bahkan sebaliknya). Jika itu hanya metode pribadi atau yang serupa, Anda tidak akan menjalankan setengah dari basis kode ketika Anda mengubah satu hal kecil.

Sebagian besar Anda memang bertanya apakah ini merupakan standar yang dapat diterima, tetapi di antara riset yang Anda sebutkan, artikel yang cukup masuk akal ini, dan hanya berbicara dari pengalaman pemrograman pribadi, sepertinya tidak terlalu menonjol.

Panzercrisis
sumber
2
Mengapa tidak hanya menyatakan melempar Throwable, dan selesai dengan itu, alih-alih menciptakan semua hierarki Anda sendiri?
Deduplicator
@Dupuplikator Itulah alasan mengapa penulis juga tidak menyukai ide itu; dia hanya berpikir Anda mungkin menggunakan semua yang tidak dicentang, jika Anda akan melakukannya. Tetapi jika siapa pun yang menggunakan API (mungkin rekan kerja Anda) memiliki daftar semua pengecualian subkelas yang berasal dari kelas dasar Anda, saya bisa melihat sedikit manfaat dalam setidaknya membiarkan mereka tahu bahwa semua pengecualian yang diharapkan adalah dalam satu set subclass tertentu; maka jika mereka merasa salah satu dari mereka lebih "dapat dikendalikan" daripada yang lain, mereka tidak akan cenderung untuk melupakan seorang pawang yang spesifik untuk itu.
Panzercrisis
Alasan mengecek pengecualian pada umumnya adalah karma buruk itu sederhana: Mereka viral, menginfeksi setiap pengguna. Menentukan spesifikasi seluas mungkin yang disengaja adalah cara untuk mengatakan bahwa semua pengecualian tidak dicentang, dan dengan demikian menghindari kekacauan. Ya, mendokumentasikan apa yang mungkin ingin Anda tangani adalah ide yang bagus, untuk dokumentasi : Mengetahui pengecualian mana saja yang berasal dari suatu fungsi memiliki nilai yang sangat terbatas (selain tidak ada sama sekali / mungkin satu, tetapi Java tetap tidak mengizinkannya) .
Deduplicator
1
@Dupuplikator Saya tidak mendukung gagasan itu, dan saya juga tidak mendukungnya. Saya hanya berbicara tentang arah dari mana saran OP diberikan mungkin berasal.
Panzercrisis
1
Terima kasih untuk tautannya. Itu adalah tempat awal yang bagus untuk membaca topik ini.
sdoca
-1

Melemparkan beberapa pengecualian yang diperiksa masuk akal ketika ada beberapa hal yang masuk akal untuk dilakukan.

Misalnya katakanlah Anda memiliki metode

public void doSomething(Credentials cred, Work work) 
    throws CredentialsRequiredException, TryAgainLaterException{...}

ini melanggar aturan pengecualian pne, tetapi masuk akal.

Sayangnya, apa yang biasanya terjadi adalah metode

void doSomething() 
    throws IOException, JAXBException,SQLException,MyException {...}

Di sini ada sedikit kesempatan bagi penelepon untuk melakukan sesuatu yang spesifik berdasarkan jenis pengecualian. Jadi jika kita ingin mendorongnya untuk menyadari bahwa metode ini BISA dan kadang-kadang AKAN salah, melempar SomethingMightGoWrongException hanya enogh dan lebih baik.

Karenanya aturan paling banyak satu pengecualian diperiksa.

Tetapi jika proyek Anda menggunakan desain di mana ada beberapa pengecualian yang bermakna diperiksa, aturan ini seharusnya tidak berlaku.

Sidenote: Sesuatu sebenarnya bisa salah hampir di mana-mana, jadi orang bisa berpikir untuk menggunakan? memperluas RuntimeException, tetapi ada perbedaan antara "kita semua melakukan kesalahan" dan "ini berbicara sistem eksternal dan kadang-kadang AKAN turun, atasi itu".

pengguna470365
sumber
1
"banyak hal yang masuk akal untuk dilakukan" hampir tidak sesuai dengan srp - poin ini dijelaskan dalam jawaban sebelumnya
agas
Itu benar. Anda dapat DETECT satu masalah dalam satu fungsi (menangani satu aspek) dan masalah lain di lain (juga menangani satu aspek) dipanggil dari satu fungsi menangani satu hal, yaitu memanggil dua fungsi ini. Dan penelepon dapat dalam satu lapisan try catch (dalam satu fungsi) MENANGANI satu masalah dan meneruskan masalah lain dan mengatasinya sendiri.
user470365