Những người khác đã tóm tắt khá tốt tại sao để ném sớm . Thay vào đó, hãy để tôi tập trung vào lý do để bắt phần muộn , mà tôi chưa thấy một lời giải thích thỏa mãn cho sở thích của mình.
VÌ SAO NGOẠI TRỪ?
Dường như có một sự nhầm lẫn xung quanh lý do tại sao các trường hợp ngoại lệ tồn tại ở nơi đầu tiên. Hãy để tôi chia sẻ bí mật lớn ở đây: lý do cho các trường hợp ngoại lệ và xử lý ngoại lệ là ... TÓM TẮT .
Bạn đã thấy mã như thế này:
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.");
}
}
Đó không phải là cách ngoại lệ nên được sử dụng. Mã như trên tồn tại trong cuộc sống thực, nhưng chúng là một quang sai, và thực sự là ngoại lệ (chơi chữ). Định nghĩa của phép chia chẳng hạn, ngay cả trong toán học thuần túy, là có điều kiện: luôn luôn là "mã người gọi", người phải xử lý trường hợp đặc biệt bằng 0 để hạn chế miền đầu vào. Nó thật là xấu xí. Nó luôn luôn đau cho người gọi. Tuy nhiên, đối với những tình huống như vậy, mô hình kiểm tra sau đó là cách tự nhiên:
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.");
}
}
Ngoài ra, bạn có thể thực hiện lệnh đầy đủ theo kiểu OOP như thế này:
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.");
}
}
Như bạn thấy, mã người gọi có trách nhiệm kiểm tra trước, nhưng không thực hiện bất kỳ xử lý ngoại lệ nào sau đó. Nếu một ArithmeticException
cuộc gọi đến từ cuộc gọi divide
hoặc eval
, thì chính BẠN phải xử lý ngoại lệ và sửa mã của bạn, vì bạn đã quên check()
. Vì những lý do tương tự, việc bắt a NullPointerException
hầu như luôn luôn là điều sai.
Bây giờ có một số người nói rằng họ muốn xem các trường hợp ngoại lệ trong chữ ký phương thức / hàm, nghĩa là mở rộng rõ ràng miền đầu ra . Họ là những người ủng hộ kiểm tra ngoại lệ . Tất nhiên, việc thay đổi miền đầu ra sẽ buộc bất kỳ mã người gọi trực tiếp nào phải thích ứng và điều đó thực sự sẽ đạt được với các ngoại lệ được kiểm tra. Nhưng bạn không cần ngoại lệ cho điều đó! Đó là lý do tại sao bạn có Nullable<T>
các lớp chung , lớp trường hợp , kiểu dữ liệu đại số và kiểu kết hợp . Một số người OO thậm chí có thể thích quay lại null
cho các trường hợp lỗi đơn giản như thế này:
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.");
}
}
Các ngoại lệ về mặt kỹ thuật có thể được sử dụng cho mục đích như trên, nhưng đây là điểm: ngoại lệ không tồn tại cho việc sử dụng đó . Ngoại lệ là sự trừu tượng pro. Ngoại lệ là về sự gián tiếp. Các ngoại lệ cho phép mở rộng miền "kết quả" mà không phá vỡ hợp đồng khách hàng trực tiếp và trì hoãn việc xử lý lỗi thành "nơi khác". Nếu mã của bạn đưa ra các ngoại lệ được xử lý trong các cuộc gọi trực tiếp của cùng một mã, mà không có bất kỳ lớp trừu tượng nào ở giữa, thì bạn đang thực hiện nó SAU
LÀM THẾ NÀO ĐỂ KIẾM ĐƯỢC
Vì thế chúng ta ở đây. Tôi đã lập luận theo cách của mình để chỉ ra rằng sử dụng các ngoại lệ trong các tình huống trên không phải là cách sử dụng ngoại lệ. Có tồn tại một trường hợp sử dụng chính hãng, trong đó sự trừu tượng và thiếu quyết đoán được cung cấp bởi xử lý ngoại lệ là không thể thiếu. Hiểu cách sử dụng như vậy sẽ giúp hiểu được đề nghị bắt muộn quá.
Trường hợp sử dụng đó là: Lập trình chống lại sự trừu tượng tài nguyên ...
Vâng, logic kinh doanh nên được lập trình chống lại sự trừu tượng , không phải là triển khai cụ thể. Mã "dây" IOC cấp cao nhất sẽ khởi tạo các triển khai cụ thể của các tóm tắt tài nguyên và chuyển chúng xuống logic nghiệp vụ. Không có gì mới ở đây. Nhưng việc triển khai cụ thể của những trừu tượng tài nguyên đó có thể có khả năng đưa ra các ngoại lệ cụ thể về triển khai của riêng họ , phải không?
Vì vậy, ai có thể xử lý những ngoại lệ cụ thể thực hiện? Có thể xử lý bất kỳ ngoại lệ cụ thể tài nguyên nào trong logic kinh doanh không? Không, không phải. Logic nghiệp vụ được lập trình chống lại sự trừu tượng, loại trừ kiến thức về những chi tiết ngoại lệ cụ thể đó.
"Aha!", Bạn có thể nói: "nhưng đó là lý do tại sao chúng ta có thể phân lớp ngoại lệ và tạo cấu trúc phân cấp ngoại lệ" (xem ông Spring !). Hãy để tôi nói với bạn, đó là một ngụy biện. Thứ nhất, mọi cuốn sách hợp lý trên OOP đều nói rằng thừa kế cụ thể là xấu, nhưng bằng cách nào đó, thành phần cốt lõi này của JVM, xử lý ngoại lệ, được liên kết chặt chẽ với kế thừa cụ thể. Trớ trêu thay, Joshua Bloch không thể viết cuốn sách Java hiệu quả của mình trước khi anh ta có thể có được trải nghiệm với một JVM đang hoạt động, phải không? Nó là một cuốn sách "bài học kinh nghiệm" cho thế hệ tiếp theo. Thứ hai, và quan trọng hơn, nếu bạn bắt gặp một ngoại lệ cấp cao thì bạn sẽ xử lý nó như thế nào?PatientNeedsImmediateAttentionException
: chúng ta có phải cho cô ấy uống thuốc hay cắt cụt chân không!? Làm thế nào về một câu lệnh chuyển đổi trên tất cả các lớp con có thể? Có sự đa hình của bạn, có sự trừu tượng. Bạn đã nhận điểm.
Vì vậy, ai có thể xử lý các ngoại lệ cụ thể tài nguyên? Nó phải là người biết cụ thể! Người đã khởi tạo tài nguyên! Mã "dây" tất nhiên! Kiểm tra này:
Logic kinh doanh được mã hóa chống lại sự trừu tượng ... KHÔNG CÓ TÀI LIỆU XÁC NHẬN XÁC NHẬN!
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);
}
}
Trong khi đó ở một nơi khác, việc triển khai cụ thể ...
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);
}
}
Và cuối cùng là mã dây ... Ai xử lý các ngoại lệ tài nguyên cụ thể? Người biết về họ!
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);
}
}
Bây giờ chịu đựng với tôi. Các mã trên là đơn giản. Bạn có thể nói rằng bạn có một ứng dụng / bộ chứa web dành cho doanh nghiệp với nhiều phạm vi tài nguyên được quản lý của bộ chứa IOC và bạn cần thử lại tự động và sắp xếp lại các tài nguyên phạm vi phiên hoặc yêu cầu, v.v. tạo tài nguyên, do đó không nhận thức được việc thực hiện chính xác. Chỉ phạm vi cấp cao hơn mới thực sự biết những ngoại lệ nào mà các tài nguyên cấp thấp hơn có thể ném. Bây giờ giữ lại!
Thật không may, các trường hợp ngoại lệ chỉ cho phép chuyển hướng trong ngăn xếp cuộc gọi và các phạm vi khác nhau với các số lượng khác nhau của chúng thường chạy trên nhiều luồng khác nhau. Không có cách nào để giao tiếp thông qua đó với ngoại lệ. Chúng tôi cần một cái gì đó mạnh mẽ hơn ở đây. Trả lời: tin nhắn async đi qua . Bắt mọi ngoại lệ ở gốc của phạm vi cấp thấp hơn. Đừng bỏ qua bất cứ điều gì, đừng để bất cứ điều gì trôi qua. Điều này sẽ đóng và loại bỏ tất cả các tài nguyên được tạo trên ngăn xếp cuộc gọi của phạm vi hiện tại. Sau đó tuyên truyền các thông báo lỗi đến phạm vi cao hơn bằng cách sử dụng hàng đợi / kênh thông báo trong quy trình xử lý ngoại lệ, cho đến khi bạn đạt đến mức độ cụ thể hóa được biết. Đó là người biết cách xử lý nó.
TỔNG HỢP SUMMA
Vì vậy, theo cách giải thích của tôi, bắt muộn có nghĩa là bắt ngoại lệ ở nơi thuận tiện nhất Ở ĐÂU BẠN KHÔNG NÓI TÓM TẮT NÀO . Đừng bắt quá sớm! Nắm bắt các ngoại lệ ở lớp nơi bạn tạo các trường hợp ném ngoại lệ cụ thể của các trừu tượng tài nguyên, lớp biết các đặc tính của trừu tượng hóa. Lớp "nối dây".
HTH. Chúc mừng mã hóa!