Apakah melempar RuntimeExceptions baru ke dalam kode yang tidak terjangkau merupakan gaya yang buruk?

31

Saya ditugaskan untuk memelihara aplikasi yang ditulis beberapa waktu lalu oleh pengembang yang lebih terampil. Saya menemukan potongan kode ini:

public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
        try {
            return translate(mailManagementService.retrieveUserMailConfiguration(id));
        } catch (Exception e) {
            rethrow(e);
        }
        throw new RuntimeException("cannot reach here");
    }

Saya ingin tahu apakah melempar RuntimeException("cannot reach here")dibenarkan. Saya mungkin kehilangan sesuatu yang jelas mengetahui bahwa kode ini berasal dari kolega yang lebih berpengalaman.
EDIT: Ini adalah badan rethrow yang beberapa jawaban disebut. Saya menganggap itu tidak penting dalam pertanyaan ini.

private void rethrow(Exception e) throws MailException {
        if (e instanceof InvalidDataException) {
            InvalidDataException ex = (InvalidDataException) e;
            rethrow(ex);
        }
        if (e instanceof EntityAlreadyExistsException) {
            EntityAlreadyExistsException ex = (EntityAlreadyExistsException) e;
            rethrow(ex);
        }
        if (e instanceof EntityNotFoundException) {
            EntityNotFoundException ex = (EntityNotFoundException) e;
            rethrow(ex);
        }
        if (e instanceof NoPermissionException) {
            NoPermissionException ex = (NoPermissionException) e;
            rethrow(ex);
        }
        if (e instanceof ServiceUnavailableException) {
            ServiceUnavailableException ex = (ServiceUnavailableException) e;
            rethrow(ex);
        }
        LOG.error("internal error, original exception", e);
        throw new MailUnexpectedException();
    }


private void rethrow(ServiceUnavailableException e) throws
            MailServiceUnavailableException {
        throw new MailServiceUnavailableException();
    }

private void rethrow(NoPermissionException e) throws PersonNotAuthorizedException {
    throw new PersonNotAuthorizedException();
}

private void rethrow(InvalidDataException e) throws
        MailInvalidIdException, MailLoginNotAvailableException,
        MailInvalidLoginException, MailInvalidPasswordException,
        MailInvalidEmailException {
    switch (e.getDetail()) {
        case ID_INVALID:
            throw new MailInvalidIdException();
        case LOGIN_INVALID:
            throw new MailInvalidLoginException();
        case LOGIN_NOT_ALLOWED:
            throw new MailLoginNotAvailableException();
        case PASSWORD_INVALID:
            throw new MailInvalidPasswordException();
        case EMAIL_INVALID:
            throw new MailInvalidEmailException();
    }
}

private void rethrow(EntityAlreadyExistsException e)
        throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
    switch (e.getDetail()) {
        case LOGIN_ALREADY_TAKEN:
            throw new MailLoginNotAvailableException();
        case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
            throw new MailEmailAddressAlreadyForwardedToException();
    }
}

private void rethrow(EntityNotFoundException e) throws
        MailAccountNotCreatedException,
        MailAliasNotCreatedException {
    switch (e.getDetail()) {
        case ACCOUNT_NOT_FOUND:
            throw new MailAccountNotCreatedException();
        case ALIAS_NOT_FOUND:
            throw new MailAliasNotCreatedException();
    }
}
Sok Pomaranczowy
sumber
12
secara teknis dapat dijangkau, jika rethrowgagal sebenarnya throwpengecualian. (yang mungkin terjadi suatu hari, jika implementasinya berubah)
njzk2
34
Selain itu, AssertionError mungkin merupakan pilihan yang secara semantik lebih baik daripada RuntimeException.
JvR
1
Lemparan terakhir adalah mubazir. Jika Anda mengaktifkan findbugs atau alat analisis statis lainnya, itu akan menandai jenis garis ini untuk dihapus.
Bon Ami
7
Sekarang saya melihat sisa kode, saya berpikir mungkin Anda harus mengubah "pengembang yang lebih terampil" menjadi " pengembang yang lebih senior ". Peninjauan Kode akan dengan senang hati menjelaskan alasannya.
JvR
3
Saya telah mencoba membuat contoh hipotetis mengapa pengecualian itu mengerikan. Tapi tidak ada yang pernah saya lakukan memegang lilin untuk ini.
Paul Draper

Jawaban:

36

Pertama, terima kasih telah memperkirakan pertanyaan Anda dan menunjukkan kepada kami apa yang rethrowbisa. Jadi, sebenarnya, apa yang dilakukannya adalah mengubah pengecualian dengan properti menjadi kelas pengecualian yang lebih halus. Lebih lanjut tentang ini nanti.

Karena saya tidak benar-benar menjawab pertanyaan utama pada awalnya, ini dia: ya, umumnya gaya buruk untuk melempar pengecualian runtime dalam kode yang tidak terjangkau; Anda sebaiknya menggunakan pernyataan, atau bahkan lebih baik, menghindari masalah. Seperti yang sudah ditunjukkan, kompiler di sini tidak dapat memastikan bahwa kode tidak pernah keluar dari try/catchblok. Anda dapat memperbaiki kode Anda dengan memanfaatkan ...

Kesalahan adalah nilai

(Tidak mengherankan, itu terkenal di mana saja )

Mari kita gunakan contoh yang lebih sederhana, yang saya gunakan sebelum edit: jadi bayangkan Anda mencatat sesuatu dan membuat pengecualian wrapper seperti dalam jawaban Konrad . Sebut saja logAndWrap.

Alih-alih melempar pengecualian sebagai efek samping logAndWrap, Anda bisa membiarkannya bekerja sebagai efek samping dan membuatnya mengembalikan pengecualian (setidaknya, yang diberikan dalam input). Anda tidak perlu menggunakan obat generik, hanya fungsi dasar:

private Exception logAndWrap(Exception exception) {
    // or whatever it actually does
    Log.e("Ouch! " + exception.getMessage());
    return new CustomWrapperException(exception);
}

Kemudian, Anda dengan throwjelas, dan kompiler Anda senang:

try {
     return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
     throw logAndWrap(e);
}

Bagaimana jika Anda lupa throw?

Seperti dijelaskan dalam komentar Joe23 , cara pemrograman defensif untuk memastikan bahwa pengecualian selalu dilontarkan akan terdiri dari melakukan secara eksplisit throw new CustomWrapperException(exception)pada akhir logAndWrap, seperti yang dilakukan oleh Guava . Dengan begitu, Anda tahu bahwa pengecualian akan dilempar, dan penganalisa tipe Anda senang. Namun, pengecualian khusus Anda harus berupa pengecualian yang tidak dicentang, yang tidak selalu memungkinkan. Juga, saya menilai risiko pengembang hilang untuk menulis throwmenjadi sangat rendah: pengembang harus melupakannya dan metode sekitarnya tidak boleh mengembalikan apa pun, jika tidak kompiler akan mendeteksi pengembalian yang hilang. Ini adalah cara yang menarik untuk melawan sistem tipe, dan itu berhasil.

Rethrow

Sebenarnya rethrowdapat ditulis sebagai fungsi juga, tapi saya punya masalah dengan implementasinya saat ini:

  • Ada banyak gips yang tidak berguna seperti Pemain sebenarnya diperlukan (lihat komentar):

    if (e instanceof ServiceUnavailableException) {
        ServiceUnavailableException ex = (ServiceUnavailableException) e;
        rethrow(ex);
    }
    
  • Saat melempar / mengembalikan pengecualian baru, yang lama dibuang; dalam kode berikut, a MailLoginNotAvailableExceptiontidak mengizinkan saya untuk mengetahui login mana yang tidak tersedia, yang tidak nyaman; selain itu, stacktraces tidak akan lengkap:

    private void rethrow(EntityAlreadyExistsException e)
        throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
        switch (e.getDetail()) {
            case LOGIN_ALREADY_TAKEN:
                throw new MailLoginNotAvailableException();
            case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
                throw new MailEmailAddressAlreadyForwardedToException();
        }
    }
    
  • Mengapa kode asal tidak melempar pengecualian khusus itu di tempat pertama? Saya menduga bahwa rethrowini digunakan sebagai lapisan kompatibilitas antara subsistem (pengiriman) dan logika bisnis (mungkin tujuannya adalah untuk menyembunyikan detail implementasi seperti pengecualian yang dilemparkan dengan menggantinya dengan pengecualian khusus). Bahkan jika saya setuju bahwa akan lebih baik memiliki kode bebas-tangkap, seperti yang disarankan dalam jawaban Pete Becker , saya tidak berpikir Anda akan memiliki kesempatan untuk menghapus catchdan rethrowkode di sini tanpa refactoring besar.

coredump
sumber
1
Para pemain tidak sia-sia. Mereka diperlukan, karena tidak ada pengiriman dinamis berdasarkan jenis argumen. Jenis argumen harus diketahui pada waktu kompilasi untuk memanggil metode yang tepat.
Joe23
Dalam logAndWrapmetode Anda, Anda bisa melempar pengecualian alih-alih mengembalikannya, tetapi simpan jenis kembali (Runtime)Exception. Dengan cara ini catch catch block dapat tetap seperti yang Anda tulis (tanpa membuang kode yang tidak dapat dijangkau). Tetapi memiliki keuntungan tambahan yang tidak dapat Anda lupakan throw. Jika Anda mengembalikan pengecualian dan lupa melempar, ini akan menyebabkan pengecualian tertelan secara diam-diam. Melempar sambil memiliki tipe kembali RuntimeException akan membuat kompiler senang sementara juga menghindari kesalahan ini. Begitulah cara Jambu Throwables.propagatediterapkan.
Joe23
@ Joe23 Terima kasih atas klarifikasi tentang pengiriman statis. Tentang melempar pengecualian di dalam fungsi: apakah maksud Anda bahwa galat yang mungkin Anda uraikan adalah blok penangkap yang memanggil logAndWraptetapi tidak melakukan apa pun dengan pengecualian yang dikembalikan? Itu menarik. Di dalam fungsi yang harus mengembalikan nilai, kompiler akan melihat bahwa ada jalan tanpa nilai yang dikembalikan, tetapi ini berguna untuk metode yang tidak mengembalikan apa pun. Terima kasih lagi.
coredump
1
Itulah tepatnya yang saya katakan. Dan hanya untuk menekankan pokoknya lagi: Ini bukan sesuatu yang saya pikirkan dan ambil pujian, tetapi diperoleh dari sumber Guava.
Joe23
@ Joe23 Saya menambahkan referensi eksplisit ke perpustakaan
coredump
52

rethrow(e);Fungsi ini melanggar prinsip yang mengatakan bahwa dalam keadaan normal, suatu fungsi akan kembali, sementara dalam keadaan luar biasa, suatu fungsi akan melempar pengecualian. Fungsi ini melanggar prinsip ini dengan melemparkan pengecualian dalam keadaan normal. Itulah sumber dari semua kebingungan.

Compiler mengasumsikan bahwa fungsi ini akan kembali dalam keadaan normal, sehingga sejauh yang bisa dikompilasi oleh kompiler, eksekusi dapat mencapai akhir dari retrieveUserMailConfigurationfungsi, di mana saat itu merupakan kesalahan untuk tidak memiliki returnpernyataan. The RuntimeExceptiondilemparkan ada seharusnya meringankan kekhawatiran ini dari kompilator, tetapi merupakan cara yang agak kikuk melakukannya. Cara lain untuk mencegah function must return a valuekesalahan adalah dengan menambahkan return null; //to keep the compiler happypernyataan, tapi itu juga sama kikuk menurut saya.

Jadi, secara pribadi, saya akan mengganti ini:

rethrow(e);

dengan ini:

report(e); 
throw e;

atau, lebih baik lagi, (seperti yang disarankan coredump ,) dengan ini:

throw reportAndTransform(e);

Dengan demikian, aliran kontrol akan dibuat jelas bagi kompiler, sehingga tugas akhir Anda throw new RuntimeException("cannot reach here");tidak hanya menjadi berlebihan, tetapi sebenarnya bahkan tidak dapat dikompilasi, karena akan ditandai oleh kompiler sebagai kode yang tidak dapat dijangkau.

Itu cara yang paling elegan dan sebenarnya juga paling sederhana untuk keluar dari situasi buruk ini.

Mike Nakis
sumber
1
-1 Menyarankan untuk membagi fungsi menjadi dua pernyataan dapat menyebabkan kesalahan pemrograman karena salinan / tempel yang buruk; Saya juga sedikit khawatir tentang fakta bahwa Anda mengedit pertanyaan Anda untuk menyarankan throw reportAndTransform(e)sebuah beberapa menit setelah saya posting jawaban saya sendiri. Mungkin Anda memodifikasi pertanyaan Anda secara independen tentang itu, dan saya minta maaf sebelumnya jika memang demikian, tetapi sebaliknya, memberikan sedikit kredit adalah sesuatu yang biasanya dilakukan orang.
coredump
4
@coredump Saya memang membaca saran Anda, dan saya bahkan membaca jawaban lain yang mengaitkan saran ini kepada Anda, dan saya ingat bahwa saya benar-benar menggunakan konstruksi semacam itu di masa lalu, jadi saya memutuskan untuk memasukkannya dalam jawaban saya. Saya pikir tidak begitu penting untuk memasukkan atribusi, karena kita tidak berbicara tentang ide asli di sini, tetapi jika Anda mempermasalahkannya, saya tidak ingin memperburuk Anda, sehingga atribusi sekarang ada di sana, lengkap dengan tautan. Tepuk tangan.
Mike Nakis
Bukannya saya memiliki paten untuk konstruksi ini, yang cukup umum; tetapi bayangkan Anda menulis jawaban dan tidak lama kemudian, jawaban itu "diasimilasi" oleh yang lain. Jadi atribusi lebih dihargai. Terima kasih.
coredump
3
Tidak ada yang salah per se dengan fungsi yang tidak kembali. Ini terjadi setiap saat, misalnya, dalam sistem waktu nyata. Masalah utamanya adalah kesalahan dalam java di mana bahasa menganalisis dan menegakkan konsep kebenaran bahasa, namun bahasa tidak menyediakan mekanisme untuk entah bagaimana menjelaskan bahwa fungsi tidak kembali. Namun, ada pola desain yang menyiasati ini seperti throw reportAndTransform(e). Banyak pola desain yang merupakan fitur bahasa yang hilang. Ini salah satunya.
David Hammen
2
Di sisi lain, banyak bahasa modern cukup kompleks. Mengubah setiap pola desain menjadi fitur bahasa inti akan mengasapi mereka lebih jauh. Ini adalah contoh yang bagus dari pola desain yang tidak termasuk dalam bahasa inti. Seperti yang ditulis, ini dipahami dengan baik tidak hanya oleh kompiler, tetapi juga oleh banyak alat lain yang harus menguraikan kode.
MSalters
7

The throwmungkin ditambahkan untuk berkeliling "metode harus kembali nilai" kesalahan yang mungkin terjadi - aliran data Java analyzer cukup cerdas untuk memahami bahwa tidak ada returnperlu setelah throw, tapi tidak setelah kustom Anda rethrow()metode, dan tidak ada @NoReturnpenjelasan yang bisa Anda gunakan untuk memperbaikinya.

Namun demikian, membuat Pengecualian baru dalam kode yang tidak terjangkau tampaknya berlebihan. Saya hanya akan menulis return null, mengetahui bahwa hal itu, pada kenyataannya, tidak pernah terjadi.

Kilian Foth
sumber
Kamu benar. Saya memeriksa apa yang akan terjadi jika saya mengomentari throw new...garis dan mengeluh tentang tidak ada pernyataan kembali. Apakah pemrograman itu buruk? Apakah ada konvensi tentang mengganti pernyataan pengembalian yang tidak terjangkau? Saya akan membiarkan pertanyaan ini tidak terjawab untuk sementara waktu untuk mendorong lebih banyak masukan. Meskipun demikian terima kasih atas jawaban Anda.
Sok Pomaranczowy
3
@ SokPomaranczowy Ya, ini pemrograman yang buruk. Bahwa rethrow()metode menyembunyikan fakta bahwa catchini rethrowing pengecualian. Saya akan menghindari melakukan hal seperti itu. Plus, tentu saja, rethrow()sebenarnya mungkin gagal untuk memikirkan kembali pengecualian, di mana hal lain terjadi.
Ross Patterson
26
Tolong jangan ganti pengecualian dengan return null. Dengan pengecualian, jika seseorang di masa depan memecah kode sehingga baris terakhir entah bagaimana menjadi terjangkau, akan segera jelas ketika pengecualian dilemparkan. Jika Anda sebaliknya return null, sekarang Anda memiliki nullnilai mengambang di aplikasi Anda yang tidak diharapkan. Siapa yang tahu di mana dalam aplikasi itu NullPointerExceptionakan muncul? Kecuali jika Anda beruntung dan sedang setelah panggilan ke fungsi ini, akan sangat sulit untuk melacak masalah sebenarnya.
unholysampler
5
@MatthewRead Eksekusi kode yang dimaksudkan untuk tidak dapat dijangkau adalah bug yang parah, return nullsangat mungkin untuk menutupi bug karena Anda tidak dapat membedakan kasus-kasus lain di mana ia kembali nulldari bug ini.
CodesInChaos
4
Lebih jauh, jika fungsi sudah mengembalikan null dalam beberapa keadaan, dan Anda juga menggunakan pengembalian nol dalam keadaan ini, maka pengguna sekarang memiliki dua skenario yang mungkin mereka berada di saat mereka mendapatkan pengembalian nol. Terkadang ini tidak masalah, karena null return sudah berarti "tidak ada objek dan saya tidak menentukan alasannya", jadi menambahkan satu lagi alasan yang mungkin mengapa membuat sedikit perbedaan. Tetapi sering menambahkan ambiguitas itu buruk, dan itu tentu saja berarti bahwa bug pada awalnya akan diperlakukan seperti "keadaan tertentu" dan bukannya langsung terlihat seperti "ini rusak". Gagal lebih awal.
Steve Jessop
6

Itu

throw new RuntimeException("cannot reach here");

pernyataan menjelaskan kepada PERSON yang membaca kode apa yang terjadi, jadi jauh lebih baik daripada mengembalikan nol misalnya. Ini juga memudahkan untuk debug jika kode diubah dengan cara yang tidak terduga.

Namun rethrow(e)sepertinya salah! Jadi dalam kasus Anda, saya pikir refactoring kode adalah pilihan yang lebih baik. Lihat jawaban lain (saya suka coredump terbaik) untuk cara memilah kode Anda.

Ian
sumber
4

Saya tidak tahu apakah ada konvensi.

Bagaimanapun, trik lain adalah dengan melakukannya:

private <T> T rethrow(Exception exception) {
    // or whatever it actually does
    Log.e("Ouch! " + exception.getMessage());
    throw new CustomWrapperException(exception);
}

Mengizinkan ini:

try {
     return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
     return rethrow(e);
}

Dan tidak RuntimeExceptionperlu lagi buatan . Meskipun rethrowtidak pernah benar-benar mengembalikan nilai apa pun, itu cukup baik untuk kompiler sekarang.

Itu mengembalikan nilai dalam teori (metode tanda tangan), dan kemudian itu dibebaskan dari benar-benar melakukannya karena ia melempar pengecualian.

Ya, itu mungkin terlihat aneh, tapi sekali lagi - melempar hantu RuntimeException, atau mengembalikan nol yang tidak akan pernah terlihat di dunia ini - itu juga bukan keindahan.

Berusaha keras agar mudah dibaca, Anda dapat mengganti nama rethrowdan memiliki sesuatu seperti:

} catch (Exception e) {
     return nothingJustRethrow(e);
}
Konrad Morawski
sumber
2

Jika Anda menghapus tryblok sepenuhnya maka Anda tidak perlu rethrowatau throw. Kode ini melakukan hal yang persis sama dengan aslinya:

public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
    return translate(mailManagementService.retrieveUserMailConfiguration(id));
}

Jangan biarkan fakta bahwa itu berasal dari pengembang yang lebih berpengalaman menipu Anda. Ini adalah kode yang membusuk, dan ini terjadi setiap saat. Perbaiki saja.

EDIT: Saya salah membaca rethrow(e)karena hanya melemparkan kembali pengecualian e. Jika rethrowmetode itu benar-benar melakukan sesuatu selain melemparkan kembali pengecualian, maka menyingkirkannya memang mengubah semantik metode ini.

Pete Becker
sumber
Orang-orang akan terus percaya bahwa tangkapan wildcard yang tidak menangani apa pun sebenarnya adalah penangan pengecualian. Versi Anda mengabaikan asumsi yang salah dengan tidak berpura-pura menangani apa yang tidak bisa. Penelepon dari recoverveUserMailConfiguration () mengharapkan untuk mendapatkan sesuatu kembali. Jika fungsi itu tidak dapat mengembalikan sesuatu yang masuk akal, itu harus gagal dengan cepat dan keras. Seolah-olah jawaban lain melihat tidak ada masalah dengan catch(Exception)yang merupakan bau kode menjijikkan.
msw
1
@ msw - Pengkodean kargo-kultus.
Pete Becker
@ msw - di sisi lain, itu memberi Anda tempat untuk mengatur breakpoint.
Pete Becker
1
Seolah-olah Anda tidak melihat masalah membuang seluruh bagian dari logika. Versi Anda jelas tidak melakukan hal yang sama seperti aslinya, yang omong-omong, sudah gagal dengan cepat dan keras. Saya lebih suka menulis metode bebas tangkapan seperti yang ada di jawaban Anda, tetapi ketika bekerja dengan kode yang ada, kita tidak boleh memutus kontrak dengan bagian kerja lainnya. Mungkin apa yang dilakukan rethrowtidak berguna (dan ini bukan intinya di sini), tetapi Anda tidak bisa begitu saja menyingkirkan kode yang tidak Anda sukai dan mengatakan itu setara dengan aslinya ketika jelas tidak. Ada efek samping dalam rethrowfungsi khusus .
coredump
1
@ jpmc26 Inti dari pertanyaan ini adalah tentang pengecualian "tidak bisa sampai di sini", yang bisa muncul karena alasan lain selain dari blok coba / tangkap sebelumnya. Tetapi saya setuju bahwa blok itu berbau amis, dan jika jawaban ini awalnya lebih hati-hati, itu akan mendapatkan lebih banyak suara yang naik turun (saya tidak downvote, btw). Hasil edit terakhir baik.
coredump