Cara mendesain pengecualian

11

Saya bergumul dengan pertanyaan yang sangat sederhana:

Saya sekarang bekerja pada aplikasi server, dan saya perlu menciptakan hierarki untuk pengecualian (beberapa pengecualian sudah ada, tetapi kerangka kerja umum diperlukan). Bagaimana saya mulai melakukan ini?

Saya sedang berpikir untuk mengikuti strategi ini:

1) Apa yang salah?

  • Ada yang bertanya, yang tidak diizinkan.
  • Ada yang bertanya, diizinkan, tetapi tidak berfungsi, karena parameter yang salah.
  • Ada yang bertanya, diizinkan, tetapi tidak berfungsi, karena kesalahan internal.

2) Siapa yang meluncurkan permintaan?

  • Aplikasi klien
  • Aplikasi server lain

3) Penyerahan pesan: karena kita berurusan dengan aplikasi server, ini semua tentang menerima dan mengirim pesan. Jadi bagaimana jika pengiriman pesan salah?

Karena itu, kami mungkin mendapatkan jenis pengecualian berikut:

  • ServerNotAllowedException
  • ClientNotAllowedException
  • ServerParameterException
  • ClientParameterException
  • InternalException (jika server tidak tahu dari mana permintaan itu berasal)
    • ServerInternalException
    • ClientInternalException
  • MessageHandlingException

Ini adalah pendekatan yang sangat umum untuk mendefinisikan hierarki pengecualian, tetapi saya khawatir bahwa saya mungkin kurang memiliki beberapa kasus yang jelas. Apakah Anda memiliki gagasan tentang bidang yang tidak saya bahas, apakah Anda mengetahui adanya kelemahan dari metode ini atau apakah ada pendekatan yang lebih umum untuk pertanyaan semacam ini (dalam kasus terakhir, di mana saya dapat menemukannya)?

Terima kasih sebelumnya

Dominique
sumber
5
Anda tidak menyebutkan apa yang ingin Anda capai dengan hierarki kelas pengecualian Anda (dan itu sama sekali tidak jelas). Pencatatan yang berarti? Memungkinkan klien untuk bereaksi secara wajar terhadap berbagai pengecualian? Atau apa?
Ralf Kleberhoff
2
Mungkin berguna untuk mengerjakan beberapa cerita dan menggunakan case terlebih dahulu, dan melihat apa yang keluar. Misalnya: permintaan klien X , tidak diizinkan. Klien meminta X , tetapi permintaan tersebut tidak valid. Kerjakan melalui mereka dengan memikirkan siapa yang harus menangani pengecualian, apa yang dapat mereka lakukan dengannya (cepat, coba lagi, apa pun), dan informasi apa yang mereka butuhkan untuk melakukannya dengan baik. Kemudian , setelah Anda tahu apa pengecualian konkret Anda dan informasi apa yang diperlukan penangan untuk memprosesnya, Anda dapat membentuknya menjadi hierarki yang bagus.
berguna
1
Saya tidak pernah benar-benar memahami keinginan untuk menggunakan begitu banyak jenis pengecualian. Biasanya untuk sebagian besar catchblok yang saya gunakan, saya tidak menggunakan lebih banyak untuk pengecualian daripada pesan kesalahan apa yang dikandungnya. Saya tidak benar-benar memiliki sesuatu yang berbeda yang dapat saya lakukan untuk pengecualian yang terlibat dalam gagal membaca file sebagai salah satu gagal mengalokasikan memori selama proses membacanya, jadi saya cenderung hanya menangkap std::exceptiondan melaporkan pesan kesalahan yang dikandungnya, mungkin dekorasi dengan "Failed to open file: %s", ex.what()ke buffer tumpukan sebelum mencetaknya.
3
Selain itu saya tidak bisa mengantisipasi semua jenis pengecualian yang akan dilemparkan di tempat pertama. Saya mungkin bisa mengantisipasi mereka sekarang, tetapi rekan-rekan mungkin memperkenalkan yang baru di masa depan, misalnya Jadi agak tidak ada harapan bagi saya untuk mengetahui, untuk sekarang dan selamanya, semua jenis pengecualian yang dapat dilemparkan ke dalam proses suatu operasi. Jadi saya hanya menangkap super umumnya dengan satu blok tangkap tunggal. Saya telah melihat contoh orang menggunakan banyak catchblok berbeda dalam satu situs pemulihan, tetapi sering kali hanya mengabaikan pesan di dalam pengecualian dan mencetak pesan yang lebih lokal ...

Jawaban:

5

Komentar umum

(sedikit bias pendapat)

Saya biasanya tidak akan memilih hierarki pengecualian yang terperinci.

Hal terpenting: sebuah pengecualian memberi tahu penelepon Anda bahwa metode Anda gagal menyelesaikan tugasnya. Dan penelepon Anda harus mengetahui hal itu, jadi dia tidak melanjutkan begitu saja. Itu bekerja dengan pengecualian apa pun, apa pun kelas pengecualian yang Anda pilih.

Aspek kedua adalah logging. Anda ingin menemukan entri log yang bermakna setiap kali terjadi kesalahan. Itu juga tidak memerlukan kelas pengecualian yang berbeda, hanya pesan teks yang dirancang dengan baik (saya kira Anda tidak perlu automat untuk membaca log kesalahan Anda ...).

Aspek ketiga adalah reaksi penelepon Anda. Apa yang bisa dilakukan penelepon Anda ketika dia menerima pengecualian? Di sini masuk akal untuk memiliki kelas pengecualian yang berbeda, sehingga penelepon dapat memutuskan apakah akan mencoba lagi panggilan yang sama, untuk menggunakan solusi yang berbeda (misalnya menggunakan sumber fallback sebagai gantinya), atau menyerah.

Dan mungkin Anda ingin menggunakan pengecualian sebagai dasar untuk memberi tahu pengguna akhir tentang masalah tersebut. Itu berarti membuat pesan yang mudah digunakan selain teks admin untuk file log, tetapi tidak memerlukan kelas pengecualian yang berbeda (walaupun mungkin itu dapat membuat pembuatan teks lebih mudah ...).

Aspek penting untuk pencatatan (dan untuk pesan kesalahan pengguna) adalah kemampuan untuk mengubah pengecualian dengan informasi konteks dengan menangkapnya pada beberapa lapisan, menambahkan beberapa informasi konteks, misalnya parameter metode, dan melemparkannya kembali.

Hierarki Anda

Siapa yang meluncurkan permintaan? Saya tidak berpikir Anda akan memerlukan informasi yang meluncurkan permintaan. Saya bahkan tidak bisa membayangkan bagaimana Anda tahu bahwa jauh di dalam tumpukan panggilan.

Penanganan pesan : itu bukan aspek yang berbeda, tetapi hanya kasus tambahan untuk "Apa yang salah?".

Dalam komentar, Anda berbicara tentang bendera " tanpa penebangan " saat membuat pengecualian. Saya tidak berpikir bahwa di tempat Anda membuat dan melempar pengecualian, Anda dapat membuat keputusan yang dapat diandalkan apakah akan mencatat pengecualian itu atau tidak.

Satu-satunya situasi yang dapat saya bayangkan adalah bahwa beberapa lapisan yang lebih tinggi menggunakan API Anda dengan cara yang kadang-kadang akan menghasilkan pengecualian, dan lapisan ini kemudian tahu bahwa itu tidak perlu mengganggu administrator dengan pengecualian, sehingga diam-diam menelan pengecualian. Tapi itu bau kode: pengecualian yang diharapkan adalah kontradiksi dalam dirinya sendiri, sebuah petunjuk untuk mengubah API. Dan itu adalah lapisan yang lebih tinggi yang harus memutuskan, bukan kode penghasil pengecualian.

Ralf Kleberhoff
sumber
Dalam pengalaman saya, kode kesalahan dalam kombinasi dengan teks ramah pengguna bekerja dengan sangat baik. Admin dapat menggunakan kode kesalahan untuk menemukan informasi tambahan.
Sjoerd
1
Saya suka ide di balik jawaban ini secara umum. Dari pengalaman saya, Pengecualian benar-benar tidak boleh berubah menjadi binatang yang terlalu rumit. Tujuan utama pengecualian adalah untuk memungkinkan kode panggilan untuk mengatasi masalah tertentu dan mendapatkan informasi debug / coba lagi yang relevan tanpa mengacaukan respons fungsi.
greggle138
2

Hal utama yang perlu diingat ketika merancang pola respons kesalahan adalah untuk memastikan itu berguna bagi penelepon. Ini berlaku apakah Anda menggunakan pengecualian atau kode kesalahan yang ditentukan, tetapi kami akan membatasi diri kami untuk memberi ilustrasi dengan pengecualian.

  • Jika bahasa atau kerangka kerja Anda sudah menyediakan kelas pengecualian umum, gunakan kelas yang sesuai dan di mana mereka diharapkan secara wajar . Jangan mendefinisikan kelas Anda sendiri ArgumentNullExceptionatau ArgumentOutOfRangepengecualian. Penelepon tidak akan berharap untuk menangkap mereka.

  • Tentukan MyClientServerAppExceptionkelas dasar untuk mencakup kesalahan yang unik dalam konteks aplikasi Anda. Jangan pernah melempar instance dari kelas dasar. Respons kesalahan ambigu adalah HAL TERBURUK YANG PERNAH . Jika ada "kesalahan internal", maka jelaskan apa itu kesalahan itu.

  • Sebagian besar, hierarki di bawah kelas dasar harus luas, tidak dalam. Anda hanya perlu memperdalamnya dalam situasi di mana itu berguna bagi penelepon. Misalnya, jika ada 5 alasan pesan mungkin gagal dari klien ke server, Anda dapat menentukan ServerMessageFaultpengecualian, lalu tentukan kelas pengecualian untuk masing-masing dari 5 kesalahan di bawahnya. Dengan begitu, penelepon bisa menangkap superclass jika perlu atau mau. Cobalah untuk membatasi ini untuk kasus yang spesifik dan masuk akal.

  • Jangan mencoba mendefinisikan semua kelas pengecualian Anda sebelum benar-benar digunakan. Anda akan kembali melakukan sebagian besar. Ketika Anda menemukan kasus kesalahan saat menulis kode, lalu putuskan cara terbaik untuk menggambarkan kesalahan itu. Idealnya, itu harus dinyatakan dalam konteks apa yang penelepon coba lakukan.

  • Terkait dengan poin sebelumnya, ingatlah bahwa hanya karena Anda menggunakan pengecualian untuk merespons kesalahan, itu tidak berarti bahwa Anda harus menggunakan hanya pengecualian untuk status kesalahan. Ingatlah bahwa melempar pengecualian adalah mahal, dan biaya kinerja dapat bervariasi dari satu bahasa ke bahasa lain. Dengan beberapa bahasa, biaya lebih tinggi tergantung pada kedalaman tumpukan panggilan, jadi jika ada kesalahan yang mendalam di dalam tumpukan panggilan, periksa untuk melihat apakah Anda tidak dapat menggunakan tipe primitif sederhana (kode kesalahan integer atau bendera boolean) untuk mendorong kesalahan cadangan stack, sehingga dapat dilemparkan lebih dekat ke pemanggil pemanggil.

  • Jika Anda memasukkan logging sebagai bagian dari respons kesalahan, seharusnya mudah bagi penelepon untuk menambahkan informasi konteks ke objek pengecualian. Mulai dari mana informasi tersebut digunakan dalam kode logging. Putuskan berapa banyak informasi yang dibutuhkan agar log berguna (tanpa menjadi dinding teks raksasa). Kemudian bekerjalah mundur untuk memastikan kelas pengecualian dapat dengan mudah diberikan informasi itu.

Akhirnya, kecuali aplikasi Anda benar - benar dapat menangani kesalahan dengan kehabisan memori dengan anggun, jangan mencoba untuk mengatasinya, atau pengecualian runtime bencana lainnya. Biarkan saja OS menghadapinya, karena pada kenyataannya, hanya itu yang bisa Anda lakukan.

Mark Benningfield
sumber
2
Kecuali untuk peluru Anda yang kedua hingga terakhir (tentang biaya pengecualian), jawabannya bagus. Peluru tentang biaya pengecualian itu menyesatkan, karena dalam semua cara umum menerapkan pengecualian pada tingkat rendah, biaya melempar pengecualian benar-benar terlepas dari kedalaman tumpukan panggilan. Metode pelaporan kesalahan alternatif mungkin lebih baik jika Anda tahu bahwa kesalahan akan ditangani oleh pemanggil langsung, tetapi tidak mendapatkan beberapa fungsi dari tumpukan panggilan sebelum melemparkan pengecualian.
Bart van Ingen Schenau
@ BartvanIngenSchenau: Saya mencoba untuk tidak mengikat ini ke bahasa tertentu. Dalam beberapa bahasa ( Java, misalnya ), kedalaman tumpukan panggilan memengaruhi biaya instantiasi. Saya akan mengedit untuk mencerminkan bahwa itu tidak begitu dipotong dan dikeringkan.
Mark Benningfield
0

Yah saya akan merekomendasikan Anda pertama dan terutama membuat Exceptionkelas dasar untuk semua pengecualian diperiksa yang mungkin dilemparkan oleh aplikasi Anda. Jika aplikasi Anda dipanggil DuckType, buat DuckTypeExceptionkelas dasar.

Ini memungkinkan Anda menangkap pengecualian DuckTypeExceptionkelas dasar Anda untuk penanganan. Dari sini, pengecualian Anda harus bercabang dengan nama-nama deskriptif yang dapat lebih menyoroti jenis masalah. Misalnya, "DatabaseConnectionException".

Biar saya perjelas, ini semua harus diperiksa pengecualian untuk situasi yang bisa terjadi yang Anda mungkin ingin menangani dengan anggun dalam program Anda. Dengan kata lain, Anda tidak dapat terhubung ke database, jadi DatabaseConnectionExceptiondilemparkan yang kemudian dapat Anda tangkap untuk menunggu dan mencoba lagi setelah periode waktu tertentu.

Anda tidak akan melihat pengecualian yang dicentang untuk masalah yang sangat tidak terduga seperti kueri SQL yang tidak valid atau pengecualian pointer nol, dan saya akan mendorong Anda untuk membiarkan pengecualian ini melampaui sebagian besar klausa tangkapan (atau ditangkap dan dirangkai ulang jika diperlukan) sampai Anda tiba di main controller itu sendiri, yang kemudian dapat menangkap RuntimeExceptionmurni untuk keperluan logging.

Preferensi pribadi saya adalah untuk tidak menggunakan RuntimeExceptionkembali pengecualian yang tidak dicentang sebagai pengecualian, karena pada dasarnya pengecualian yang tidak dicentang, Anda tidak akan mengharapkannya dan oleh karena itu rethrow dengan pengecualian lain adalah menyembunyikan informasi. Namun, jika itu pilihan Anda, Anda masih bisa menangkap RuntimeExceptiondan melempar DuckTypeInternalExceptionyang tidak seperti DuckTypeExceptiondari RuntimeExceptiondan karenanya tidak dicentang.

Jika Anda mau, Anda dapat mensubkategorikan pengecualian Anda untuk tujuan organisasi seperti DatabaseExceptionuntuk apa pun yang berhubungan dengan basis data, tetapi saya masih mendorong sub-pengecualian tersebut untuk berasal dari pengecualian dasar Anda DuckTypeExceptiondan untuk menjadi abstrak dan karena itu diturunkan dengan nama deskriptif eksplisit.

Sebagai aturan umum, hasil tangkapan Anda harus semakin generik saat Anda menaikkan callstack of penelepon untuk menangani pengecualian, dan sekali lagi, di pengontrol utama Anda, Anda tidak akan menangani sedikit DatabaseConnectionExceptiontetapi lebih sederhana DuckTypeExceptiondari mana semua pengecualian Anda diperiksa berasal.

Neil
sumber
2
Perhatikan bahwa pertanyaan ini ditandai "C ++".
Martin Ba
0

Coba sederhanakan itu.

Hal pertama yang akan membantu Anda untuk berpikir dalam strategi lain adalah: menangkap banyak pengecualian sangat mirip dengan penggunaan pengecualian yang diperiksa dari Jawa (maaf, saya bukan pengembang C ++). Ini tidak baik karena banyak alasan jadi saya selalu berusaha untuk tidak menggunakannya, dan strategi pengecualian hierarki Anda mengingat saya banyak hal.

Jadi, saya sarankan Anda strategi lain dan fleksibel: gunakan pengecualian yang tidak dicentang dan kesalahan kode.

Per contoh, lihat kode Java ini:

public class SystemErrorCode implements ErrorCode {

    INVALID_NAME(101),
    ORDER_NOT_FOUND(102),
    PARAMETER_NOT_FOUND(103),
    VALUE_TOO_SHORT(104);

    private final int number;

    private ErrorCode(int number) {
        this.number = number;
    }

    @Override
    public int getNumber() {
        return number;
    }
}

Dan pengecualian unik Anda :

public class SystemException extends RuntimeException {

    private ErrorCode errorCode;

    public SystemException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

}

Strategi ini saya temukan pada tautan ini dan Anda dapat menemukan implementasi Java di sini , di mana Anda dapat melihat lebih detail tentang itu, karena kode di atas disederhanakan.

Karena Anda perlu memisahkan pengecualian berbeda antara aplikasi "klien" dan "server lain", Anda dapat memiliki beberapa kelas kode kesalahan yang mengimplementasikan antarmuka ErrorCode.

Dherik
sumber
2
Perhatikan bahwa pertanyaan ini ditandai "C ++".
Sjoerd
Saya mengedit untuk mengadaptasi gagasan itu.
Dherik
0

Pengecualian adalah goto yang tidak dibatasi dan harus digunakan dengan hati-hati. Strategi terbaik bagi mereka adalah membatasi mereka. Fungsi panggilan harus menangani semua pengecualian yang dilemparkan oleh fungsi yang dipanggilnya atau program mati. Hanya fungsi panggilan yang memiliki konteks yang benar untuk menangani pengecualian. Memiliki fungsi lebih jauh menangani pohon panggilan mereka adalah goto tidak terbatas.

Pengecualian bukan kesalahan. Mereka terjadi ketika keadaan mencegah program dari menyelesaikan satu cabang kode dan menunjukkan cabang lain untuk diikuti.

Pengecualian harus dalam konteks fungsi yang dipanggil. Misalnya: fungsi yang memecahkan persamaan kuadrat . Itu harus menangani dua pengecualian: division_by_zero dan square_root_of_negative_number. Tapi pengecualian ini tidak masuk akal untuk fungsi yang memanggil pemecah persamaan kuadrat ini. Mereka terjadi karena metode yang digunakan untuk menyelesaikan persamaan dan hanya memikirkan kembali mereka mengekspos internal dan istirahat enkapsulasi. Sebaliknya, division_by_zero harus diposisikan ulang sebagai not_quadratic dan square_root_of_negative_number dan no_real_roots.

Tidak perlu hierarki pengecualian. Enum dari pengecualian (yang mengidentifikasi mereka) yang dilemparkan oleh suatu fungsi sudah cukup karena fungsi panggilan harus menanganinya. Mengizinkan mereka memproses pohon panggilan adalah goto di luar konteks (tidak dibatasi).

shawnhcorey
sumber