Ngoại lệ: Tại sao ném sớm? Tại sao bắt muộn?


156

Có nhiều thực tiễn tốt nhất nổi tiếng về xử lý ngoại lệ trong sự cô lập. Tôi biết đủ "làm và không nên", nhưng mọi thứ trở nên phức tạp khi nói đến các thực tiễn hoặc mô hình tốt nhất trong môi trường lớn hơn. "Ném sớm, bắt muộn" - Tôi đã nghe nhiều lần và nó vẫn làm tôi bối rối.

Tại sao tôi nên ném sớm và bắt muộn, nếu ở tầng thấp, một ngoại lệ con trỏ null được ném? Tại sao tôi nên bắt nó ở lớp cao hơn? Tôi không có ý nghĩa gì khi bắt ngoại lệ cấp thấp ở cấp cao hơn, chẳng hạn như tầng doanh nghiệp. Nó dường như vi phạm các mối quan tâm của từng lớp.

Hãy tưởng tượng tình huống sau:

Tôi có một dịch vụ tính toán một con số. Để tính toán hình, dịch vụ truy cập vào kho lưu trữ để lấy dữ liệu thô và một số dịch vụ khác để chuẩn bị tính toán. Nếu có lỗi xảy ra ở lớp truy xuất dữ liệu, tại sao tôi nên ném DataRetr cổException lên cấp cao hơn? Ngược lại, tôi thích bọc ngoại lệ thành một ngoại lệ có ý nghĩa, ví dụ như tính toánServiceException.

Tại sao ném sớm, tại sao bắt muộn?


104
Ý tưởng đằng sau "bắt muộn" là, thay vì bắt càng muộn càng tốt, để bắt càng sớm càng hữu ích. Nếu bạn có một trình phân tích cú pháp tệp, chẳng hạn, không có điểm nào xử lý một tệp không tìm thấy. Bạn có ý định làm gì với điều đó, con đường phục hồi của bạn là gì? Không có ai, vì vậy đừng bắt. Chuyển đến lexer của bạn, bạn làm gì ở đó, làm thế nào để bạn phục hồi từ điều này để chương trình của bạn có thể tiếp tục? Không thể, hãy để ngoại lệ vượt qua. Làm thế nào máy quét của bạn có thể xử lý này? Nó không thể, hãy để nó qua đi. Làm thế nào các mã gọi có thể xử lý này? Nó có thể thử filepath khác hoặc cảnh báo người dùng, vì vậy hãy nắm bắt.
Phoshi

16
Có rất ít trường hợp NullPulumException (tôi cho rằng đó là ý nghĩa của NPE); nếu có thể, nên tránh ở nơi đầu tiên. Nếu bạn đang có NullPulumExceptions, thì bạn có một số mã bị hỏng cần được sửa. Đây rất có thể là một sửa chữa dễ dàng.
Phil

6
Xin vui lòng, trước khi đề nghị đóng câu hỏi này thành một bản sao, hãy kiểm tra xem câu trả lời cho câu hỏi khác không trả lời tốt câu hỏi này.
Doc Brown

1
(Citebot) today.java.net/article/2003/11/20/... Nếu đây không phải là nguồn gốc của các báo giá, vui lòng cung cấp một tham chiếu đến các nguồn mà bạn nghĩ là quote gốc nhất có khả năng.
rwong

1
Chỉ là một lời nhắc nhở cho bất cứ ai tiếp cận câu hỏi này và thực hiện phát triển Android. Trên Android, các ngoại lệ phải được bắt và xử lý cục bộ - trong cùng chức năng nơi lần đầu tiên được bắt. Điều này là do các trường hợp ngoại lệ không lan truyền trên các trình xử lý tin nhắn - ứng dụng của bạn sẽ bị hủy nếu xảy ra. Vì vậy, bạn không nên trích dẫn lời khuyên này khi thực hiện phát triển Android.
rwong

Câu trả lời:


118

Theo kinh nghiệm của tôi, tốt nhất là đưa ra các ngoại lệ tại điểm xảy ra lỗi. Bạn làm điều này bởi vì đó là điểm mà bạn biết nhiều nhất về lý do tại sao ngoại lệ được kích hoạt.

Khi ngoại lệ thư giãn lại các lớp, bắt và suy nghĩ lại là một cách tốt để thêm bối cảnh bổ sung vào ngoại lệ. Điều này có thể có nghĩa là ném một loại ngoại lệ khác, nhưng bao gồm ngoại lệ ban đầu khi bạn làm điều này.

Cuối cùng, ngoại lệ sẽ đến một lớp nơi bạn có thể đưa ra quyết định về luồng mã (ví dụ: nhắc nhở người dùng hành động). Đây là điểm cuối cùng bạn nên xử lý ngoại lệ và tiếp tục thực hiện bình thường.

Với thực tiễn và kinh nghiệm với cơ sở mã của bạn, việc đánh giá bối cảnh bổ sung vào lỗi sẽ trở nên khá dễ dàng và khi thực sự hợp lý nhất, cuối cùng sẽ xử lý các lỗi.

Bắt → nghĩ lại

Làm điều này khi bạn có thể bổ sung thêm thông tin một cách hữu ích để tiết kiệm cho nhà phát triển phải làm việc thông qua tất cả các lớp để hiểu vấn đề.

Bắt → Xử lý

Làm điều này khi bạn có thể đưa ra quyết định cuối cùng về luồng thực thi phù hợp, nhưng khác nhau thông qua phần mềm.

Bắt → Trả về Lỗi

Trong khi có những tình huống phù hợp, việc xem xét các ngoại lệ và trả về giá trị lỗi cho người gọi nên được xem xét để tái cấu trúc thành một triển khai Catch → Rethrow.


Vâng, tôi đã biết và không có vấn đề gì, rằng tôi nên đưa ra ngoại lệ tại điểm bắt nguồn lỗi. Nhưng tại sao tôi không nên bắt NPE và thay vào đó hãy để nó leo lên Stacktrace? Tôi luôn luôn bắt NPE và gói nó thành một ngoại lệ có ý nghĩa. Tôi cũng không thể thấy bất kỳ lợi thế nào tại sao tôi nên ném DAO-Exception lên dịch vụ hoặc lớp ui. Tôi luôn luôn bắt nó ở lớp dịch vụ và bọc nó thành một ngoại lệ dịch vụ với thông tin chi tiết bổ sung, tại sao gọi dịch vụ không thành công.
shylynx

8
@shylynx Nắm bắt một ngoại lệ và sau đó suy nghĩ lại một ngoại lệ có ý nghĩa hơn là một việc nên làm. Những gì bạn không nên làm là bắt một ngoại lệ quá sớm và sau đó không nghĩ lại. Lỗi mà câu nói đang cảnh báo là bắt ngoại lệ quá sớm, và sau đó cố gắng xử lý nó ở lớp sai trong mã.
Simon B

Làm cho bối cảnh rõ ràng tại điểm nhận được ngoại lệ giúp cuộc sống dễ dàng hơn cho các nhà phát triển trong nhóm của bạn. Một NPE yêu cầu điều tra thêm để hiểu vấn đề
Michael Shaw

4
@shylynx Người ta có thể đặt câu hỏi: "Tại sao bạn có một điểm trong mã của mình có thể ném NullPointerException? Tại sao không kiểm tra nullvà ném ngoại lệ (có thể là một IllegalArgumentException) sớm hơn để người gọi biết chính xác nơi xấu nullđã đi qua?" Tôi tin rằng đó sẽ là những gì phần "ném sớm" của câu nói sẽ gợi ý.
jpmc26

2
@jpmc Tôi chỉ lấy NPE làm ví dụ để nhấn mạnh những lo ngại xung quanh các lớp và ngoại lệ. Tôi cũng có thể thay thế nó bằng IllegalArgumentException.
shylynx

56

Bạn muốn ném một ngoại lệ càng sớm càng tốt bởi vì điều đó giúp tìm ra nguyên nhân dễ dàng hơn. Ví dụ, hãy xem xét một phương pháp có thể thất bại với các đối số nhất định. Nếu bạn xác thực các đối số và thất bại ngay từ đầu phương thức, bạn sẽ biết ngay lỗi xảy ra trong mã gọi. Nếu bạn đợi cho đến khi các đối số là cần thiết trước khi thất bại, bạn phải theo dõi quá trình thực thi và tìm hiểu xem lỗi có nằm trong mã gọi (đối số xấu) hay phương thức có lỗi không. Bạn ném ngoại lệ càng sớm thì càng gần với nguyên nhân cơ bản của nó và càng dễ dàng tìm ra nơi mà mọi thứ đã sai.

Lý do các trường hợp ngoại lệ được xử lý ở cấp cao hơn là vì các cấp thấp hơn không biết cách xử lý lỗi phù hợp để xử lý lỗi. Trong thực tế, có thể có nhiều cách thích hợp để xử lý cùng một lỗi tùy thuộc vào mã gọi là gì. Lấy một tập tin chẳng hạn. Nếu bạn đang cố mở tệp cấu hình và không có ở đó, bỏ qua ngoại lệ và tiếp tục với cấu hình mặc định có thể là một phản hồi thích hợp. Nếu bạn đang mở một tệp riêng quan trọng đối với việc thực hiện chương trình và phần nào bị thiếu, tùy chọn duy nhất của bạn có lẽ là đóng chương trình.

Bao bọc các ngoại lệ trong các loại đúng là một mối quan tâm trực giao hoàn toàn.


1
+1 để giải thích rõ ràng tại sao các cấp độ khác nhau quan trọng. Ví dụ tuyệt vời về lỗi hệ thống tập tin.
Juan Carlos Coto

24

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 ArithmeticExceptioncuộc gọi đến từ cuộc gọi dividehoặ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 NullPointerExceptionhầ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ố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 đơ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!


Bạn đã đúng rằng mã cung cấp giao diện sẽ biết nhiều hơn về những gì có thể sai so với mã sử dụng giao diện, nhưng giả sử một phương thức sử dụng hai tài nguyên có cùng loại giao diện và các lỗi cần phải được xử lý khác nhau? Hoặc nếu một trong những tài nguyên đó trong nội bộ, như một chi tiết triển khai mà người tạo ra nó không biết, sử dụng các tài nguyên lồng nhau khác cùng loại? Có lớp ném doanh nghiệp WrappedFirstResourceExceptionhoặc WrappedSecondResourceExceptionvà yêu cầu lớp "nối dây" nhìn vào bên trong ngoại lệ đó để xem nguyên nhân gốc rễ của vấn đề ...
supercat

... Có thể là khó khăn, nhưng có vẻ tốt hơn là giả sử rằng bất kỳ FailingInputResourcengoại lệ nào sẽ là kết quả của một hoạt động với in1. Trên thực tế, tôi nghĩ rằng trong nhiều trường hợp, cách tiếp cận đúng sẽ là để lớp dây vượt qua một đối tượng xử lý ngoại lệ và có lớp nghiệp vụ bao gồm một lớp catchsau đó gọi handleExceptionphương thức của đối tượng đó . Phương pháp đó có thể chia sẻ lại, hoặc cung cấp dữ liệu mặc định hoặc đưa ra lời nhắc "Hủy bỏ / Thử lại / Thất bại" và để người vận hành quyết định những việc cần làm, v.v. tùy thuộc vào ứng dụng yêu cầu.
supercat

@supercat Tôi hiểu bạn đang nói gì. Tôi muốn nói rằng một triển khai tài nguyên cụ thể có trách nhiệm biết các ngoại lệ mà nó có thể ném ra. Nó không phải chỉ định mọi thứ (có cái gọi là hành vi không xác định ), nhưng nó phải đảm bảo không có sự mơ hồ. Ngoài ra, các ngoại lệ thời gian chạy không được kiểm tra sẽ được ghi lại. Nếu nó mâu thuẫn với tài liệu, thì đó là một lỗi. Nếu mã người gọi được dự kiến ​​sẽ làm bất cứ điều gì hợp lý về một ngoại lệ, thì mức tối thiểu là tài nguyên bao bọc chúng trong một số UnrecoverableInternalException, tương tự như mã lỗi HTTP 500.
Daniel Dinnyes

@supercat Giới thiệu về đề xuất của bạn về các trình xử lý lỗi có thể định cấu hình: chính xác! Trong ví dụ cuối cùng của tôi, logic xử lý lỗi được mã hóa cứng, gọi doMyBusinessphương thức tĩnh . Điều này là vì lợi ích của sự ngắn gọn, và hoàn toàn có thể làm cho nó năng động hơn. Một Handlerlớp như vậy sẽ được khởi tạo với một số tài nguyên đầu vào / đầu ra và có một handlephương thức nhận một lớp thực hiện a ReusableBusinessLogicInterface. Sau đó, bạn có thể kết hợp / cấu hình để sử dụng các trình triển khai logic, tài nguyên và logic nghiệp vụ khác nhau trong lớp dây ở đâu đó phía trên chúng.
Daniel Dinnyes

10

Để trả lời đúng câu hỏi này, chúng ta hãy lùi lại một bước và hỏi một câu hỏi thậm chí còn cơ bản hơn.

Tại sao chúng ta có ngoại lệ ở nơi đầu tiên?

Chúng tôi đưa ra các ngoại lệ để cho người gọi phương thức của chúng tôi biết rằng chúng tôi không thể làm những gì chúng tôi được yêu cầu. Loại ngoại lệ giải thích lý do tại sao chúng ta không thể làm những gì chúng ta muốn làm.

Chúng ta hãy xem một số mã:

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

Mã này rõ ràng có thể ném một ngoại lệ tham chiếu null nếu PropertyBlà null. Có hai điều mà chúng ta có thể làm trong trường hợp này để "sửa" cho tình huống này. Chúng ta có thể:

  • Tự động tạo PropertyB nếu chúng ta không có nó; hoặc là
  • Hãy để bong bóng ngoại lệ lên đến phương thức gọi.

Tạo PropertyB ở đây có thể rất nguy hiểm. Lý do nào khiến phương pháp này phải tạo ra PropertyB? Chắc chắn điều này sẽ vi phạm nguyên tắc trách nhiệm duy nhất. Trong tất cả khả năng, nếu PropertyB không tồn tại ở đây, điều đó cho thấy rằng đã xảy ra sự cố. Phương thức này đang được gọi trên một đối tượng được xây dựng một phần hoặc PropertyB được đặt thành null không chính xác. Bằng cách tạo PropertyB ở đây, chúng ta có thể che giấu một lỗi lớn hơn nhiều có thể cắn chúng ta sau này, chẳng hạn như một lỗi gây ra hỏng dữ liệu.

Thay vào đó, nếu chúng ta để bong bóng tham chiếu null lên, chúng ta sẽ cho nhà phát triển đã gọi phương thức này biết, ngay khi chúng ta có thể, thì có gì đó không ổn. Một điều kiện tiên quyết quan trọng của việc gọi phương pháp này đã bị bỏ lỡ.

Vì vậy, trong thực tế, chúng tôi đang ném sớm vì nó phân tách mối quan tâm của chúng tôi tốt hơn nhiều. Ngay sau khi xảy ra lỗi, chúng tôi sẽ cho các nhà phát triển thượng nguồn biết về nó.

Tại sao chúng ta "bắt muộn" là một câu chuyện khác. Chúng tôi không thực sự muốn bắt muộn, chúng tôi thực sự muốn bắt sớm vì chúng tôi biết cách xử lý vấn đề đúng cách. Một số thời gian này sẽ là mười lăm lớp trừu tượng sau đó và một số thời gian này sẽ ở điểm sáng tạo.

Vấn đề là chúng tôi muốn bắt ngoại lệ ở lớp trừu tượng cho phép chúng tôi xử lý ngoại lệ tại điểm chúng tôi có tất cả thông tin chúng tôi cần để xử lý ngoại lệ một cách chính xác.


Tôi nghĩ rằng bạn đang sử dụng các nhà phát triển ngược dòng theo nghĩa sai. Ngoài ra, bạn nói rằng nó vi phạm nguyên tắc trách nhiệm duy nhất, nhưng trong thực tế, nhiều bộ khởi tạo theo yêu cầu và bộ nhớ đệm giá trị được triển khai theo cách đó (tất nhiên là có các điều khiển đồng thời thích hợp)
Daniel Dinnyes

Trong ví dụ đã cho của bạn, những gì về việc kiểm tra null trước hoạt động trừ, như thế nàyif(PropertyB == null) return 0;
user1451111

1
Bạn cũng có thể giải thích chi tiết về đoạn cuối của bạn, cụ thể là bạn có ý nghĩa gì về ' lớp trừu tượng '.
dùng1451111

Nếu chúng ta đang thực hiện một số công việc IO, lớp trừu tượng để bắt Ngoại lệ IO sẽ là nơi chúng ta đang thực hiện công việc. Tại thời điểm đó, chúng tôi có tất cả thông tin chúng tôi cần để quyết định xem chúng tôi có muốn thử lại hoặc đưa hộp thông báo cho người dùng hoặc tạo một đối tượng bằng cách sử dụng một bộ mặc định hay không.
Stephen

"Trong ví dụ đã cho của bạn, những gì về việc kiểm tra null trước hoạt động trừ, như thế này nếu (PropertyB == null) trả về 0;" Kinh quá. Điều đó sẽ nói với phương thức gọi rằng tôi có những thứ hợp lệ để trừ và từ. Tất nhiên đây là ngữ cảnh nhưng sẽ là một thực tế tồi khi kiểm tra lỗi của tôi ở đây trong hầu hết các trường hợp.
Stephen

6

Ném ngay khi bạn thấy thứ gì đó đáng để ném để tránh đặt đồ vật vào trạng thái không hợp lệ. Có nghĩa là nếu một con trỏ null được thông qua, bạn hãy kiểm tra sớm và ném NPE trước khi nó có cơ hội chảy xuống mức thấp.

Hãy nắm bắt ngay khi bạn biết phải làm gì để khắc phục lỗi (đây thường không phải là nơi bạn ném nếu không bạn chỉ có thể sử dụng if-if), nếu tham số không hợp lệ được thông qua thì lớp cung cấp tham số sẽ giải quyết hậu quả .


1
Bạn đã viết: Ném sớm, ... Bắt sớm ...! Tại sao? Đó là một cách tiếp cận hoàn toàn trái ngược với "ném sớm, bắt muộn".
shylynx

1
@shylynx Tôi không biết "ném sớm, bắt muộn" đến từ đâu, nhưng giá trị của nó là đáng nghi ngờ. Chính xác thì bắt "muộn" nghĩa là gì? Trường hợp có ý nghĩa để bắt một ngoại lệ (nếu có) phụ thuộc vào vấn đề. Điều duy nhất rõ ràng là bạn muốn phát hiện vấn đề (và ném) càng sớm càng tốt.
Doval

2
Tôi giả sử "bắt muộn" có nghĩa là tương phản với cách bắt trước khi bạn có thể biết phải làm gì để khắc phục lỗi - ví dụ: đôi khi bạn thấy các hàm bắt mọi thứ chỉ để chúng có thể in thông báo lỗi và sau đó lấy lại ngoại lệ.

@Hurkyl: Một vấn đề với "bắt muộn" là nếu một ngoại lệ nổi lên qua các lớp mà không biết gì về nó, có thể rất khó để mã có thể ở một vị trí để làm gì đó về tình huống để biết rằng mọi thứ thực sự là như vậy hy vọng. Ví dụ đơn giản, giả sử nếu trình phân tích cú pháp cho tệp tài liệu người dùng cần tải CODEC từ đĩa và xảy ra lỗi đĩa trong khi đọc, mã gọi trình phân tích cú pháp có thể hành động không phù hợp nếu nó nghĩ rằng có lỗi đĩa khi đọc người dùng tài liệu.
supercat

4

Quy tắc kinh doanh hợp lệ là 'nếu phần mềm cấp thấp hơn không tính được giá trị, thì ...'

Điều này chỉ có thể được thể hiện ở cấp độ cao hơn, nếu không, phần mềm cấp thấp hơn đang cố gắng thay đổi hành vi của nó dựa trên tính chính xác của chính nó, điều này sẽ chỉ kết thúc trong một nút thắt.


2

Trước hết, ngoại lệ là cho các tình huống đặc biệt. Trong ví dụ của bạn, không có con số nào có thể được tính nếu dữ liệu thô không xuất hiện vì không thể tải được.

Từ kinh nghiệm của tôi, đó là cách thực hành tốt đến các ngoại lệ trừu tượng trong khi đi lên ngăn xếp. Thông thường các điểm mà bạn muốn làm điều này là bất cứ khi nào một ngoại lệ vượt qua ranh giới giữa hai lớp.

Nếu có lỗi khi thu thập dữ liệu thô của bạn trong lớp dữ liệu, hãy ném ngoại lệ để thông báo cho bất kỳ ai yêu cầu dữ liệu. Đừng cố gắng giải quyết vấn đề này ở đây. Độ phức tạp của mã xử lý có thể rất cao. Ngoài ra, lớp dữ liệu chỉ chịu trách nhiệm yêu cầu dữ liệu, không xử lý các lỗi xảy ra trong khi thực hiện việc này. Đây là những gì có nghĩa là "ném sớm" .

Trong ví dụ của bạn, lớp bắt là lớp dịch vụ. Bản thân dịch vụ là một lớp mới, nằm trên lớp truy cập dữ liệu. Vì vậy, bạn muốn bắt ngoại lệ ở đó. Có lẽ dịch vụ của bạn có một số cơ sở hạ tầng chuyển đổi dự phòng và cố gắng yêu cầu dữ liệu từ kho lưu trữ khác. Nếu điều này cũng không thành công, hãy bọc ngoại lệ bên trong thứ mà người gọi dịch vụ hiểu (nếu đó là dịch vụ web thì đây có thể là lỗi SOAP). Đặt ngoại lệ ban đầu là ngoại lệ bên trong để các lớp sau có thể ghi lại chính xác những gì đã sai.

Lỗi dịch vụ có thể bị bắt bởi lớp gọi dịch vụ (ví dụ: UI). Và đây là những gì có nghĩa là "bắt muộn" . Nếu bạn không thể xử lý ngoại lệ ở lớp thấp hơn, hãy thử lại. Nếu lớp trên cùng không thể xử lý ngoại lệ, xử lý nó! Điều này có thể bao gồm đăng nhập hoặc trình bày nó.

Lý do tại sao bạn nên chia sẻ lại các ngoại lệ (như được mô tả ở trên bằng cách gói chúng trong các ngoại lệ chung hơn) là, người dùng rất có thể không hiểu rằng có lỗi vì, ví dụ, một con trỏ trỏ vào bộ nhớ không hợp lệ. Và anh không quan tâm. Anh ta chỉ quan tâm rằng con số không thể được tính bởi dịch vụ và đây là thông tin nên được hiển thị cho anh ta.

Đi xa hơn bạn có thể (trong một thế giới lý tưởng) hoàn toàn loại bỏ try/ catchmã khỏi UI. Thay vào đó, hãy sử dụng một trình xử lý ngoại lệ toàn cầu có thể hiểu các ngoại lệ có thể bị ném bởi các lớp thấp hơn, ghi chúng vào một số nhật ký và bọc chúng vào các đối tượng lỗi có chứa thông tin có ý nghĩa (và có thể được bản địa hóa) của lỗi. Những đối tượng đó có thể dễ dàng được trình bày cho người dùng dưới bất kỳ hình thức nào bạn muốn (hộp thông báo, thông báo, tin nhắn, v.v.).


1

Nói chung, ngoại lệ sớm là một cách tốt vì bạn không muốn các hợp đồng bị hỏng chảy qua mã hơn mức cần thiết. Ví dụ, nếu bạn mong đợi một tham số hàm nhất định là một số nguyên dương thì bạn nên thực thi ràng buộc đó tại điểm gọi hàm thay vì đợi cho đến khi biến đó được sử dụng ở nơi khác trong ngăn xếp mã.

Bắt muộn tôi không thể bình luận vì tôi có quy tắc riêng của mình và nó thay đổi từ dự án này sang dự án khác. Một điều tôi cố gắng làm là phân tách các ngoại lệ thành hai nhóm. Một là chỉ sử dụng nội bộ và hai là chỉ sử dụng bên ngoài. Ngoại lệ bên trong được bắt và xử lý bởi mã của riêng tôi và ngoại lệ bên ngoài có nghĩa là được xử lý bởi bất kỳ mã nào đang gọi cho tôi. Đây về cơ bản là một hình thức bắt mọi thứ sau đó nhưng không hoàn toàn vì nó mang lại cho tôi sự linh hoạt để đi lệch khỏi quy tắc khi cần thiết trong mã nội bộ.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.