Чому метод не повинен викидати кілька типів перевірених винятків?


47

Ми використовуємо SonarQube для аналізу нашого Java-коду, і воно має це правило (встановлене на критичне):

Публічні методи повинні містити не більше одного перевіреного винятку

Використання перевірених винятків змушує абонентів методу вирішувати помилки, або шляхом їх поширення, або шляхом їх обробки. Це робить ці винятки повністю частиною API методу.

Щоб зберегти складність для абонентів розумною, методи не повинні містити більше ніж один вид перевірених винятків. "

Ще трохи в Sonar є це :

Публічні методи повинні містити не більше одного перевіреного винятку

Використання перевірених винятків змушує абонентів методу вирішувати помилки, або шляхом їх поширення, або шляхом їх обробки. Це робить ці винятки повністю частиною API методу.

Щоб зберегти складність для абонентів розумною, методи не повинні містити більше ніж один вид перевірених винятків.

Наступний код:

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

має бути реконструйовано на:

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

Методи переосмислення не перевіряються цим правилом і дозволяється викидати кілька перевірених винятків.

Я ніколи не стикався з цим правилом / рекомендацією в своїх читаннях щодо поводження з винятками і намагався знайти деякі стандарти, дискусії тощо по цій темі. Єдине, що я виявив - це це з CodeRach: Скільки винятків має мати метод не більше?

Це добре прийнятий стандарт?


7
Що ти думаєш? Роз'яснення, яке ви цитували з SonarQube, здається розумним; у вас є підстави сумніватися?
Роберт Харві

3
Я написав багато коду, який містить декілька винятків і використовував багато бібліотек, які також викидають більше ніж один виняток. Крім того, у книгах / статтях про обробку винятків тема обмеження кількості викинутих винятків зазвичай не порушується. Але багато з прикладів показують, що метання / ловлення декількох дає чітке схвалення практики. Отже, я вважав це правило дивовижним і хотів провести більше досліджень щодо найкращих практик / філософії поводження з винятками порівняно з основними прикладами практичних завдань.
sdoca

Відповіді:


32

Розглянемо ситуацію, коли ви надали код:

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

Небезпека тут полягає в тому, що код, який ви пишете на дзвінок, delete()матиме вигляд:

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

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

Головне - не писати код, який змушує вас писати поганий код в іншому місці.

Правило, з яким ви стикаєтесь, є досить поширеним. У Checkstyle це є свої правила дизайну:

ThrowCount

Обмежує кидає висловлювання до визначеного рахунку (1 за замовчуванням).

Обґрунтування: Винятки становлять частину інтерфейсу методу. Оголошення методу для викидання занадто багатьох винятків з різними коренями робить обробку винятків обтяжливою і призводить до поганих практик програмування, таких як написання коду, як улов (Exception ex). Ця перевірка змушує розробників вводити винятки в таку ієрархію, що в найпростішому випадку абонент повинен перевірити лише один тип винятку, але будь-які підкласи можуть бути спеціально зафіксовані, якщо це необхідно.

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

І хоча ви можете робити це відповідно до мовного дизайну, і можуть бути випадки, коли це правильно робити, це ви повинні побачити і негайно піти "гм, чому я це роблю?" Це може бути прийнятним для внутрішнього коду, де кожен достатньо дисциплінований, щоб ніколи catch (Exception e) {}, але частіше за все я не бачив, як люди вирізали куточки, особливо у внутрішніх ситуаціях.

Не змушуйте людей, які користуються вашим класом, бажати писати неправильний код.


Я мушу зазначити, що важливість цього питання зменшується за допомогою Java SE 7 і пізніше, оскільки в одній заяві вилову можуть входити декілька винятків ( Ловля декількох типів винятків та повторне вилучення винятків з покращеною перевіркою типів від Oracle).

З Java 6 і раніше ви мали б код, який виглядав так:

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;
}

або

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

Жоден із цих варіантів із Java 6 не є ідеальним. Перший підхід порушує сухість . Кілька блоків роблять те саме, знову і знову - один раз для кожного винятку. Ви хочете записати виняток і повторно скинути його? Гаразд. Однакові рядки коду для кожного винятку.

Другий варіант гірший з кількох причин. По-перше, це означає, що ви ловите всі винятки. Нульовий вказівник потрапляє туди (і не повинен). Крім того, ви Повторне викидання , Exceptionщо означає , що метод підпис була б , deleteSomething() throws Exceptionякий тільки робить безлад далі вгору по стеку , як люди з допомогою коду тепер змушені до catch(Exception e).

Для Java 7 це не так важливо, оскільки ви можете:

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

Крім того, тип перевірки , якщо один робить зловити типи винятків кидають:

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

Перевірка типу визнає, що eможе бути лише типів IOExceptionабо SQLException. Я все ще не надто захоплений використанням цього стилю, але він не викликає такого поганого коду, як це було в Java 6 (де це змусить вас, щоб підпис методу був надкласом, який поширюються винятки).

Незважаючи на всі ці зміни, багато інструментів статичного аналізу (Sonar, PMD, Checkstyle) все ще застосовують посібники зі стилю Java 6. Це не погано. Я схильний погоджуватися з попередженням, що вони все ще виконуються, але ви можете змінити їх пріоритет на основний або другорядний відповідно до того, як ваша команда надає їм пріоритет.

Якщо виключення повинні бути перевірені або незареєстрований ... це питання г р е з т дебати , які можна легко знайти незліченну кількість повідомлень в блозі , приймаючи кожну сторону аргументу. Однак якщо ви працюєте з перевіреними винятками, вам, мабуть, слід уникати перекидання декількох типів, принаймні під Java 6.


Приймаючи це як відповідь, оскільки він відповідає на моє запитання про те, що це добре прийнятий стандарт. Однак я все ще читаю різні дискусії про / кон, починаючи з посилання Panzercrisis, наданого у його відповіді, щоб визначити, яким буде мій стандарт.
sdoca

"Перевірка типу визнає, що e може бути лише типів IOException або SQLException.": Що це означає? Що відбувається, коли викид іншого типу викидається foo.delete()? Це все-таки спіймано і перекинуто?
Джорджіо

@Giorgio Це буде помилка часу компіляції, якщо deleteв цьому прикладі буде викинуто перевірене виключення, відмінне від IOException або SQLException. Ключовим моментом, який я намагався зробити, є те, що метод, який викликає rethrowException, все одно отримає тип винятку на Java 7. У Java 6 все це зливається до загального Exceptionтипу, що робить статичний аналіз та інші кодери сумними.

Я бачу. Мені це здається трохи заплутаним. Мені буде інтуїтивніше забороняти catch (Exception e)і змушувати це бути catch (IOException e)або catch (SQLException e)замість цього.
Джорджіо

@Giorgio Це додатковий крок вперед від Java 6, щоб спробувати полегшити написання хорошого коду. На жаль, варіант написання поганого коду буде у нас надовго. Пам'ятайте, що catch(IOException|SQLException ex)замість цього може зробити Java 7 . Але, якщо ви просто збираєтеся повторно скинути виняток, дозволяючи перевіряльнику типів поширювати фактичний тип wile винятків, спрощуючи код, це не погано.

22

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

Скажімо, у нас є метод, який отримує дані з постійності, і це стійкість - це набір файлів. Оскільки ми маємо справу з файлами, ми можемо мати FileNotFoundException:

public String getData(int id) throws FileNotFoundException

Тепер ми змінимо вимоги, і наші дані надходять із бази даних. Замість FileNotFoundException(оскільки ми не маємо справи з файлами), ми зараз кидаємо SQLException:

public String getData(int id) throws SQLException

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

Інверсія залежності залежить від того, що ми не повинні кидати жодного з цих винятків, оскільки вони розкривають деталі внутрішнього впровадження, над якими ми працюємо, щоб інкапсулювати. Код виклику повинен знати, який тип наполегливості ми використовуємо, коли він дійсно повинен просто хвилюватися, чи можна отримати запис. Натомість нам слід викинути виняток, який передає помилку на тому самому рівні абстракції, який ми виявляємо через наш API:

public String getData(int id) throws InvalidRecordException

Тепер, якщо ми змінимо внутрішню реалізацію, ми можемо просто загорнути цей виняток у InvalidRecordExceptionта передати його уздовж (або не перегорнути, а просто кинути новий InvalidRecordException). Зовнішній код не знає і не цікавить, який тип стійкості використовується. Це все інкапсульовано.


Що стосується єдиної відповідальності, нам потрібно думати про код, який викидає безліч непов’язаних винятків. Скажімо, у нас є такий метод:

public Record parseFile(String filename) throws IOException, ParseException

Що можна сказати про цей метод? З підпису ми можемо сказати, що він відкриває файл і аналізує його. Коли ми бачимо сполучення типу "і" або "чи" в описі методу, ми знаємо, що він робить більше ніж одне; вона несе більше відповідальності . Методами з більш ніж однією відповідальністю важко керувати, оскільки вони можуть змінитися, якщо зміниться будь-яка відповідальність. Натомість нам слід розбити методи, щоб вони мали єдину відповідальність:

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

Ми відповідали за зчитування файлу з відповідальності за аналіз даних. Одним із побічних ефектів цього є те, що тепер ми можемо передавати будь-які дані String до даних аналізу з будь-якого джерела: в пам'яті, файлі, мережі тощо. parseЗараз ми також можемо простіше перевірити, оскільки нам не потрібен файл на диску проводити тести проти.


Іноді насправді є два (або більше) винятки, які ми можемо викинути з методу, але якщо ми дотримуємось SRP та DIP, часи, з якими ми стикаємося з цією ситуацією, стають рідшими.


Я цілком погоджуюся з обговоренням винятків нижчого рівня згідно з вашим прикладом. Ми робимо це регулярно і викидаємо відхилення MyAppExceptions. Один із прикладів, коли я кидаю кілька винятків, - це при спробі оновлення запису в базі даних. Цей метод кидає RecordNotFoundException. Однак запис можна оновлювати лише у певному стані, тому метод також видає InvalidRecordStateException. Я думаю, що це дійсно і надає абоненту цінну інформацію.
sdoca

@sdoca Якщо ваш updateметод настільки атомний, як ви можете його зробити, і винятки знаходяться на належному рівні абстракції, то так, це здається, що вам потрібно кинути два різних типи винятків, оскільки є два виняткових випадки. Це має бути мірою того, скільки винятків можна кинути, а не цих (іноді довільних) лінійних правил.
cbojar

2
Але якщо у мене є метод, який зчитує дані з потоку і аналізує їх у процесі проходження, я не можу розбити ці дві функції, не прочитавши весь потік у буфер, який може бути непростим. Крім того, код, який вирішує, як правильно поводитись з винятками, може бути окремим від коду, який виконує читання та аналіз. Коли я пишу код для читання та розбору, я не знаю, як код, що викликає мій код, може хотіти обробляти будь-який тип винятку, тому мені потрібно пропустити їх обох.
користувач3294068

+1: мені дуже подобається ця відповідь, тим більше, що вона вирішує проблему з точки зору моделювання. Часто не варто вживати ще одну ідіому (як catch (IOException | SQLException ex)), оскільки справжня проблема полягає в моделі / дизайні програми.
Джорджіо

3

Я пам’ятаю, що трохи повозився з цим, граючи з Java на той час, але я не усвідомлював різниці між перевіреним та неперевіреним, поки не прочитав ваше запитання. Я знайшов цю статтю в Google досить швидко, і вона вступає в деякі очевидні суперечки:

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

При цьому, одна з проблем, яку цей хлопець згадував із перевіреними винятками, полягає в тому, що (і я особисто зіткнувся з цим з самого початку з Java), якщо ви постійно додаєте купу перевірених винятків до throwsпунктів у своїх деклараціях методів, а не тільки чи потрібно вводити набагато більше кодового коду, щоб підтримувати його під час переходу до методів вищого рівня, але він також просто збільшує безлад і порушує сумісність, коли ви намагаєтеся ввести більше типів винятків до методів нижчого рівня. Якщо ви додаєте перевірений тип винятку до методу нижчого рівня, вам доведеться запустити через код і відрегулювати ще кілька декларацій методу.

Одним із пунктів пом'якшення, згаданим у статті, - і автору це не подобалося особисто - було створити виняток із базового класу, обмежити свої throwsпункти лише його використанням, а потім просто підняти підкласи з нього всередині. Таким чином ви можете створювати нові перевірені типи винятків без необхідності виконувати весь код.

Автору цієї статті, можливо, це не сподобалось, але це має ідеальний сенс в моєму особистому досвіді (особливо, якщо ви все-таки зможете знайти, які всі підкласи є), і я думаю, що саме тому вам даються поради: обмежте все одним перевіреним типом винятків кожен. Більше того, що згадані вами поради фактично дозволяють використовувати кілька перевірених типів винятків у непублічних методах, що має ідеальний сенс, якщо це їхній мотив (або навіть інакше). Якщо це все-таки лише приватний метод або щось подібне, ви не будете пробігати половину кодової бази, змінюючи одну дрібницю.

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


2
Чому б просто не оголосити кидки Throwable, а зробити це з ним, а не придумувати всю власну ієрархію?
Дедуплікатор

@Deduplicator Це певна причина, що автор також не сподобався ідеї; він просто подумав, що ти можеш також скористатись усіма перевіреними, якщо ти це збиратимешся. Але якщо той, хто використовує API (можливо, ваш колега), має перелік усіх підкласових винятків, що походять з вашого базового класу, я можу побачити трохи користі, принаймні давши їм знати, що всі очікувані винятки є в межах певного набору підкласів; тоді, якщо вони відчують, що один з них "легше обробляти", ніж інші, вони не будуть настільки схильні відмовитись від конкретного для цього обробника.
Panzercrisis

Причина перевірених винятків загалом - погана карма проста: вони вірусні, що заражають кожного користувача. Визначення навмисно найширшої специфікації - це лише спосіб сказати, що всі винятки не перевірені, і, таким чином, уникнути безладу. Так, документування того, з чим ви можете звернутися, є хорошою ідеєю для документації : Просто знати, які винятки можуть виникнути з функції, має суворо обмежене значення (окрім жодного взагалі / можливо, одного, але Java це все одно не дозволяє) .
Дедуплікатор

1
@Deduplicator Я не схвалюю цю ідею, і я не обов'язково її підтримую. Я просто говорю про напрямок, з якого, можливо, надходила порада з ОП.
Panzercrisis

1
Дякуємо за посилання Це було чудовим початковим місцем для читання на цю тему.
sdoca

-1

Метання декількох перевірених винятків має сенс, коли є кілька розумних справ.

Наприклад, скажімо, у вас є метод

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

це порушує правило виключення pne, але має сенс.

На жаль, як правило, такі методи є

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

Тут мало шансів для абонента зробити щось конкретне на основі типу винятку. Тож, якщо ми хочемо підштовхнути його до усвідомлення того, що ці методи МОГУТЬ, а іноді йдуть НЕ БУДУТИ, кидаючи просто SomethingMightGoWrongException - це одно і краще.

Звідси правило не більше одного перевіреного винятку.

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

Sidenote: Щось насправді може помилитися майже скрізь, тож можна подумати про використання? розширює RuntimeException, але є різниця між "ми всі робимо помилки" і "це говорить про зовнішню систему, і вона іноді ВИМОЖЕ, вирішувати це".


1
"кілька розумних справ" важко відповідати srp - цей пункт був викладений у попередній відповіді
gnat

Це робить. Ви можете виявити одну проблему в одній функції (обробка одного аспекту) та іншу проблему в іншій (також обробка одного аспекту), викликану однією функцією, що обробляє одну річ, а саме викликати ці дві функції. І той, хто телефонує, може в одному шарі спробувати зловити (в одній функції) ВІДКРИТИ одну проблему і передати вгору іншу і вирішити цю проблему теж самотужки.
user470365
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.