Чи кидання нових RuntimeExceptions в недоступному коді є поганим стилем?


31

Мені було призначено підтримувати додаток, написане деякий час тому більш кваліфікованими розробниками. Я натрапив на цей фрагмент коду:

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

Мені цікаво, якщо кидання RuntimeException("cannot reach here")виправдане. Я, мабуть, пропускаю щось очевидне, знаючи, що цей фрагмент коду походить від більш досвідченого колеги.
РЕДАКТУВАННЯ: Ось орган перезарядки, на який згадуються деякі відповіді. Я вважав це не важливим у цьому питанні.

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
це технічно доступно, якщо rethrowфактично throwне виняток. (що може трапитися одного дня, якщо впровадження зміниться)
njzk2

34
В сторону, AssertionError може бути семантично кращим вибором, ніж RuntimeException.
JvR

1
Фінальний кидок є зайвим. Якщо ви включите Findbugs або інші інструменти статичного аналізу, вони позначать ці типи рядків для видалення.
Бон-Амі

7
Тепер, коли я бачу решту коду, я думаю, що, можливо, ви повинні змінити "більш кваліфікованих розробників" на "більш старших розробників". Code Review із задоволенням пояснить, чому.
JvR

3
Я намагався створити гіпотетичні приклади того, чому винятки страшні. Але ніщо, що я коли-небудь робив, не містить свічки до цього.
Пол Дрейпер

Відповіді:


36

По-перше, дякую за те, що ви поставили запитання і показали нам, що rethrowробить. Отже, насправді, це перетворення винятків із властивостями у більш дрібнозернисті класи винятків. Детальніше про це пізніше.

Оскільки я спочатку не відповідав на головне запитання, тут ідеться: так, загалом поганий стиль кидати винятки з виконання часу у недоступному коді; вам краще використовувати твердження, а ще краще, уникайте проблеми. Як уже зазначалося, компілятор тут не може бути впевнений, що код ніколи не виходить з try/catchблоку. Ви можете повторно змінити код, скориставшись тим, що ...

Помилки - значення

(Не дивно, що це добре відомо в дорозі )

Давайте скористаємося більш простим прикладом, який я використовував перед вашою редагуванням: тому уявіть, що ви щось записуєте та створюєте виняток для обгортки, як у відповіді Конрада . Давайте назвемо це logAndWrap.

Замість того, щоб кидати виняток як побічний ефект logAndWrap , ви можете дозволити йому виконувати свою роботу як побічний ефект і змусити його повернути виняток (принаймні, той, що вказаний у введенні). Вам не потрібно використовувати дженерики, а лише основні функції:

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

Тоді ти throw чітко, і ваш компілятор радий:

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

Що робити, якщо забудеш throw?

Як пояснено в коментарі Joe23 , захисний спосіб програмування для того, щоб завжди викидати виняток, полягав би в тому, що явно робимо в throw new CustomWrapperException(exception)кінці logAndWrap, як це робиться Guava.Throwables . Таким чином, ви знаєте, що виняток буде кинутий, і ваш аналізатор типу задоволений. Однак ваші власні винятки мають бути вивіреними винятками, що не завжди можливо. Крім того, я б оцінив ризик, який програвач не вистачає, щоб написати throwдуже низьким: розробник повинен забути це а навколишній метод нічого не повинен повертати, інакше компілятор виявить відсутність повернення. Це цікавий спосіб боротьби з типовою системою, і вона працює.

Ретроу

Фактичний також rethrowможе бути записаний як функція, але у мене є проблеми з його поточною реалізацією:

  • Існує безліч марних ролей на кшталт Фактично потрібні ролі (див. Коментарі):

    if (e instanceof ServiceUnavailableException) {
        ServiceUnavailableException ex = (ServiceUnavailableException) e;
        rethrow(ex);
    }
  • При киданні / поверненні нових винятків старий відкидається; у наступному коді a MailLoginNotAvailableExceptionне дозволяє мені знати, який вхід недоступний, що незручно; до того ж, стек траксів буде неповним:

    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();
        }
    }
  • Чому вихідний код не викидає ці спеціалізовані винятки в першу чергу? Я підозрюю, що rethrowвикористовується як шар сумісності між підсистемою (розсилка) та логікою Busineess (можливо, наміром є приховати деталі реалізації, як викинуті винятки, замінивши їх на власні винятки). Навіть якщо я погоджуюся, що було б краще мати код без лову, як це запропоновано у відповіді Піта Беккера , я не думаю, що у вас не буде можливості видалити тут catchі rethrowкод без великих рефакторинга.


1
Касти не марні. Вони необхідні, оскільки немає динамічної відправки на основі типу аргументу. Тип аргументу повинен бути відомий під час компіляції, щоб викликати правильний метод.
Joe23

У своєму logAndWrapметоді ви можете кинути виняток замість повернення, але зберігати тип повернення (Runtime)Exception. Таким чином блок лову може залишитися таким, яким ви його написали (без кидання недосяжного коду). Але це має додаткову перевагу, яку ви не можете забути throw. Якщо ви повернете виняток і забудете кидок, це призведе до того, що виняток проковтнеться. Закидання під час повернення типу RuntimeException зробить компілятора щасливим, а також уникне цих помилок. Саме так реалізується Guava Throwables.propagate.
Joe23

@ Joe23 Дякую за роз’яснення щодо статичної відправки. Щодо викидання винятків усередині функції: ви маєте на увазі, що можлива помилка, яку ви описуєте, є блоком вилову, який викликає, logAndWrapале нічого не робить із поверненим винятком? Це цікаво. Всередині функції, яка повинна повернути значення, компілятор зауважив, що існує шлях, який не повертає значення, але це корисно для методів, які нічого не повертають. Знову дякую.
coredump

1
Саме це і я. І лише ще раз наголошу на тому, що я не придумав і взяв на себе кредити, а зібрався з джерела Guava.
Jo23 23

@ Joe23 Я додав явну посилання на бібліотеку
coredump

52

Ця rethrow(e);функція порушує принцип, який говорить про те, що за звичайних обставин функція повернеться, тоді як за виняткових обставин функція кине виняток. Ця функція порушує цей принцип, кидаючи виняток за звичайних обставин. Це джерело всієї плутанини.

Компілятор припускає, що ця функція повернеться за звичайних обставин, тому, наскільки компілятор може сказати, виконання може дійти до кінця retrieveUserMailConfigurationфункції, і тоді помилка не має returnзаяви. RuntimeException, Кинутих там , як передбачається , щоб полегшити цю проблему компілятора, але це досить незграбний спосіб зробити це. Ще один спосіб запобігання function must return a valueпомилки - додавання return null; //to keep the compiler happyзаяви, але це настільки ж незграбно, на мою думку.

Тож особисто я би замінив це:

rethrow(e);

з цим:

report(e); 
throw e;

або, ще краще, (як запропоновано coredump ) з цим:

throw reportAndTransform(e);

Таким чином, потік керування стане очевидним для компілятора, тож ваш фінал throw new RuntimeException("cannot reach here");став би не лише надмірним, а фактично навіть не компільованим, оскільки його буде позначено компілятором як недоступний код.

Це найелегантніший і насправді найпростіший спосіб вийти з цієї потворної ситуації.


1
-1 Запропонувати розділити функцію на два оператори може ввести помилки програмування через неправильну копію / вставлення; Я також трохи хвилююся з приводу того, що ви відредагували своє запитання throw reportAndTransform(e)через пару хвилин після того, як я опублікував власну відповідь. Можливо, ви модифікували питання, незалежно від цього, і я заздалегідь вибачаюсь, якщо це так, але в іншому випадку давати трохи кредиту - це те, що зазвичай роблять люди.
coredump

4
@coredump Я насправді прочитав вашу пропозицію, і я навіть прочитав ще одну відповідь, яка приписує вам цю пропозицію, і я пам’ятав, що я фактично використовував саме таку конструкцію в минулому, тому вирішив включити її у свою відповідь. Я подумав, що це не так важливо, щоб включити атрибуцію, тому що ми не говоримо тут про оригінальну ідею, але якщо ви поставитеся до цього, я не хочу вас погіршувати, тому атрибуція зараз є, доповнити посиланням. Ура.
Майк Накіс

Справа не в тому, що у мене є патент на цю конструкцію, який є досить поширеним; але уявіть, що ви пишете відповідь, і невдовзі вона "засвоюється" іншою. Тож атрибуція дуже цінується. Дякую.
coredump

3
Там нічого поганого в собі з функцією , яка повертається. Це відбувається постійно, наприклад, в системах реального часу. Кінцева проблема полягає в недоліці Java в тому, що мова аналізує та виконує концепцію правильності мов, але мова не дає механізму якось зауважувати, що функція не повертається. Однак є дизайнерські зразки, які обходять це таке throw reportAndTransform(e). У багатьох моделях дизайну є виправлення, відсутні функції мови. Це одна з них.
Девід Хаммен

2
З іншого боку, багато сучасних мов є досить складними. Перетворення кожного дизайнерського зразка в основну мову мови ще більше посилить їх. Це хороший приклад схеми дизайну, яка не належить до основної мови. Як написано, це добре розуміється не лише компілятором, але й багатьма іншими інструментами, які повинні розбирати код.
MSalters

7

throw, Ймовірно , додали , щоб обійти «метод повинен повертати значення» помилку , яка в іншому випадку мала б місце - дані Java потік аналізатор досить розумний, щоб зрозуміти , що ні returnтреба після throw, але не після вашого користувацького rethrow()методу, і немає @NoReturnанотації які ви могли використовувати для виправлення цього.

Тим не менш, створення нового Винятку в недоступному коді видається зайвим. Я б просто писав return null, знаючи, що це насправді ніколи не буває.


Ти правий. Я перевірив, що буде, якщо я прокоментую throw new...рядок, і він скаржиться на відсутність заяви про повернення. Це погано програмування? Чи є умова щодо заміни недоступної декларації про повернення? Я залишаю це питання без відповіді на деякий час, щоб заохочувати більше вкладу. Тим не менше дякую за вашу відповідь.
Sok Pomaranczowy

3
@SokPomaranczowy Так, це погано програмування. Цей rethrow()метод приховує той факт, що catchце відновлює виняток. Я б уникнув зробити щось подібне. Плюс, звичайно, rethrow()насправді може не вдатися до викиду винятку, і в цьому випадку щось інше станеться.
Росс Паттерсон

26
Будь ласка, не замінюйте виняток на return null. За винятком випадків, якщо хтось у майбутньому порушить код, щоб останній рядок дещо став доступним, йому буде відразу зрозуміло, коли викид буде викинуто. Якщо ви замість цього return null, тепер nullу вашій програмі є значення, яке не очікувалося. Хто знає, де в додатку NullPointerExceptionвиникне? Якщо вам не пощастить, і це не буде опосередковано після виклику цієї функції, буде дуже важко відстежити справжню проблему.
unholysampler

5
@MatthewRead Виконання коду, який повинен бути недосяжним, є серйозною помилкою, return nullцілком ймовірно, що маскує помилку, оскільки ви не можете відрізнити інші випадки, коли він повертається nullз цієї помилки.
CodesInChaos

4
Крім того, якщо функція вже повертає null за певних обставин, а ви також використовуєте null return при цій обставині, тепер у користувачів є два можливі сценарії, в яких вони можуть бути, коли вони отримують нульове повернення. Іноді це не має значення, оскільки нульове повернення вже означає "немає об'єкта, і я не вказую чому", тому додавання ще однієї можливої ​​причини, чому мало значення. Але часто додавати неоднозначність погано, і це, безумовно, означає, що помилки спочатку трактуються як "певні обставини", а не одразу виглядають як "це порушено". Невдача рано.
Стів Джессоп

6

The

throw new RuntimeException("cannot reach here");

заява дає зрозуміти ОСОБІ, що читає код, що відбувається, тому набагато краще, ніж повернути нуль, наприклад. Це також спрощує налагодження, якщо код буде змінено несподівано.

Однак rethrow(e)просто здається неправильним! Тож у вашому випадку я думаю, що рефакторинг коду є кращим варіантом. Дивіться інші відповіді (мені подобається coredump найкраще) щодо способів впорядкування вашого коду.


4

Я не знаю, чи є конвенція.

У будь-якому разі, ще одна хитрість - це зробити так:

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

Дозволяючи це:

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

І жоден штучний більше RuntimeExceptionне потрібен. Хоча rethrowніколи фактично не повертає жодного значення, це досить добре для компілятора зараз.

Він теоретично повертає значення (підпис методу), і тоді він звільнений від фактичного виконання так, як він замість цього викидає виняток.

Так, це може виглядати дивно, але знову ж таки - кинути фантома RuntimeExceptionабо повернути нулі, які ніколи не побачать у цьому світі - це теж не сама річ краси.

Прагнучи до читабельності, ви можете перейменувати rethrowта мати щось на кшталт:

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

2

Якщо ви видалите tryблок повністю, то вам не потрібно rethrowабо throw. Цей код робить точно так само, як оригінал:

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

Не дозволяйте обдурити те, що це надходить від більш досвідчених розробників. Це кодова гниль, і вона відбувається постійно. Просто виправте це.

EDIT: Я неправильно читаю rethrow(e)як просто перекидання винятку e. Якщо цей rethrowметод насправді робить щось інше, ніж повторно викидати виняток, то позбавлення від нього змінює семантику цього методу.


Люди й надалі вірять, що улов підстановки, який нічого не обробляє, насправді є обробкою виключень. Ваша версія нехтує цим хибним припущенням, не претендуючи на те, що справляється з тим, чого не може. Абонент retrieveUserMailConfiguration () розраховує отримати щось назад. Якщо ця функція не може повернути щось розумне, вона повинна вийти з ладу швидко і голосно. Так, ніби інші відповіді не бачили жодної проблеми, з catch(Exception)якою має одіозний кодовий запах.
msw

1
@msw - кодування культового культу.
Піт Бекер

@msw - з іншого боку, це дає вам місце для встановлення точки перерви.
Піт Бекер

1
Це так, як якщо ви не бачите жодної проблеми, яка б відкинула цілу частину логіки. Ваша версія явно не робить те саме, що оригінал, який, до речі, вже швидко і голосно виходить з ладу. Я вважаю за краще писати безвідмовні методи, як той у вашій відповіді, але працюючи з існуючим кодом, ми не повинні розривати договори з іншими робочими частинами. Можливо, те, що робиться в цьому, rethrowє марним (і тут справа не в цьому), але ви не можете просто позбутися коду, який вам не подобається, і сказати, що це рівнозначно оригіналу, коли цього явно немає. У користувацькій rethrowфункції є побічні ефекти .
coredump

1
@ jpmc26 Суть питання полягає у виключенні "не можна дістатися сюди", яке може виникнути з інших причин, окрім попереднього блоку спробу / лову. Але я погоджуюсь, що блок пахне рибкою, і якби ця відповідь була більш ретельно сформульована спочатку, вона отримала б набагато більше результатів (я не знижував, BTW). Остання редакція хороша.
coredump
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.