Tại sao phương pháp không nên ném nhiều loại ngoại lệ được kiểm tra?


47

Chúng tôi sử dụng SonarQube để phân tích mã Java của mình và nó có quy tắc này (được đặt thành quan trọng):

Các phương thức công khai nên ném tối đa một ngoại lệ được kiểm tra

Sử dụng các ngoại lệ được kiểm tra buộc người gọi phương thức xử lý lỗi, bằng cách truyền bá chúng hoặc bằng cách xử lý chúng. Điều này làm cho các ngoại lệ đó hoàn toàn là một phần của API của phương thức.

Để giữ sự phức tạp cho người gọi hợp lý, các phương thức không nên ném nhiều hơn một loại ngoại lệ được kiểm tra. "

Một chút nữa trong Sonar có điều này :

Các phương thức công khai nên ném tối đa một ngoại lệ được kiểm tra

Sử dụng các ngoại lệ được kiểm tra buộc người gọi phương thức xử lý lỗi, bằng cách truyền bá chúng hoặc bằng cách xử lý chúng. Điều này làm cho các ngoại lệ đó hoàn toàn là một phần của API của phương thức.

Để giữ độ phức tạp cho người gọi hợp lý, các phương thức không nên ném nhiều hơn một loại ngoại lệ được kiểm tra.

Các mã sau đây:

public void delete() throws IOException, SQLException {      // Non-Compliant
  /* ... */
}

nên được tái cấu trúc thành:

public void delete() throws SomeApplicationLevelException {  // Compliant
    /* ... */
}

Các phương thức ghi đè không được kiểm tra theo quy tắc này và được phép đưa ra một số ngoại lệ được kiểm tra.

Tôi chưa bao giờ bắt gặp quy tắc / khuyến nghị này trong các bài đọc về xử lý ngoại lệ và đã cố gắng tìm một số tiêu chuẩn, thảo luận, v.v. về chủ đề này. Điều duy nhất tôi tìm thấy là điều này từ CodeRach: Phương pháp nên ném bao nhiêu ngoại lệ?

Đây có phải là một tiêu chuẩn được chấp nhận?


7
Làm gì bạn nghĩ? Lời giải thích mà bạn trích dẫn từ SonarQube có vẻ hợp lý; Bạn có bất kỳ lý do để nghi ngờ nó?
Robert Harvey

3
Tôi đã viết rất nhiều mã ném nhiều hơn một ngoại lệ và đã sử dụng nhiều thư viện cũng ném nhiều hơn một ngoại lệ. Ngoài ra, trong sách / bài viết về xử lý ngoại lệ, chủ đề giới hạn số lượng ngoại lệ được ném thường không được đưa lên. Nhưng nhiều ví dụ cho thấy ném / bắt nhiều người đưa ra sự chấp thuận ngầm cho thực tiễn. Vì vậy, tôi thấy quy tắc đáng ngạc nhiên và muốn nghiên cứu thêm về các thực tiễn / triết lý tốt nhất về xử lý ngoại lệ so với chỉ các ví dụ hướng dẫn cơ bản.
sdoca

Câu trả lời:


32

Hãy xem xét tình huống mà bạn có mã được cung cấp:

public void delete() throws IOException, SQLException {      // Non-Compliant
  /* ... */
}

Điều nguy hiểm ở đây là mã mà bạn viết để gọi delete()sẽ giống như:

try {
  foo.delete()
} catch (Exception e) {
  /* ... */
}

Điều này cũng tệ. Và nó sẽ được bắt với một quy tắc khác là cờ bắt lớp Exception cơ sở.

Điều quan trọng là không viết mã khiến bạn muốn viết mã xấu ở nơi khác.

Quy tắc mà bạn đang gặp phải là một quy tắc khá phổ biến. Checkstyle có nó trong các quy tắc thiết kế của nó:

Ném

Hạn chế ném các câu lệnh vào một số lượng được chỉ định (1 theo mặc định).

Đặt vấn đề: Ngoại lệ là một phần của giao diện của phương thức. Việc tuyên bố một phương pháp để ném quá nhiều ngoại lệ bắt nguồn khác nhau làm cho việc xử lý ngoại lệ trở nên khó khăn và dẫn đến các thực tiễn lập trình kém như viết mã như bắt (Exception ex). Kiểm tra này buộc các nhà phát triển đặt ngoại lệ vào một hệ thống phân cấp sao cho trong trường hợp đơn giản nhất, chỉ có một loại ngoại lệ cần được kiểm tra bởi người gọi nhưng bất kỳ lớp con nào cũng có thể bị bắt nếu cần thiết.

Điều này mô tả chính xác vấn đề và vấn đề là gì và tại sao bạn không nên làm điều đó. Đó là một tiêu chuẩn được chấp nhận tốt mà nhiều công cụ phân tích tĩnh sẽ xác định và gắn cờ.

Và trong khi bạn có thể làm điều đó theo thiết kế ngôn ngữ, và có thể đôi khi đó là điều đúng đắn, đó là điều bạn nên xem và ngay lập tức "ừ, tại sao tôi lại làm điều này?" Có thể chấp nhận mã nội bộ khi mọi người đều bị kỷ luật đủ để không bao giờ catch (Exception e) {}, nhưng thường xuyên hơn là tôi không thấy mọi người cắt góc đặc biệt là trong các tình huống nội bộ.

Đừng làm cho những người sử dụng lớp của bạn muốn viết mã xấu.


Tôi nên chỉ ra rằng tầm quan trọng của điều này đã giảm đi với Java SE 7 và sau đó bởi vì một câu lệnh bắt duy nhất có thể bắt được nhiều ngoại lệ ( Bắt nhiều loại ngoại lệ và lấy lại ngoại lệ với Kiểm tra loại cải tiến từ Oracle).

Với Java 6 trở về trước, bạn sẽ có mã giống như:

public void delete() throws IOException, SQLException {
  /* ... */
}

try {
  foo.delete()
} catch (IOException ex) {
     logger.log(ex);
     throw ex;
} catch (SQLException ex) {
     logger.log(ex);
     throw ex;
}

hoặc là

try {
    foo.delete()
} catch (Exception ex) {
    logger.log(ex);
    throw ex;
}

Cả hai tùy chọn này với Java 6 đều lý tưởng. Cách tiếp cận đầu tiên vi phạm DRY . Nhiều khối làm cùng một việc, lặp đi lặp lại - một lần cho mỗi ngoại lệ. Bạn muốn đăng nhập ngoại lệ và suy nghĩ lại? Đồng ý. Các dòng mã giống nhau cho mỗi ngoại lệ.

Tùy chọn thứ hai là tồi tệ hơn vì nhiều lý do. Đầu tiên, nó có nghĩa là bạn đang nắm bắt tất cả các ngoại lệ. Con trỏ Null bị bắt ở đó (và nó không nên). Hơn nữa, bạn đang rethrowing một Exceptioncó nghĩa là phương pháp chữ ký sẽ được deleteSomething() throws Exceptionmà chỉ làm cho một mớ hỗn độn thêm lên stack như những người sử dụng mã của bạn bây giờ được buộc vào catch(Exception e).

Với Java 7, đây không phải quan trọng bởi vì bạn thay vì có thể làm:

catch (IOException|SQLException ex) {
    logger.log(ex);
    throw ex;
}

Hơn nữa, các loại kiểm tra nếu một không bắt các loại trường hợp ngoại lệ được ném:

public void rethrowException(String exceptionName)
throws IOException, SQLException {
    try {
        foo.delete();
    } catch (Exception e) {
        throw e;
    }
}

Trình kiểm tra loại sẽ nhận ra rằng ecó thể chỉ được các loại IOExceptionhoặc SQLException. Tôi vẫn không quá nhiệt tình về việc sử dụng kiểu này, nhưng nó không gây ra mã xấu như trong Java 6 (nơi nó sẽ buộc bạn phải có chữ ký phương thức là siêu lớp mà các ngoại lệ mở rộng).

Bất chấp tất cả những thay đổi này, nhiều công cụ phân tích tĩnh (Sonar, PMD, Checkstyle) vẫn đang thực thi các hướng dẫn kiểu Java 6. Đó không phải là một điều xấu. Tôi có xu hướng đồng ý với một cảnh báo những điều này vẫn được thi hành, nhưng bạn có thể thay đổi mức độ ưu tiên của chúng thành chính hoặc phụ theo cách nhóm của bạn ưu tiên chúng.

Nếu trường hợp ngoại lệ nên được kiểm tra hoặc không kiểm soát ... đó là một vấn đề của g r e một t tranh luận rằng người ta có thể dễ dàng tìm thấy vô số bài đăng trên blog chiếm mỗi bên của cuộc tranh cãi. Tuy nhiên, nếu bạn đang làm việc với các ngoại lệ được kiểm tra, có lẽ bạn nên tránh ném nhiều loại, ít nhất là theo Java 6.


Chấp nhận đây là câu trả lời vì nó trả lời câu hỏi của tôi về nó là một tiêu chuẩn được chấp nhận tốt. Tuy nhiên, tôi vẫn đang đọc các cuộc thảo luận ủng hộ khác nhau bắt đầu bằng liên kết Panzercrisis được cung cấp trong câu trả lời của anh ấy để xác định tiêu chuẩn của tôi sẽ là gì.
sdoca

"Trình kiểm tra loại sẽ nhận ra rằng e chỉ có thể thuộc loại IOException hoặc SQLException.": Điều này có nghĩa là gì? Điều gì xảy ra khi một ngoại lệ của loại khác bị ném qua foo.delete()? Nó vẫn bị bắt và rút lại?
Giorgio

@Giorgio Sẽ là một lỗi thời gian biên dịch nếu deleteném một ngoại lệ được kiểm tra khác với IOException hoặc SQLException trong ví dụ đó. Điểm mấu chốt mà tôi đã cố gắng thực hiện là một phương thức gọi rethrowException vẫn sẽ nhận được loại Exception trong Java 7. Trong Java 6, tất cả các phương thức đó được hợp nhất thành Exceptionloại phổ biến khiến phân tích tĩnh và các bộ mã hóa khác buồn.

Tôi hiểu rồi. Có vẻ như một chút phức tạp với tôi. Tôi sẽ thấy nó trực quan hơn để cấm catch (Exception e)và buộc nó phải catch (IOException e)hoặc catch (SQLException e)thay vào đó.
Giorgio

@Giorgio Đây là một bước tiến tăng dần từ Java 6 để cố gắng viết mã tốt dễ dàng hơn. Thật không may, tùy chọn để viết mã xấu sẽ ở với chúng tôi trong một thời gian dài. Hãy nhớ rằng Java 7 có thể làm catch(IOException|SQLException ex)thay thế. Nhưng, nếu bạn sắp sửa lại ngoại lệ, cho phép trình kiểm tra kiểu truyền bá loại thực tế của ngoại lệ, đơn giản hóa mã không phải là một điều xấu.

22

Lý do mà bạn muốn, lý tưởng nhất là chỉ muốn ném một loại ngoại lệ là vì nếu không thì có thể vi phạm các nguyên tắc Đảo ngược trách nhiệmphụ thuộc duy nhất . Hãy sử dụng một ví dụ để chứng minh.

Giả sử chúng ta có một phương pháp lấy dữ liệu từ tính bền bỉ và tính bền bỉ đó là một tập hợp các tệp. Vì chúng tôi đang xử lý các tệp, chúng tôi có thể có FileNotFoundException:

public String getData(int id) throws FileNotFoundException

Bây giờ, chúng tôi có một sự thay đổi trong các yêu cầu và dữ liệu của chúng tôi đến từ cơ sở dữ liệu. Thay vì một FileNotFoundException(vì chúng tôi không xử lý các tệp), bây giờ chúng tôi ném một SQLException:

public String getData(int id) throws SQLException

Bây giờ chúng ta sẽ phải duyệt qua tất cả các mã sử dụng phương thức của chúng ta và thay đổi ngoại lệ mà chúng ta phải kiểm tra, nếu không thì mã sẽ không được biên dịch. Nếu phương pháp của chúng tôi được gọi là xa và rộng, điều đó có thể thay đổi nhiều / khiến người khác thay đổi. Mất rất nhiều thời gian và mọi người sẽ không hạnh phúc.

Sự đảo ngược phụ thuộc nói rằng chúng ta thực sự không nên đưa ra một trong những ngoại lệ này bởi vì chúng phơi bày chi tiết thực hiện nội bộ mà chúng ta đang làm việc để gói gọn. Gọi mã cần biết loại kiên trì nào chúng ta đang sử dụng, khi nào thực sự cần lo lắng về việc có thể lấy lại được bản ghi hay không. Thay vào đó, chúng ta nên đưa ra một ngoại lệ truyền tải lỗi ở cùng mức độ trừu tượng như chúng ta đang phơi bày thông qua API của mình:

public String getData(int id) throws InvalidRecordException

Bây giờ, nếu chúng ta thay đổi triển khai nội bộ, chúng ta có thể chỉ cần bọc ngoại lệ đó trong một InvalidRecordExceptionvà chuyển nó đi (hoặc không bao bọc nó, và chỉ cần ném một cái mới InvalidRecordException). Mã bên ngoài không biết hoặc quan tâm loại kiên trì nào đang được sử dụng. Tất cả được gói gọn.


Đối với Trách nhiệm đơn lẻ, chúng ta cần suy nghĩ về mã đưa ra nhiều ngoại lệ, không liên quan. Giả sử chúng ta có phương pháp sau:

public Record parseFile(String filename) throws IOException, ParseException

Chúng ta có thể nói gì về phương pháp này? Chúng ta có thể nói chỉ từ chữ ký rằng nó sẽ mở một tệp phân tích cú pháp. Khi chúng ta thấy một kết hợp, như "và" hoặc "hoặc" trong mô tả của một phương thức, chúng ta biết rằng nó đang làm nhiều hơn một việc; nó có nhiều hơn một trách nhiệm . Các phương thức có nhiều trách nhiệm khó quản lý vì chúng có thể thay đổi nếu bất kỳ trách nhiệm nào thay đổi. Thay vào đó, chúng ta nên phá vỡ các phương thức để chúng có một trách nhiệm duy nhất:

public String readFile(String filename) throws IOException
public Record parse(String data) throws ParseException

Chúng tôi đã trích xuất trách nhiệm đọc tệp từ trách nhiệm phân tích dữ liệu. Một tác dụng phụ của việc này là giờ đây chúng ta có thể chuyển bất kỳ dữ liệu Chuỗi nào sang dữ liệu phân tích từ bất kỳ nguồn nào: trong bộ nhớ, tệp, mạng, v.v. parseHiện tại chúng ta cũng có thể kiểm tra dễ dàng hơn vì chúng ta không cần tệp trên đĩa để chạy thử nghiệm chống lại.


Đôi khi thực sự có hai (hoặc nhiều) ngoại lệ mà chúng ta có thể đưa ra từ một phương thức, nhưng nếu chúng ta dính vào SRP và DIP, thời gian chúng ta gặp phải tình huống này trở nên hiếm hơn.


Tôi hoàn toàn đồng ý với việc gói các ngoại lệ cấp thấp hơn theo ví dụ của bạn. Chúng tôi làm điều đó thường xuyên và đưa ra các phương sai của MyAppExceptions. Một trong những ví dụ mà tôi đưa ra nhiều ngoại lệ là khi cố gắng cập nhật một bản ghi trong cơ sở dữ liệu. Phương thức này đưa ra một RecordNotFoundException. Tuy nhiên, bản ghi chỉ có thể được cập nhật nếu nó ở một trạng thái nhất định, vì vậy phương thức này cũng ném một UnlimitedRecordStateException. Tôi nghĩ rằng điều này là hợp lệ và cung cấp cho người gọi với thông tin có giá trị.
sdoca

@sdoca Nếu updatephương pháp của bạn là nguyên tử như bạn có thể thực hiện và các trường hợp ngoại lệ ở mức độ trừu tượng thích hợp, thì có, có vẻ như bạn cần phải ném hai loại ngoại lệ khác nhau, vì có hai trường hợp ngoại lệ. Đó phải là thước đo xem có bao nhiêu trường hợp ngoại lệ có thể được ném ra, thay vì các quy tắc nói dối (đôi khi tùy tiện) này.
cbojar

2
Nhưng nếu tôi có một phương thức đọc dữ liệu từ một luồng và phân tích cú pháp khi nó đi cùng, tôi không thể chia hai hàm đó mà không đọc toàn bộ luồng vào bộ đệm, điều này có thể khó sử dụng. Hơn nữa, mã quyết định cách xử lý đúng các ngoại lệ có thể tách biệt với mã đọc và phân tích cú pháp. Khi tôi đang viết mã đọc và phân tích cú pháp, tôi không biết làm thế nào mã gọi mã của tôi có thể muốn xử lý một trong hai loại Ngoại lệ, vì vậy tôi cần phải cho cả hai thông qua.
dùng3294068

+1: Tôi thích câu trả lời này rất nhiều, đặc biệt là vì nó giải quyết vấn đề theo quan điểm mô hình hóa. Thường thì không cần thiết phải sử dụng một thành ngữ khác (như catch (IOException | SQLException ex)) vì vấn đề thực sự nằm ở mô hình / thiết kế chương trình.
Giorgio

3

Tôi nhớ một chút lo lắng về điều này một chút khi chơi với Java một thời gian trước, nhưng tôi không thực sự ý thức được sự khác biệt giữa kiểm tra và không được kiểm tra cho đến khi tôi đọc câu hỏi của bạn. Tôi tìm thấy bài viết này trên Google khá nhanh và nó đi vào một số tranh cãi rõ ràng:

http://tutorials.jenkov.com/java-exception-handling/checked-or-unchecked-exceptions.html

Điều đó đang được nói, một trong những vấn đề mà anh chàng này đã đề cập với các ngoại lệ được kiểm tra là (và cá nhân tôi đã gặp phải vấn đề này ngay từ đầu với Java) nếu bạn tiếp tục thêm một loạt các ngoại lệ được kiểm tra vào throwscác mệnh đề trong khai báo phương thức của bạn, không chỉ bạn có phải đưa thêm mã soạn sẵn để hỗ trợ nó khi bạn chuyển sang các phương thức cấp cao hơn không, nhưng nó cũng chỉ tạo ra một mớ hỗn độn lớn hơn và phá vỡ tính tương thích khi bạn cố gắng giới thiệu nhiều loại ngoại lệ hơn cho các phương thức cấp thấp hơn. Nếu bạn thêm một loại ngoại lệ được kiểm tra vào một phương thức cấp thấp hơn, thì bạn phải chạy lại mã của mình và điều chỉnh một số khai báo phương thức khác.

Một điểm giảm thiểu được đề cập trong bài viết - và tác giả không thích cá nhân này - là tạo một ngoại lệ lớp cơ sở, giới hạn các throwsmệnh đề của bạn chỉ sử dụng nó, và sau đó chỉ nâng các lớp con của nó trong nội bộ. Bằng cách đó, bạn có thể tạo các loại ngoại lệ được kiểm tra mới mà không phải chạy lại tất cả mã của mình.

Tác giả của bài viết này có thể không thích điều này lắm, nhưng nó có ý nghĩa hoàn hảo trong trải nghiệm cá nhân của tôi (đặc biệt là nếu bạn có thể tìm kiếm tất cả các lớp con là gì), và tôi cá rằng đó là lý do tại sao lời khuyên bạn đưa ra là giới hạn tất cả mọi thứ cho một loại ngoại lệ được kiểm tra mỗi loại. Thêm nữa là lời khuyên mà bạn đề cập thực sự cho phép nhiều loại ngoại lệ được kiểm tra trong các phương pháp không công khai, điều này có ý nghĩa hoàn hảo nếu đây là động cơ của họ (hoặc thậm chí khác). Nếu đó chỉ là một phương thức riêng tư hoặc một cái gì đó tương tự, thì bạn sẽ không chạy qua một nửa cơ sở mã của mình khi bạn thay đổi một điều nhỏ.

Bạn đã hỏi phần lớn đây có phải là một tiêu chuẩn được chấp nhận hay không, nhưng giữa nghiên cứu của bạn mà bạn đề cập, bài viết được suy nghĩ hợp lý này và chỉ nói từ kinh nghiệm lập trình cá nhân, nó chắc chắn dường như không nổi bật theo bất kỳ cách nào.


2
Tại sao không chỉ tuyên bố ném Throwable, và được thực hiện với nó, thay vì phát minh ra tất cả hệ thống phân cấp của riêng bạn?
Ded repeatator

@Ded repeatator Đó là lý do tác giả cũng không thích ý tưởng đó; anh ta chỉ hình dung bạn cũng có thể sử dụng tất cả những gì không được kiểm tra, nếu bạn sẽ làm điều đó. Nhưng nếu bất cứ ai đang sử dụng API (có thể là đồng nghiệp của bạn) có một danh sách tất cả các ngoại lệ được phân lớp xuất phát từ lớp cơ sở của bạn, tôi có thể thấy một chút lợi ích ít nhất là cho họ biết rằng tất cả các ngoại lệ dự kiến ​​là trong một tập hợp các lớp con nhất định; sau đó nếu họ cảm thấy như một trong số họ "dễ cầm tay" hơn những người khác, họ sẽ không dễ dàng từ bỏ một người xử lý cụ thể cho việc đó.
Panzercrisis

Lý do các trường hợp ngoại lệ được kiểm tra nói chung là nghiệp xấu rất đơn giản: Chúng là virus, lây nhiễm cho mọi người dùng. Chỉ định một đặc điểm kỹ thuật rộng nhất có thể có chủ ý chỉ là một cách để nói tất cả các ngoại lệ không được kiểm tra, và do đó tránh được sự lộn xộn. Đúng, ghi lại những gì bạn có thể muốn xử lý là một ý tưởng hay, đối với tài liệu : Chỉ cần biết ngoại lệ nào có thể đến từ một hàm có giá trị giới hạn nghiêm ngặt (không có gì cả / có thể là một, nhưng dù sao thì Java cũng không cho phép điều đó) .
Ded repeatator

1
@Ded repeatator Tôi không tán thành ý tưởng này và tôi cũng không nhất thiết ủng hộ nó. Tôi chỉ nói về hướng mà lời khuyên mà OP đưa ra có thể đến từ đâu.
Panzercrisis

1
Cảm ơn các liên kết. Đó là một nơi khởi đầu tuyệt vời để đọc về chủ đề này.
sdoca

-1

Ném nhiều ngoại lệ được kiểm tra có ý nghĩa khi có nhiều việc hợp lý để làm.

Ví dụ: giả sử bạn có phương pháp

public void doSomething(Credentials cred, Work work) 
    throws CredentialsRequiredException, TryAgainLaterException{...}

Điều này vi phạm quy tắc ngoại lệ pne, nhưng có ý nghĩa.

Thật không may, những gì thường xảy ra là những phương pháp như

void doSomething() 
    throws IOException, JAXBException,SQLException,MyException {...}

Ở đây có rất ít cơ hội cho người gọi để làm một cái gì đó cụ thể dựa trên loại ngoại lệ. Vì vậy, nếu chúng tôi muốn thúc đẩy anh ấy nhận ra rằng các phương thức này CÓ THỂ và đôi khi SILL sai, việc ném SomethingMightGoWrongException là tốt hơn và tốt hơn.

Do đó quy tắc nhiều nhất là một ngoại lệ được kiểm tra.

Nhưng nếu dự án của bạn sử dụng thiết kế nơi có nhiều ngoại lệ được kiểm tra đầy ý nghĩa, quy tắc này sẽ không được áp dụng.

Sidenote: Một cái gì đó thực sự có thể đi sai ở hầu hết mọi nơi, vì vậy người ta có thể nghĩ về việc sử dụng? mở rộng RuntimeException, nhưng có sự khác biệt giữa "tất cả chúng ta đều mắc lỗi" và "điều này nói về hệ thống bên ngoài và đôi khi nó sẽ bị hỏng, giải quyết nó".


1
"Nhiều việc hợp lý để làm" hầu như không tuân thủ srp - điểm này đã được nêu ra trong câu trả lời trước
gnat

Nó làm. Bạn có thể XÁC ĐỊNH một vấn đề trong một chức năng (xử lý một khía cạnh) và một vấn đề khác trong một khía cạnh khác (cũng xử lý một khía cạnh) được gọi từ một chức năng xử lý một điều, cụ thể là gọi hai chức năng này. Và người gọi có thể trong một lớp thử bắt (trong một chức năng) XỬ LÝ một vấn đề và chuyển lên một vấn đề khác và xử lý vấn đề đó một mình.
user470365
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.