Là ném RuntimeExceptions mới trong mã không thể truy cập là một phong cách xấu?


31

Tôi đã được chỉ định để duy trì một ứng dụng được viết trước đây bởi các nhà phát triển lành nghề hơn. Tôi đã xem qua đoạn mã này:

public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
        try {
            return translate(mailManagementService.retrieveUserMailConfiguration(id));
        } catch (Exception e) {
            rethrow(e);
        }
        throw new RuntimeException("cannot reach here");
    }

Tôi tò mò nếu ném RuntimeException("cannot reach here")là hợp lý. Có lẽ tôi đang thiếu một cái gì đó rõ ràng khi biết rằng đoạn mã này đến từ đồng nghiệp dày dạn hơn.
EDIT: Đây là cơ thể suy nghĩ lại mà một số câu trả lời đề cập đến. Tôi cho rằng nó không quan trọng trong câu hỏi này.

private void rethrow(Exception e) throws MailException {
        if (e instanceof InvalidDataException) {
            InvalidDataException ex = (InvalidDataException) e;
            rethrow(ex);
        }
        if (e instanceof EntityAlreadyExistsException) {
            EntityAlreadyExistsException ex = (EntityAlreadyExistsException) e;
            rethrow(ex);
        }
        if (e instanceof EntityNotFoundException) {
            EntityNotFoundException ex = (EntityNotFoundException) e;
            rethrow(ex);
        }
        if (e instanceof NoPermissionException) {
            NoPermissionException ex = (NoPermissionException) e;
            rethrow(ex);
        }
        if (e instanceof ServiceUnavailableException) {
            ServiceUnavailableException ex = (ServiceUnavailableException) e;
            rethrow(ex);
        }
        LOG.error("internal error, original exception", e);
        throw new MailUnexpectedException();
    }


private void rethrow(ServiceUnavailableException e) throws
            MailServiceUnavailableException {
        throw new MailServiceUnavailableException();
    }

private void rethrow(NoPermissionException e) throws PersonNotAuthorizedException {
    throw new PersonNotAuthorizedException();
}

private void rethrow(InvalidDataException e) throws
        MailInvalidIdException, MailLoginNotAvailableException,
        MailInvalidLoginException, MailInvalidPasswordException,
        MailInvalidEmailException {
    switch (e.getDetail()) {
        case ID_INVALID:
            throw new MailInvalidIdException();
        case LOGIN_INVALID:
            throw new MailInvalidLoginException();
        case LOGIN_NOT_ALLOWED:
            throw new MailLoginNotAvailableException();
        case PASSWORD_INVALID:
            throw new MailInvalidPasswordException();
        case EMAIL_INVALID:
            throw new MailInvalidEmailException();
    }
}

private void rethrow(EntityAlreadyExistsException e)
        throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
    switch (e.getDetail()) {
        case LOGIN_ALREADY_TAKEN:
            throw new MailLoginNotAvailableException();
        case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
            throw new MailEmailAddressAlreadyForwardedToException();
    }
}

private void rethrow(EntityNotFoundException e) throws
        MailAccountNotCreatedException,
        MailAliasNotCreatedException {
    switch (e.getDetail()) {
        case ACCOUNT_NOT_FOUND:
            throw new MailAccountNotCreatedException();
        case ALIAS_NOT_FOUND:
            throw new MailAliasNotCreatedException();
    }
}

12
nó là kỹ thuật có thể truy cập, nếu rethrowkhông thực sự throwlà một ngoại lệ. (điều này có thể xảy ra vào một ngày nào đó, nếu việc triển khai thay đổi)
njzk2

34
Bên cạnh đó, một AssertsError có thể là một lựa chọn tốt hơn về mặt ngữ nghĩa so với RuntimeException.
JvR

1
Cú ném cuối cùng là dư thừa. Nếu bạn bật findbugs hoặc các công cụ phân tích tĩnh khác, nó sẽ gắn cờ các loại đường này để xóa.
Bon Ami

7
Bây giờ tôi thấy phần còn lại của mã, tôi nghĩ rằng có lẽ bạn nên thay đổi "nhà phát triển lành nghề hơn" thành " nhà phát triển cao cấp hơn ". Đánh giá mã sẽ được hạnh phúc để giải thích tại sao.
JvR

3
Tôi đã cố gắng tạo ra các ví dụ giả thuyết về lý do tại sao các ngoại lệ là khủng khiếp. Nhưng không có gì tôi từng làm giữ một ngọn nến cho điều này.
Paul Draper

Câu trả lời:


36

Đầu tiên, cảm ơn bạn đã đưa ra câu hỏi của bạn và cho chúng tôi biết những gì rethrowlàm. Vì vậy, trên thực tế, những gì nó làm là chuyển đổi các ngoại lệ với các thuộc tính thành các lớp ngoại lệ chi tiết hơn. Thêm về điều này sau.

Vì ban đầu tôi không thực sự trả lời câu hỏi chính, nên đây là: có, nói chung là phong cách xấu khi ném ngoại lệ thời gian chạy vào mã không thể truy cập được; bạn nên sử dụng các xác nhận, hoặc thậm chí tốt hơn, tránh vấn đề. Như đã chỉ ra, trình biên dịch ở đây không thể chắc chắn rằng mã không bao giờ đi ra khỏi try/catchkhối. Bạn có thể cấu trúc lại mã của mình bằng cách tận dụng ...

Lỗi là giá trị

(Không có gì đáng ngạc nhiên, nó rất nổi tiếng khi đi )

Chúng ta hãy sử dụng một ví dụ đơn giản hơn, ví dụ tôi đã sử dụng trước khi chỉnh sửa: vì vậy hãy tưởng tượng rằng bạn đang ghi nhật ký một cái gì đó và xây dựng một ngoại lệ của trình bao bọc như trong câu trả lời của Konrad . Hãy gọi nó là logAndWrap.

Thay vì ném ngoại lệ như một hiệu ứng phụ của logAndWrap, bạn có thể để nó thực hiện công việc của nó như một hiệu ứng phụ và làm cho nó trả lại một ngoại lệ (ít nhất là một ngoại lệ được đưa vào). Bạn không cần sử dụng thuốc generic, chỉ cần các chức năng cơ bản:

private Exception logAndWrap(Exception exception) {
    // or whatever it actually does
    Log.e("Ouch! " + exception.getMessage());
    return new CustomWrapperException(exception);
}

Sau đó, bạn throwrõ ràng và trình biên dịch của bạn rất vui:

try {
     return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
     throw logAndWrap(e);
}

Nếu bạn quên throwthì sao?

Như đã giải thích trong nhận xét của Joe23 , một cách lập trình phòng thủ để đảm bảo rằng ngoại lệ luôn được ném sẽ bao gồm việc thực hiện một cách rõ ràng throw new CustomWrapperException(exception)vào cuối logAndWrap, như được thực hiện bởi Guava.Throwables . Bằng cách đó, bạn biết rằng ngoại lệ sẽ được ném và bộ phân tích loại của bạn rất vui. Tuy nhiên, các ngoại lệ tùy chỉnh của bạn cần phải được kiểm tra ngoại lệ, điều này không phải lúc nào cũng có thể. Ngoài ra, tôi đánh giá rủi ro của một nhà phát triển bị thiếu để viết throwlà rất thấp: nhà phát triển phải quên nó phương thức xung quanh sẽ không trả về bất cứ điều gì, nếu không trình biên dịch sẽ phát hiện ra một trả về bị thiếu. Đây là một cách thú vị để chống lại hệ thống loại, và nó hoạt động, mặc dù.

Suy nghĩ lại

Thực tế rethrowcó thể được viết như là một chức năng, nhưng tôi có vấn đề với việc thực hiện hiện tại của nó:

  • Có rất nhiều diễn viên vô dụng như Thực tế là bắt buộc (xem bình luận):

    if (e instanceof ServiceUnavailableException) {
        ServiceUnavailableException ex = (ServiceUnavailableException) e;
        rethrow(ex);
    }
    
  • Khi ném / trả lại các ngoại lệ mới, cái cũ bị loại bỏ; trong đoạn mã sau, a MailLoginNotAvailableExceptionkhông cho phép tôi biết đăng nhập nào không khả dụng, điều này bất tiện; hơn nữa, stacktraces sẽ không đầy đủ:

    private void rethrow(EntityAlreadyExistsException e)
        throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
        switch (e.getDetail()) {
            case LOGIN_ALREADY_TAKEN:
                throw new MailLoginNotAvailableException();
            case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
                throw new MailEmailAddressAlreadyForwardedToException();
        }
    }
    
  • Tại sao mã đầu tiên không ném các ngoại lệ chuyên biệt đó ngay từ đầu? Tôi nghi ngờ rằng nó rethrowđược sử dụng như một lớp tương thích giữa hệ thống con (gửi thư) và logic busineess (có thể mục đích là để ẩn các chi tiết thực hiện như ngoại lệ bị ném bằng cách thay thế chúng bằng các ngoại lệ tùy chỉnh). Ngay cả khi tôi đồng ý rằng sẽ tốt hơn nếu có mã không bị bắt, như được đề xuất trong câu trả lời của Pete Becker , tôi không nghĩ bạn sẽ có cơ hội xóa catchrethrowmã ở đây mà không cần tái cấu trúc lớn.


1
Các diễn viên không phải là vô dụng. Chúng được yêu cầu, bởi vì không có công văn động dựa trên loại đối số. Kiểu đối số phải được biết tại thời điểm biên dịch để gọi đúng phương thức.
Joe23

Trong logAndWrapphương thức của bạn, bạn có thể ném ngoại lệ thay vì trả lại, nhưng vẫn giữ kiểu trả về (Runtime)Exception. Bằng cách này, khối bắt có thể vẫn theo cách bạn đã viết (mà không cần ném mã không thể truy cập được). Nhưng nó có lợi thế bổ sung mà bạn không thể quên throw. Nếu bạn trả lại ngoại lệ và quên đi cú ném này sẽ dẫn đến ngoại lệ bị âm thầm nuốt chửng. Ném trong khi có kiểu trả về RuntimeException sẽ làm cho trình biên dịch hài lòng đồng thời tránh được các lỗi này. Đó là cách ổi Throwables.propagateđược thực hiện.
Joe23

@ Joe23 Cảm ơn bạn đã làm rõ về công văn tĩnh. Về việc ném các ngoại lệ bên trong hàm: bạn có nghĩa là lỗi có thể xảy ra mà bạn đang mô tả là một khối bắt mà gọi logAndWrapnhưng không có gì với ngoại lệ được trả về? Nó thật thú vị. Bên trong một hàm phải trả về một giá trị, trình biên dịch sẽ nhận thấy rằng có một đường dẫn không có giá trị nào được trả về, nhưng điều này hữu ích cho các phương thức không trả về bất cứ thứ gì. Cảm ơn một lần nữa.
coredump

1
Đó chính xác là những gì tôi nghĩ. Và chỉ để nhấn mạnh một lần nữa: Đó không phải là thứ tôi nghĩ ra và lấy tín dụng, mà lượm lặt từ nguồn ổi.
Joe23

@ Joe23 Tôi đã thêm một tài liệu tham khảo rõ ràng vào thư viện
coredump

52

rethrow(e);Hàm này vi phạm nguyên tắc nói rằng trong các trường hợp thông thường, một hàm sẽ trở lại, trong khi trong các trường hợp đặc biệt, một hàm sẽ đưa ra một ngoại lệ. Hàm này vi phạm nguyên tắc này bằng cách ném ngoại lệ trong trường hợp bình thường. Đó là nguồn gốc của tất cả sự nhầm lẫn.

Trình biên dịch giả định rằng hàm này sẽ trả về trong các trường hợp thông thường, theo như trình biên dịch có thể cho biết, việc thực thi có thể đạt đến cuối của retrieveUserMailConfigurationhàm, tại thời điểm đó, đó là lỗi không có returncâu lệnh. Việc RuntimeExceptionném ở đó được cho là để giảm bớt mối quan tâm này của trình biên dịch, nhưng đó là một cách làm khá vụng về. Một cách khác để ngăn chặn function must return a valuelỗi là thêm một return null; //to keep the compiler happytuyên bố, nhưng theo tôi thì điều đó cũng khó hiểu không kém.

Vì vậy, cá nhân, tôi sẽ thay thế điều này:

rethrow(e);

Với cái này:

report(e); 
throw e;

hoặc, tốt hơn nữa, (như đã đề xuất ,) với điều này:

throw reportAndTransform(e);

Do đó, luồng điều khiển sẽ được hiển thị rõ ràng đối với trình biên dịch, do đó, bản cuối cùng của bạn throw new RuntimeException("cannot reach here");sẽ không chỉ trở nên dư thừa mà còn không thể biên dịch được, vì nó sẽ được trình biên dịch gắn cờ là mã không thể truy cập được.

Đó là cách thanh lịch nhất và thực sự cũng đơn giản nhất để thoát khỏi tình huống xấu xí này.


1
-1 Đề xuất chia chức năng thành hai câu lệnh có thể gây ra lỗi lập trình do sao chép / dán sai; Tôi cũng hơi lo lắng về thực tế là bạn thay đổi nội dung câu hỏi của bạn để đề nghị throw reportAndTransform(e)một vài phút sau khi tôi đăng câu trả lời của riêng tôi. Có thể bạn đã sửa đổi câu hỏi của bạn một cách độc lập về nó, và tôi xin lỗi trước nếu nó là như vậy, nhưng nếu không, đưa ra một chút tín dụng là điều mọi người thường làm.
coredump

4
@coredump Tôi thực tế đã đọc đề xuất của bạn và thậm chí tôi đã đọc một câu trả lời khác thuộc tính gợi ý này cho bạn và tôi nhớ rằng tôi thực sự đã sử dụng chính xác một cấu trúc như vậy trong quá khứ, vì vậy tôi đã quyết định đưa nó vào câu trả lời của mình. Tôi nghĩ rằng nó không quá quan trọng để bao gồm một sự ghi nhận, bởi vì chúng tôi không nói về một ý tưởng ban đầu ở đây, nhưng nếu bạn gặp vấn đề với điều đó, tôi không muốn làm cho bạn thêm trầm trọng, vì vậy, sự quy kết hiện đang ở đó, hoàn thành với một liên kết. Chúc mừng.
Mike Nakis

Không phải là tôi có bằng sáng chế cho công trình này, điều này khá phổ biến; nhưng hãy tưởng tượng bạn viết một câu trả lời và không lâu sau đó, nó bị "đồng hóa" bởi một câu trả lời khác. Vì vậy, sự ghi nhận được nhiều đánh giá cao. Cảm ơn bạn.
coredump

3
Không có gì sai mỗi se với một chức năng không trở lại. Nó xảy ra mọi lúc, ví dụ, trong các hệ thống thời gian thực. Vấn đề cuối cùng là một lỗ hổng trong java ở chỗ ngôn ngữ phân tích và thực thi khái niệm chính xác của ngôn ngữ và ngôn ngữ không cung cấp một cơ chế nào đó để chú thích rằng một hàm không quay trở lại. Tuy nhiên, có những mẫu thiết kế có được xung quanh này như throw reportAndTransform(e). Nhiều mẫu thiết kế là bản vá lỗi thiếu tính năng ngôn ngữ. Đây là một trong số họ.
David Hammen

2
Mặt khác, nhiều ngôn ngữ hiện đại đủ phức tạp. Biến mọi mẫu thiết kế thành một tính năng ngôn ngữ cốt lõi sẽ làm chúng phát triển hơn nữa. Đây là một ví dụ tốt về mẫu thiết kế không thuộc ngôn ngữ cốt lõi. Như đã viết, nó được hiểu rõ không chỉ bởi trình biên dịch, mà còn bởi nhiều công cụ khác phải phân tích mã.
MSalters

7

throwlẽ đã được thêm vào để khắc phục lỗi "phương thức phải trả về giá trị" nếu không sẽ xảy ra - bộ phân tích luồng dữ liệu Java đủ thông minh để hiểu rằng không returncần thiết sau một throw, nhưng không phải sau rethrow()phương thức tùy chỉnh của bạn và không có @NoReturnchú thích mà bạn có thể sử dụng để sửa lỗi này.

Tuy nhiên, việc tạo một Ngoại lệ mới trong mã không thể truy cập có vẻ không cần thiết. Tôi chỉ đơn giản là viết return null, biết rằng nó thực sự không bao giờ xảy ra.


Bạn đúng rồi. Tôi đã kiểm tra những gì sẽ xảy ra nếu tôi bình luận ra throw new...dòng và nó phàn nàn về việc không có tuyên bố trở lại. Có phải là lập trình xấu? Có bất kỳ quy ước về việc thay thế một tuyên bố hoàn trả không thể truy cập? Tôi sẽ không trả lời câu hỏi này trong một thời gian để khuyến khích thêm đầu vào. Tuy nhiên cảm ơn câu trả lời của bạn.
Sok Pomaranczowy

3
@SokPomaranczowy Vâng, đó là chương trình tồi. rethrow()Phương pháp đó che giấu thực tế rằng việc catchlấy lại ngoại lệ. Tôi sẽ tránh làm điều gì đó như thế. Thêm vào đó, tất nhiên, rethrow()thực sự có thể thất bại trong việc lấy lại ngoại lệ, trong trường hợp có điều gì đó khác xảy ra.
Ross Patterson

26
Vui lòng không thay thế ngoại lệ bằng return null. Với ngoại lệ, nếu ai đó trong tương lai phá vỡ mã để dòng cuối cùng có thể truy cập được, nó sẽ rõ ràng ngay lập tức khi ném ngoại lệ. Nếu bạn thay vào đó return null, bây giờ bạn có một nullgiá trị nổi xung quanh trong ứng dụng của bạn không được mong đợi. Ai biết nơi nào trong ứng dụng NullPointerExceptionsẽ phát sinh? Trừ khi bạn gặp may mắn và nó là trung gian sau cuộc gọi đến chức năng này, sẽ rất khó để theo dõi vấn đề thực sự.
unolysampler

5
@MatthewRead Việc thực thi mã dự định không thể truy cập được là một lỗi nghiêm trọng, return nullrất có khả năng che dấu lỗi vì bạn không thể phân biệt các trường hợp khác mà nó trả về nulltừ lỗi này.
CodeInChaos

4
Hơn nữa, nếu hàm đã trả về null trong một số trường hợp và bạn cũng sử dụng trả về null trong trường hợp này, thì người dùng hiện có hai trường hợp có thể xảy ra khi họ nhận được trả về null. Đôi khi điều này không thành vấn đề, bởi vì trả về null có nghĩa là "không có đối tượng và tôi không xác định lý do tại sao", vì vậy, thêm một lý do có thể có tại sao tạo ra sự khác biệt nhỏ. Nhưng thường thêm sự mơ hồ là xấu, và điều đó chắc chắn có nghĩa là các lỗi ban đầu sẽ được xử lý như "một số trường hợp nhất định" thay vì ngay lập tức trông giống như "điều này bị phá vỡ". Thất bại sớm.
Steve Jessop

6

Các

throw new RuntimeException("cannot reach here");

tuyên bố làm rõ cho một NGƯỜI đọc mã đang xảy ra, vì vậy tốt hơn rất nhiều sau đó trả về null chẳng hạn. Nó cũng làm cho việc gỡ lỗi dễ dàng hơn nếu mã được thay đổi theo cách không mong muốn.

Tuy nhiên rethrow(e)có vẻ sai! Vì vậy, trong trường hợp của bạn, tôi nghĩ rằng tái cấu trúc mã là một lựa chọn tốt hơn. Xem các câu trả lời khác (tôi thích nhất của coredump) để biết cách sắp xếp mã của bạn.


4

Tôi không biết nếu có một quy ước.

Dù sao đi nữa, một mẹo khác sẽ là làm như vậy:

private <T> T rethrow(Exception exception) {
    // or whatever it actually does
    Log.e("Ouch! " + exception.getMessage());
    throw new CustomWrapperException(exception);
}

Cho phép điều này:

try {
     return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
     return rethrow(e);
}

Và không cần nhân tạo RuntimeExceptionnữa. Mặc dù rethrowkhông bao giờ thực sự trả về bất kỳ giá trị nào, nhưng nó đủ tốt cho trình biên dịch bây giờ.

Nó trả về một giá trị trong lý thuyết (chữ ký phương thức), và sau đó nó được miễn thực sự làm như vậy vì nó ném một ngoại lệ thay thế.

Vâng, nó có thể trông kỳ lạ, nhưng sau đó một lần nữa - ném một bóng ma RuntimeException, hoặc trả lại những con số không bao giờ được nhìn thấy trên thế giới này - đó cũng không hẳn là một thứ đẹp đẽ.

Phấn đấu để dễ đọc, bạn có thể đổi tên rethrowvà có một cái gì đó như:

} catch (Exception e) {
     return nothingJustRethrow(e);
}

2

Nếu bạn loại bỏ tryhoàn toàn khối thì bạn không cần rethrowhoặc throw. Mã này thực hiện chính xác giống như bản gốc:

public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
    return translate(mailManagementService.retrieveUserMailConfiguration(id));
}

Đừng để thực tế rằng nó đến từ các nhà phát triển dày dạn hơn đánh lừa bạn. Đây là mã thối, và nó xảy ra mọi lúc. Chỉ cần sửa nó.

EDIT: Tôi đọc sai rethrow(e)như chỉ đơn giản là ném lại ngoại lệ e. Nếu rethrowphương pháp đó thực sự làm một cái gì đó ngoài việc ném lại ngoại lệ, thì việc loại bỏ nó sẽ thay đổi ngữ nghĩa của phương pháp này.


Mọi người sẽ tiếp tục tin rằng các ký tự đại diện bắt được xử lý không có gì thực sự là xử lý ngoại lệ. Phiên bản của bạn bỏ qua giả định thiếu sót đó bằng cách không giả vờ xử lý cái mà nó không thể. Người gọi của lấyUserMailConfiguration () mong muốn nhận lại một cái gì đó. Nếu chức năng đó không thể trả lại một cái gì đó hợp lý, nó sẽ thất bại nhanh và lớn. Như thể các câu trả lời khác không có vấn đề gì với catch(Exception)mùi mã khó chịu.
msw

1
@msw - Mã hóa tôn giáo hàng hóa.
Pete Becker

@msw - mặt khác, nó cung cấp cho bạn một nơi để đặt điểm dừng.
Pete Becker

1
Đó là - nếu bạn thấy không có vấn đề gì khi loại bỏ toàn bộ phần logic. Phiên bản của bạn rõ ràng không làm điều tương tự như bản gốc, do đó, đã thất bại nhanh chóng và lớn tiếng. Tôi muốn viết các phương thức miễn phí như câu trả lời của bạn, nhưng khi làm việc với mã hiện có, chúng ta không nên phá vỡ hợp đồng với các bộ phận làm việc khác. Có thể những gì được thực hiện rethrowlà vô ích (và đây không phải là vấn đề ở đây), nhưng bạn không thể loại bỏ mã bạn không thích và nói nó tương đương với bản gốc khi rõ ràng là không phải. Có tác dụng phụ trong rethrowchức năng tùy chỉnh .
coredump

1
@ jpmc26 Cốt lõi của câu hỏi là về ngoại lệ "không thể đến đây", có thể phát sinh vì những lý do khác ngoài khối thử / bắt trước đó. Nhưng tôi đồng ý rằng khối này có mùi tanh và nếu câu trả lời này được diễn đạt cẩn thận hơn ban đầu, nó sẽ thu được nhiều sự ủng hộ hơn (tôi đã không downvote, btw). Chỉnh sửa cuối cùng là tốt.
coredump
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.