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 ArithmeticException
ada yang datang dari menelepon divide
atau eval
, maka ANDA yang harus melakukan pengecualian menangani dan memperbaiki kode Anda, karena Anda lupa check()
. Untuk alasan yang sama menangkap a NullPointerException
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 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!