Статьи

Проверенные исключения я люблю тебя, но ты должен идти

Давным-давно Java создала эксперимент под названием Проверенные исключения, вы знаете, что должны объявлять исключения или перехватывать их. С тех пор ни один другой язык (я знаю) не решил копировать эту идею, но каким-то образом разработчики Java влюблены в проверенные исключения. Здесь я собираюсь «попытаться» убедить вас, что проверенные исключения, даже если на первый взгляд они кажутся хорошей идеей, на самом деле вообще не являются хорошей идеей:

Эмпирическое доказательство

Начнем с наблюдения за вашей кодовой базой. Посмотрите код и скажите, какой процент блоков catch перебрасывает или печатает? Я думаю, что это в высоких 90-х. Я бы сказал, что 98% блоков catch не имеют смысла, так как они просто печатают ошибку или перебрасывают исключение, которое позже будет напечатано как ошибка. Причина этого очень проста. Большинство исключений, таких как FileNotFoundException, IOException и т. Д., Являются признаком того, что мы, как разработчики, пропустили угловой случай. Исключения используются для информирования нас о том, что мы, как разработчики, все испортили. Поэтому, если бы у нас не было проверенных исключений, то было бы выброшено исключение, и метод main распечатал бы его, и мы с этим покончили бы (по желанию мы могли бы перехватить в main все исключения и записать их в журнал, если мы являемся сервером).

Проверенные исключения вынуждают меня писать блоки catch, которые не имеют смысла: больше кода, труднее читать и больше шансов, что я испорчу логику переброса и сожду исключение.

Потерянный в Шуме

Теперь давайте посмотрим на 2-5% блоков захвата, которые не отбрасываются, и там происходит действительно интересная логика. Эти интересные кусочки полезной и важной информации теряются в шуме, так как мой глаз обучен скользить по блокам улова. Я бы предпочел иметь код, в котором улов будет указывать, обращать внимание, здесь происходит нечто интересное, а не просто повторение. Теперь, если бы у нас не было проверенных исключений, вы бы написали свой код без перехвата, протестировали его (вы правильно тестировали?) И поняли, что в этих обстоятельствах исключение — это throw и с ним работать. В таком случае забывание писать блок catch ничем не отличается от забывания писать блок else оператора if. Мы не проверили, если и никто не пропустил их, так почему мы должны сказать разработчикам, что FileNotFound может произойти.Что, если разработчик точно знает, что это не может произойти, поскольку он только что поместил туда файл, и поэтому такое исключение будет означать, что ваша файловая система только что исчезла, а ваше приложение не подходит для этого.

Проверенное исключение заставляет меня снимать ловушку, так как большинство из них просто отбрасывают, что дает мне шанс пропустить что-то важное

Недостижимый код

Я люблю сначала писать тесты и внедрять их как следствие. В такой ситуации у вас всегда должен быть 100% охват, так как вы пишете только то, о чем просят тесты. Но ты не! Это меньше 100%, потому что проверенные исключения заставляют вас писать блоки catch, которые невозможно выполнить. Проверьте этот код:

bytesToString(byte[] bytes) {
  ByteArrayOutputStream out = new ByteArrayOutputStream();
  try {
    out.write(bytes);
    out.close()
    return out.toSring();
  } catch (IOException e) {
    // This can never happen!
    // Should I rethrow? Eat it? Print Error?
  }
}

ByteArrayOutputStream никогда не вызовет IOException! Вы можете просмотреть его реализацию и увидеть, что это правда! Так почему же вы заставляете меня ловить фантомное исключение, которое никогда не может произойти и для которого я не могу написать тест? В результате я не могу требовать 100% покрытия из-за вещей, которые находятся вне моего контроля.

Проверенные исключения создают мертвый код, который никогда не будет выполнен.

Вы не любите замыкания

У Java нет замыканий, но есть шаблон посетителей. Позвольте мне объяснить на конкретном примере. Я создавал пользовательский загрузчик классов, и мне нужно переопределить метод load () для MyClassLoader, который в некоторых случаях выдает исключение ClassNotFoundException. Я использую библиотеку ASM, которая позволяет мне проверять байт-коды Java. ASM работает так, что это шаблон посетителей, я пишу посетителей, и, когда ASM анализирует байт-коды, он вызывает определенные методы в моей реализации посетителей. Один из моих посетителей, изучающий байт-коды, решает, что все не так, и должен вызвать исключение ClassNotFondException, которое, согласно договору загрузчика классов, должно быть сгенерировано. Но сейчас у нас проблема. В стеке у нас есть MyClassLoader -> ASMLibrary -> MyVisitor.MyVisitor хочет выдать исключение, которое ожидает MyClassLoader, но не может, поскольку проверено исключение ClassNotFoundException и ASMLibrary не объявляет его (и не должно). Поэтому я должен сгенерировать исключение RuntimeClassNotFoundException из MyVisitor, которое может проходить через ASMLibrary, которую MyClassLoader может перехватывать и перебрасывать как ClassNotFoundException.

Проверенное исключение мешает функциональному программированию.

Потерянная верность

Предположим, что пакет java.sql будет реализован с полезным исключением, таким как SqlDuplicateKeyExceptions и SqlForeignKeyViolationException и т. Д. (Мы можем пожелать), и предположим, что эти исключения проверены (какие они есть). Мы говорим, что пакет SQL имеет высокую точность исключения, поскольку каждое исключение связано с очень специфической проблемой. Теперь предположим, что у нас есть те же настройки, что и раньше, когда между нами и пакетом SQL есть какой-то другой слой, который может либо повторно объявить все исключения, либо, что более вероятно, выдать его. Давайте рассмотрим пример, Hibernate — это объектно-реляционный-database-mapper, который означает, что он преобразует ваши строки SQL в объекты Java. Таким образом, в стеке у вас есть MyApplication -> Hibernate -> SQL. Здесь Hibernate старается скрыть тот факт, что вы говорите с SQL, поэтому он генерирует HibernateExceptions вместо SQLExceptions.И здесь кроется проблема. Ваш код знает, что в Hibernate есть SQL, и поэтому он мог бы обработать SqlDuplicateException некоторым полезным способом, например, показать ошибку пользователю, но Hibernate был вынужден перехватить исключение и перезапустить его как универсальное исключение HibernateException. Мы прошли путь от SqlException с высокой точностью до HibernateException с низкой точностью. А так MyApplication ничего не может сделать. Теперь Hibernate мог генерировать HibernateDuplicateKeyException, но это означает, что Hibernate теперь имеет ту же иерархию исключений, что и SQL, и мы дублируем усилия и повторяем себя.но Hibernate был вынужден перехватить исключение и перевести его как универсальное исключение HibernateException. Мы прошли путь от SqlException с высокой точностью до HibernateException с низкой точностью. А так MyApplication ничего не может сделать. Теперь Hibernate мог генерировать HibernateDuplicateKeyException, но это означает, что Hibernate теперь имеет ту же иерархию исключений, что и SQL, и мы дублируем усилия и повторяем себя.но Hibernate был вынужден перехватить исключение и перевести его как универсальное исключение HibernateException. Мы прошли путь от SqlException с высокой точностью до HibernateException с низкой точностью. А так MyApplication ничего не может сделать. Теперь Hibernate мог генерировать HibernateDuplicateKeyException, но это означает, что Hibernate теперь имеет ту же иерархию исключений, что и SQL, и мы дублируем усилия и повторяем себя.

Повторное использование проверенных исключений приводит к потере верности и, следовательно, снижает вероятность того, что вы сможете сделать что-то полезное с этим исключением позже.

Вы ничего не можете сделать в любом случае

В большинстве случаев, когда исключением является бросок, восстановление отсутствует. Мы показываем типичную ошибку пользователю и регистрируем исключение, чтобы зафиксировать ошибку и убедиться, что это исключение больше не повторится. Поскольку более 90% исключений составляют ошибки в нашем коде, и все, что мы делаем, это регистрируем, почему мы вынуждены перебрасывать его снова и снова.

Редко когда что-либо полезное может быть сделано, когда происходит проверенное исключение, в большинстве случаев мы умираем с ошибкой, поэтому сделайте так, чтобы код по умолчанию работал без дополнительной типизации.

Как я справляюсь с кодом

Вот моя стратегия борьбы с Java:

  • Всегда перехватывайте все проверенные исключения в источнике и перебрасывайте их как LogRuntimeException.

    • Моя не проверенная исключительная ситуация во время выполнения, которая говорит, что мне все равно, просто зарегистрируйте ее.
    • Здесь я потерял исключение верности.
  • Все мои методы не объявляют никаких исключений
  • Когда я обнаружил, что мне нужно иметь дело с конкретным исключением, я возвращаюсь к источнику, в котором было сгенерировано LogRuntimeException, и меняю его на <Specific> RuntimeException (это реже, чем вы думаете)

    • Я восстанавливаю точность исключения только там, где это необходимо.
  • Чистый эффект заключается в том, что когда вы сталкиваетесь с предложением «поймай-лови», тебе лучше обратить внимание, так как там происходят интересные вещи.

    • Очень мало пробных мозолей, код гораздо легче читать.
    • Очень близко к 100% тестированию, так как в моих блоках перехвата нет мертвого кода.

 

С http://misko.hevery.com