Có một câu châm ngôn đã có từ lâu trong ngành phát triển phần mềm:
80% nỗ lực code của chúng ta được sử dụng trong 20% thời gian.
80% đó là nỗ lực để check và xử lý các lỗi của chương trình. Trong rất nhiều ngôn ngữ lập trình, việc check lỗi, bug rất buồn chán. Nó khiến cho chương trình trở nên hỗn loạn. Tôi đã từng thấy cảnh một nhóm lập trình viên PHP phải túm tụm lại với nhau và debug trên giấy! Phát hiện và xử lý lỗi là một phần vô cùng quan trọng trong bất kì chương trình nào. Java đưa ra một phương pháp hiệu quả và rất lịch sự để làm việc với các lỗi: Exception Handling.
*Exception = exceptional conditions
Xử lý ngoại lệ (Exception Handling) cho phép các developer có thể phát hiện lỗi mà không phải viết các dòng code test đặc biệt để test các giá trị trả về. Nó còn có một công dụng tốt hơn: Những dòng code để xử lý lỗi sẽ được tách biệt với những dòng code gây ra lỗi đó làm cho code sáng sủa hơn. Như vậy chương trình của chúng ta có thể đưa ra những thông báo lịch sự hoặc xử lý trong im lặng và tiếp tục chạy bình thường. Sau đây tôi sẽ trình bày 3 lợi ích của việc xử dụng Exception Handling trong Java.
The Good Parts – Part 1: Tách bạch code lỗi và code gây lỗi
Giả sử tôi có đoạn code sẽ ghi file vào RAM như sau:
readFile {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
}
Thoạt nhìn chương trình này có vẻ ổn, nhưng nó đã bỏ qua vài phần quan trọng:
- Điều gì xảy ra nếu không mở được file?
- Điều gì xảy ra nếu không tính được dung dượng?
- Điều gì xảy ra nếu không có đủ RAM?
- Điều gì xảy ra nếu không đọc được file?
- Điều gì xảy ra nếu không đóng được file?
Để xử lý những điều trên, hàm này cần có nhiều code hơn:
errorCodeType readFile {
initialize errorCode = 0;
open the file;
if (theFileIsOpen) {
determine the length of the file;
if (gotTheFileLength) {
allocate that much memory;
if (gotEnoughMemory) {
read the file into memory;
if (readFailed) {
errorCode = -1;
}
} else {
errorCode = -2;
}
} else {
errorCode = -3;
}
close the file;
if (theFileDidntClose && errorCode == 0) {
errorCode = -4;
} else {
errorCode = errorCode and -4;
}
} else {
errorCode = -5;
}
return errorCode;
}
Chúng ta dễ dàng nhận thấy có quá nhiều if, else, trả về, ở đoạn code trên. Nói chung là rất khó đọc. Chúng ta có thể sử dụng Exception Handling để giúp đoạn code trên bớt “nguy hiểm” như sau:
readFile {
try {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
} catch (fileOpenFailed) {
doSomething;
} catch (sizeDeterminationFailed) {
doSomething;
} catch (memoryAllocationFailed) {
doSomething;
} catch (readFailed) {
doSomething;
} catch (fileCloseFailed) {
doSomething;
}
}
Dễ thấy là dùng exception handling thì cũng chẳng giúp chúng ta đỡ được mấy việc. Chúng ta vẫn phải check, phát hiện và xử lý lỗi. Nhưng nó giúp code trông “sáng sủa” hơn rất nhiều.
The Good Parts – Part 2: Truyền lỗi ngược lại call stack
Giả sử tôi có 3 hàm mà hàm 1 gọi hàm 2, hàm 2 gọi hàm 3 như sau:
method1 {
call method2;
}
method2 {
call method3;
}
method3 {
call readFile;
}
Giả sử trong chương trình tôi chỉ gọi đến hàm 1. Đó là hàm duy nhất tôi cần quan tâm. Nhưng hàm 3 lại có thể gây ra lỗi. Tôi có thể xử lý như sau:
method1 {
errorCodeType error;
error = call method2;
if (error)
doErrorProcessing;
else
proceed;
}
errorCodeType method2 {
errorCodeType error;
error = call method3;
if (error)
return error;
else
proceed;
}
errorCodeType method3 {
errorCodeType error;
error = call readFile;
if (error)
return error;
else
proceed;
}
Vâng và tôi chỉ cần quan tâm đến hàm 1, nhưng tôi lại phải đi code cho hàm 2 (hàm không liên quan) và hàm cuối. Lưu ý rằng JVM sẽ tìm ngược trở lại call stack và xem hàm nào sẽ xử lý cái exception mà một hàm gọi nó đã ném ra. Như vậy tôi có thể tôi có thể xử lý bằng cách khác:
method1 {
try {
call method2;
} catch (exception e) {
doErrorProcessing;
}
}
method2 throws exception {
call method3;
}
method3 throws exception {
call readFile;
}
Như vậy, chỉ có hàm 1 nhận trách nhiệm bắt và xử lý lỗi. Đơn giản hơn rất nhiều.
The Good Parts – Part 3: Nhóm và phân biệt các lỗi khác nhau
Do tất cả các Exception trong java đều là Object, việc nhóm các lỗi lại là điều dễ hiểu sau khi phân chia các class của chúng. Ví dụ FileNotFoundException thông báo chương trình không tìm thấy file trên đĩa cứng. Một method có thể bắt lấy nó:
catch (FileNotFoundException e) {
...
}
Do IOException là class cha của FileNotFoundException, chúng ta có thể làm như sau để bắt lấy tất cả các lỗi Input/Output:
catch (IOException e) {
...
}
Trong phần lớn các trường hợp, các lập trình viên sẽ muốn catch một lỗi cụ thể hơn là chỉ ra một lỗi chung chung. Nhưng dù sao, chúng ta có thể catch một lỗi chung, tùy từng tình huống. Đó là một điều tốt.
Bài viết sử dụng tư liệu từ docs.oracle.com và cuốn sách Introduction to Java Programming 10th Edition. Mọi ý kiến đóng góp xin để lại tại phần comment.