Pengecualian: Mengapa melempar lebih awal? Kenapa terlambat?

156

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?

shylynx
sumber
104
Gagasan di balik "menangkap terlambat" adalah, daripada menangkap selambat mungkin, untuk menangkap sedini mungkin. Jika Anda memiliki file parser, misalnya, tidak ada gunanya menangani file yang tidak ditemukan. Apa yang harus Anda lakukan dengan itu, apa jalan pemulihan Anda? Tidak ada, jadi jangan tangkap. Pergi ke lexer Anda, apa yang Anda lakukan di sana, bagaimana Anda pulih dari ini sehingga program Anda dapat melanjutkan? Tidak bisa, biarkan pengecualian berlalu. Bagaimana pemindai Anda menangani ini? Tidak bisa, biarkan berlalu. Bagaimana kode panggilan menangani ini? Itu bisa mencoba filepath lain atau mengingatkan pengguna, jadi tangkap.
Phoshi
16
Ada sangat sedikit kasus di mana NullPointerException (saya berasumsi itulah yang dimaksud NPE) harus ditangkap; jika mungkin, itu harus dihindari sejak awal. Jika Anda memiliki NullPointerExceptions, maka Anda memiliki beberapa kode rusak yang perlu diperbaiki. Kemungkinan besar perbaikan yang mudah juga.
Phil
6
Tolong, teman-teman, sebelum menyarankan untuk menutup pertanyaan ini sebagai duplikat, periksa bahwa jawaban untuk pertanyaan lain tidak menjawab pertanyaan ini dengan sangat baik.
Doc Brown
1
(Citebot) today.java.net/article/2003/11/20/... Jika ini bukan asal dari kutipan, berikan referensi ke sumber yang menurut Anda adalah kutipan asli yang paling mungkin.
rwong
1
Hanya pengingat bagi siapa pun yang mencapai pertanyaan ini dan melakukan pengembangan Android. Di Android, pengecualian harus ditangkap dan ditangani secara lokal - dalam fungsi yang sama dengan yang pertama kali ditangkap. Ini karena pengecualian tidak menyebar di seluruh penangan pesan - aplikasi Anda akan dimatikan jika itu terjadi. Jadi, Anda sebaiknya tidak mengutip saran ini ketika melakukan pengembangan Android.
rwong

Jawaban:

118

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.

Michael Shaw
sumber
Ya, saya sudah tahu dan keluar dari pertanyaan, bahwa saya harus melempar pengecualian pada titik di mana kesalahan berasal. Tapi mengapa saya tidak menangkap NPE dan membiarkannya naik ke atas Stacktrace? Saya selalu akan menangkap NPE dan membungkusnya menjadi pengecualian yang berarti. Saya juga tidak dapat melihat keuntungan mengapa saya harus melempar DAO-Exception ke lapisan layanan atau ui. Saya selalu akan menangkapnya di lapisan layanan dan membungkusnya menjadi pengecualian layanan dengan informasi rinci tambahan, mengapa panggilan layanan gagal.
shylynx
8
@shylynx Menangkap pengecualian dan kemudian menyusun kembali pengecualian yang lebih bermakna adalah hal yang baik untuk dilakukan. Apa yang seharusnya tidak Anda lakukan adalah menangkap pengecualian terlalu dini dan kemudian tidak memikirkannya kembali. Kesalahan yang dikatakan pepatah adalah menangkap pengecualian terlalu dini, dan kemudian berusaha menanganinya di lapisan yang salah dalam kode.
Simon B
Membuat konteks menjadi jelas pada saat menerima pengecualian membuat hidup lebih mudah bagi para pengembang di tim Anda. NPE membutuhkan penyelidikan lebih lanjut untuk memahami masalahnya
Michael Shaw
4
@shylynx Seseorang mungkin mengajukan pertanyaan, "Mengapa Anda memiliki poin dalam kode Anda yang dapat melempar NullPointerException? Mengapa tidak memeriksa nulldan melempar pengecualian (mungkin sebuah IllegalArgumentException) lebih cepat sehingga penelepon tahu persis di mana yang buruk nulldilewatkan?" Saya percaya itu akan menjadi apa yang disarankan oleh bagian "lempar lebih awal" dari perkataan itu.
jpmc26
2
@ jpmc Saya mengambil NPE hanya sebagai contoh untuk menekankan kekhawatiran di sekitar lapisan dan pengecualian. Saya bisa menggantinya dengan IllegalArgumentException juga.
shylynx
56

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.

Doval
sumber
1
1 untuk menjelaskan dengan jelas mengapa perbedaan level penting. Contoh yang bagus tentang kesalahan sistem file.
Juan Carlos Coto
24

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:

static int divide(int dividend, int divisor) throws DivideByZeroException {
    if (divisor == 0)
        throw new DivideByZeroException(); // that's a checked exception indeed

    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    try {
        int res = divide(a, b);
        System.out.println(res);
    } catch (DivideByZeroException e) {
        // checked exception... I'm forced to handle it!
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

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:

static int divide(int dividend, int divisor) {
    // throws unchecked ArithmeticException for 0 divisor
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt();
    if (b != 0) {
        int res = divide(a, b);
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

Atau, Anda dapat menggunakan komando penuh pada gaya OOP seperti ini:

static class Division {
    final int dividend;
    final int divisor;

    private Division(int dividend, int divisor) {
        this.dividend = dividend;
        this.divisor = divisor;
    }

    public boolean check() {
        return divisor != 0;
    }

    public int eval() {
        return dividend / divisor;
    }

    public static Division with(int dividend, int divisor) {
        return new Division(dividend, divisor);
    }
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Division d = Division.with(a, b);
    if (d.check()) {
        int res = d.eval();
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

Seperti yang Anda lihat, kode penelepon memiliki beban pemeriksaan awal, tetapi tidak melakukan penanganan pengecualian setelahnya. Jika ArithmeticExceptionada yang datang dari menelepon divideatau eval, maka ANDA yang harus melakukan pengecualian menangani dan memperbaiki kode Anda, karena Anda lupa check(). Untuk alasan yang sama menangkap a NullPointerExceptionhampir 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 kembali null untuk kasus kesalahan sederhana seperti ini:

static Integer divide(int dividend, int divisor) {
    if (divisor == 0) return null;
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Integer res = divide(a, b);
    if (res != null) {
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

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!

static interface InputResource {
    String fetchData();
}

static interface OutputResource {
    void writeData(String data);
}

static void doMyBusiness(InputResource in, OutputResource out, int times) {
    for (int i = 0; i < times; i++) {
        System.out.println("fetching data");
        String data = in.fetchData();
        System.out.println("outputting data");
        out.writeData(data);
    }
}

Sementara itu di tempat lain implementasi konkret ...

static class ConstantInputResource implements InputResource {
    @Override
    public String fetchData() {
        return "Hello World!";
    }
}

static class FailingInputResourceException extends RuntimeException {
    public FailingInputResourceException(String message) {
        super(message);
    }
}

static class FailingInputResource implements InputResource {
    @Override
    public String fetchData() {
        throw new FailingInputResourceException("I am a complete failure!");
    }
}

static class StandardOutputResource implements OutputResource {
    @Override
    public void writeData(String data) {
        System.out.println("DATA: " + data);
    }
}

Dan akhirnya kode pengkabelan ... Siapa yang menangani pengecualian sumber daya konkret? Orang yang tahu tentang mereka!

static void start() {
    InputResource in1 = new FailingInputResource();
    InputResource in2 = new ConstantInputResource();
    OutputResource out = new StandardOutputResource();

    try {
        ReusableBusinessLogicClass.doMyBusiness(in1, out, 3);
    }
    catch (FailingInputResourceException e)
    {
        System.out.println(e.getMessage());
        System.out.println("retrying...");
        ReusableBusinessLogicClass.doMyBusiness(in2, out, 3);
    }
}

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!

Daniel Dinnyes
sumber
Anda benar bahwa kode yang memasok antarmuka akan tahu lebih banyak tentang apa yang salah dari kode yang menggunakan antarmuka, tetapi anggap suatu metode menggunakan dua sumber daya dari jenis antarmuka yang sama dan kegagalan perlu ditangani secara berbeda? Atau jika salah satu sumber daya tersebut secara internal, sebagai detail implementasi yang tidak diketahui oleh penciptanya, menggunakan sumber daya bersarang lainnya dari jenis yang sama? Memiliki lapisan bisnis lempar WrappedFirstResourceExceptionatau WrappedSecondResourceExceptiondan membutuhkan lapisan "kabel" untuk melihat ke dalam pengecualian itu untuk melihat akar penyebab masalah ...
supercat
... mungkin menjengkelkan, tetapi akan tampak lebih baik daripada mengasumsikan bahwa FailingInputResourcepengecualian apa pun akan merupakan hasil dari operasi dengan in1. Sebenarnya, saya pikir dalam banyak kasus pendekatan yang tepat adalah memiliki lapisan kabel melewati objek penanganan pengecualian, dan meminta lapisan bisnis menyertakan catchyang kemudian memanggil handleExceptionmetode 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.
supercat
@supercat Saya mengerti apa yang Anda katakan. Saya akan mengatakan implementasi sumber daya konkret bertanggung jawab untuk mengetahui pengecualian yang dapat dilontarkan. Itu tidak harus menentukan semuanya (ada yang disebut perilaku tidak terdefinisi ), tetapi harus memastikan tidak ada ambiguitas. Juga, pengecualian run-time yang tidak dicentang harus didokumentasikan. Jika itu bertentangan dengan dokumentasi, maka itu adalah bug. Jika kode penelepon diharapkan melakukan sesuatu yang masuk akal tentang pengecualian, minimum adalah bahwa sumber daya membungkus mereka dalam beberapa UnrecoverableInternalException, mirip dengan kode kesalahan HTTP 500.
Daniel Dinnyes
@supercat Tentang saran Anda tentang penangan kesalahan yang dapat dikonfigurasi: persis! Dalam contoh terakhir saya logika penanganan kesalahan adalah kode keras, memanggil doMyBusinessmetode statis . Ini demi singkatnya, dan sangat mungkin untuk membuatnya lebih dinamis. HandlerKelas semacam itu akan dipakai dengan beberapa sumber input / output, dan memiliki handlemetode yang menerima kelas yang mengimplementasikan a ReusableBusinessLogicInterface. Anda kemudian dapat menggabungkan / mengkonfigurasi untuk menggunakan implementasi penangan, sumber daya, dan logika bisnis yang berbeda di lapisan kabel di suatu tempat di atasnya.
Daniel Dinnyes
10

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:

double MethodA()
{
    return PropertyA - PropertyB.NestedProperty;
}

Kode ini jelas dapat membuang pengecualian referensi nol jika PropertyBnol. Ada dua hal yang bisa kita lakukan dalam kasus ini untuk "memperbaiki" situasi ini. Kita bisa:

  • Secara otomatis membuat PropertyB jika kita tidak memilikinya; atau
  • Biarkan pengecualian muncul hingga metode pemanggilan.

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.

Stephen
sumber
Saya pikir Anda menggunakan pengembang hulu dalam arti yang salah. Juga, Anda mengatakan itu melanggar prinsip tanggung jawab tunggal, tetapi pada kenyataannya banyak inisialisasi permintaan dan caching nilai diterapkan seperti itu (dengan kontrol konkurensi yang tepat di tempat saja)
Daniel Dinnyes
Dalam contoh yang Anda berikan, bagaimana dengan memeriksa nol sebelum operasi pengurangan, seperti iniif(PropertyB == null) return 0;
user1451111
1
Dapatkah Anda juga menguraikan paragraf terakhir Anda, khususnya apa yang Anda maksud dengan ' lapisan abstraksi '.
user1451111
Jika kita melakukan beberapa pekerjaan IO, lapisan abstraksi untuk menangkap Pengecualian IO adalah tempat kita melakukan pekerjaan itu. Pada saat itu kami memiliki semua informasi yang perlu kami putuskan jika kami ingin mencoba lagi atau melempar kotak pesan ke pengguna atau membuat objek menggunakan seperangkat default.
Stephen
"Dalam contoh yang Anda berikan, bagaimana dengan memeriksa nol sebelum operasi pengurangan, seperti ini jika (PropertyB == null) menghasilkan 0;" Yuck. Itu akan mengatakan metode panggilan bahwa saya memiliki hal-hal yang valid untuk dikurangkan dari dan ke. Tentu saja ini kontekstual tetapi akan menjadi praktik yang buruk untuk melakukan kesalahan saya memeriksa di sini dalam banyak kasus.
Stephen
6

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 .

ratchet freak
sumber
1
Anda menulis: Lempar segera, ... Tangkap segera ...! Mengapa? Itu pendekatan yang sangat bertentangan dengan "melempar lebih awal, terlambat."
shylynx
1
@shylynx Saya tidak tahu dari mana "melempar lebih awal, terlambat" berasal, tetapi nilainya dipertanyakan. Apa, tepatnya, yang dimaksud dengan menangkap kata "terlambat"? Masuk akal untuk menangkap pengecualian (jika ada) tergantung pada masalahnya. Satu-satunya hal yang jelas adalah bahwa Anda ingin mendeteksi masalah (dan melempar) sedini mungkin.
Doval
2
Saya berasumsi "catch late" dimaksudkan untuk membandingkan praktik penangkapan sebelum Anda dapat mengetahui apa yang harus dilakukan untuk memperbaiki kesalahan - mis. Kadang-kadang Anda melihat fungsi yang menangkap semuanya hanya agar mereka dapat mencetak pesan kesalahan dan kemudian menyusun kembali pengecualian.
@Hurkyl: Masalah dengan "keterlambatan" adalah bahwa jika pengecualian muncul melalui lapisan yang tidak tahu apa-apa tentang itu, bisa jadi sulit untuk kode yang mungkin berada dalam posisi untuk melakukan sesuatu tentang situasi untuk mengetahui bahwa segala sesuatunya benar-benar sama. diharapkan. Sebagai contoh sederhana, misalkan jika parser untuk file dokumen pengguna perlu memuat CODEC dari disk dan kesalahan disk terjadi saat membaca itu, kode yang memanggil parser dapat bertindak tidak tepat jika berpikir ada kesalahan disk saat membaca pengguna dokumen.
supercat
4

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.

soru
sumber
2

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/ catchkode 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).

Aschratt
sumber
1

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.

davidk01
sumber