вторник, 23 мая 2017 г.

Checked vs. Unchecked Exceptions

Нужны ли вообще проверяемые исключения? Дискуссия окончена, не так ли? Но, не для меня. В то время как большинство объектно-ориентированных языков вообще не имеют их, и большинство программистов считают, что исключения являются ошибкой Java, я верю в противоположное: непроверенные исключения - это ошибка. Более того, я считаю, что несколько типов исключений - тоже плохая идея.

Позвольте мне сначала объяснить, как я понимаю исключения в объектно-ориентированном программировании. Затем я сравню свое понимание с «традиционным» подходом, и мы обсудим различия. Итак, мое понимание в первую очередь.

Предположим, что существует способ, который сохраняет некоторые двоичные данные в файл:

public void save(File file, byte[] data)
  throws Exception {
  // сохранять данные в файл
}
Когда все пойдет правильно, метод просто сохраняет данные и возвращает управление. Когда что-то не так, оно выбрасывает исключение, и мы должны что-то с этим сделать:
try {
  save(file, data);
} catch (Exception ex) {
  System.out.println(«Извините, мы не можем сохранить сейчас.»);
}
Когда метод говорит, что он выбрасывает исключение, я понимаю, что этот метод не безопасен. Иногда это может произойти сбой, и я обязан либо 1) отреагировать на эту ошибку, либо 2) заявить о своей небезопасности.

Я знаю, что каждый метод разработан с учетом принципа единой ответственности. Это является гарантией того, что если метод save () не сработает, это означает, что вся операция сохранения не может быть завершена. Если мне нужно знать, что было причиной этой неудачи, я развяжу исключение - прохождение стека цепочечных исключений и стека, инкапсулированных в ex.

Я никогда не использую исключения для управления потоком, а это значит, что я никогда не восстанавливаю ситуации, в которых возникают исключения. Когда возникает исключение, я позволяю ему плавать до самого высокого уровня приложения. Иногда я перебрасываю его, чтобы добавить в семантическую информацию большую цепочку. Поэтому для меня не имеет значения, в чем причина исключения, созданного функцией save (). Я просто знаю, что метод провалился. Этого достаточно для меня. Всегда.

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

Вот так я понимаю исключения.

Согласно этой парадигме, я бы сказал, что мы должны:
  • Всегда используйте проверенные исключения.
  • Никогда не бросать / использовать непроверенные исключения.
  • Используйте только исключение, без каких-либо подтипов.
  • Всегда объявлять один тип исключения в блоке throws.
  • Никогда не ловить без повторного броска; Читайте больше об этом здесь.

Эта парадигма расходится со многими другими статьями, которые я нашел по этому вопросу. Давайте сравним и обсудим.

Исключения времени выполнения и API

Oracle говорит, что некоторые исключения должны быть частью API (проверенные), в то время как некоторые являются исключениями во время выполнения и не должны быть частью этого (непроверенными). Они будут документированы в JavaDoc, но не в сигнатуре метода.

Я не понимаю логики здесь, и я уверен, что Java-дизайнеры тоже этого не понимают. Как и почему некоторые исключения важны, а другие нет? Почему некоторые из них заслуживают правильной позиции API в throws блоке сигнатуры метода, а другие нет? Каковы критерии?

У меня есть ответ. Введя checked и unchecked исключения, Java-разработчики пытались решить проблему слишком сложных и запутанных методов. Когда метод слишком велик и делает слишком много вещей одновременно (нарушает принцип единой ответственности), определенно лучше позволить нам сохранить некоторые исключения «скрытыми» (a.k.a. unchecked). Но это не реальное решение. Это лишь временный патч, который причиняет всем нам больше вреда, когда хорошие методы продолжают расти в размерах и сложности.

Unchecked исключения - это ошибка в дизайне Java, а не проверка.

Скрыть тот факт, что метод может не сработать в какой-то момент, является ошибкой. Именно это и делают Unchecked исключения.

Вместо этого мы должны сделать этот факт видимым. Когда метод делает слишком много вещей, будет слишком много точек отказа, и автор метода поймет, что что-то не так - метод не должен бросать исключения во многих ситуациях. Это приведет к рефакторингу. Существование Unchecked исключений приводит к беспорядку. Кстати, checked исключения вообще не существуют в Ruby, C #, Python, PHP и т.д. Это означает, что создатели этих языков понимают ООП даже меньше, чем авторы Java.

checked исключения слишком громкие

Другим распространенным аргументом против checked исключений является то, что они делают наш код более многословным. Мы должны поставить try / catch везде вместо того, чтобы сосредоточиться на основной логике. Божидар Божанов даже предлагает техническое решение этой многословной проблемы.

Опять же, я не понимаю эту логику. Если я хочу что-то сделать, когда не удается выполнить метод save (), я улавливаю исключение и как-нибудь справляюсь с ситуацией. Если я не хочу этого делать, я просто говорю, что мой метод также выбрасывает и не обращает внимания на обработку исключений. В чем проблема? Откуда многословность?

У меня есть ответ на это. Это происходит из-за существования Unchecked исключений. Мы просто не можем игнорировать отказ, потому что используемые нами интерфейсы не позволяют нам это делать. Это все. Например, класс Runnable, который широко используется для многопоточного программирования, имеет метод run (), который не должен ничего бросать. Вот почему мы всегда должны перехватывать все внутри метода и проверять checked исключения как Unchecked.

Если все методы во всех интерфейсах Java будут объявлены либо «безопасными» (ничего не выбрасывает), либо «небезопасными» (выбрасывает исключение), все станет логичным и понятным. Если вы хотите остаться «в безопасности», возьмите на себя ответственность за обработку отказов. В противном случае, будьте «небезопасными», и пусть ваши пользователи будут беспокоиться о безопасности.

Нет шума, очень чистый код и очевидная логика.

Неподходящая информация об экспорте

Некоторые говорят, что возможность помещать проверенное исключение в throws в сигнатуре метода вместо того, чтобы ловить его здесь и пересоздавать новый тип, побуждает нас иметь слишком много нерелевантных типов исключений в сигнатурах методов. Например, наш метод save () может объявить, что он может выкинуть OutOfMemoryException, даже если он, кажется, не имеет никакого отношения к распределению памяти. Но он действительно выделяет некоторую память, правильно? Таким образом, такое переполнение памяти может произойти во время операции сохранения файла.

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

Если мы действительно хотим сделать память приложения устойчивой к переполнению, мы представим некоторый диспетчер памяти, который будет иметь что-то вроде метода bigEnough (), который скажет нам, достаточно ли наша куча для следующей операции. Использование исключений в таких ситуациях - совершенно неподходящий подход к управлению исключениями в ООП.

Исправимые исключения

Джошуа Блох (Joshua Bloch) из Effective Java говорит «использовать checked исключения для восстанавливаемых условий и исключений во время выполнения (runtime) для ошибок программирования». Он имеет в виду что-то вроде этого:
try {
  save(file, data);
} catch (Exception ex) {
  // Мы не можем сохранить файл, но все в порядке
  // Давайте сделаем что-нибудь еще

}
Как это отличается от известного анти-шаблона под названием «Не использовать исключения для управления потоками»? Джошуа, при всем уважении, ты ошибаешься. В ООП нет таких условий, которые можно было бы восстановить. Исключение указывает, что выполнение цепочки вызовов от метода к методу нарушено, и пришло время подняться по цепочке и остановиться где-то. Но мы никогда не возвращаемся после исключения:
App#run()
  Data#update()
    Data#write()
      File#save() <-- Boom, здесь есть сбой, поэтому мы идем вверх
Мы можем начать эту цепочку снова, но мы не возвращаемся после броска. Другими словами, мы ничего не делаем в блоке catch. Мы сообщаем о проблеме и завершаем исполнение. Мы никогда не «выздоравливаем»!

Все аргументы против checked исключений не демонстрируют ничего, кроме серьезного непонимания объектно-ориентированного программирования их авторами. Ошибка в Java и во многих других языках заключается в существовании непроверенных исключений, а не проверенных.


Комментариев нет:

Отправить комментарий