Checked vs. Unchecked Exceptions: The Debate Is Not Over

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

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

Давайте сначала объясню, как я понимаю исключения в объектно-ориентированном программировании. Затем я сравню свое понимание с “традиционным” подходом, и мы обсудим различия. Итак, сначала мое понимание.

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

Когда все идет по плану, метод просто сохраняет данные и возвращает управление. Когда что-то идет не так, он выбрасывает исключение Exception, и нам нужно что-то с этим сделать:

Когда метод указывает, что он throws исключение, я понимаю, что этот метод не безопасен. Иногда он может завершиться неудачей, и на мне лежит ответственность либо 1) обработать эту ситуацию, либо 2) также объявить себя небезопасным.

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

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

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

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

Согласно этой парадигме, я бы сказал, что мы должны:

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

  • Используйте только Exception, без каких-либо подтипов.

  • Всегда объявляйте только один тип исключения в блоке throws.

  • Никогда не ловите без повторного выбрасывания; подробнее об этом можно прочитать здесь.

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

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

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

У меня есть ответ на это. Введя проверяемые и непроверяемые исключения, разработчики Java пытались решить проблему методов, которые слишком сложны и запутаны. Когда метод слишком большой и делает слишком много вещей одновременно (нарушает принцип единственной ответственности), определенно лучше позволить нам “скрыть” некоторые исключения (т.е. непроверяемые). Но это не настоящее решение. Это всего лишь временный патч, который наносит нам больше вреда, чем пользы: методы продолжают расти по размеру и сложности.

Непроверяемые исключения - это ошибка в проектировании Java, а не проверяемые.

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

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

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

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

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

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

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

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

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

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

Джошуа Блох в Effective Java говорит, что “используйте проверяемые исключения для восстанавливаемых ситуаций и исключения времени выполнения для ошибок программирования”. Он имеет в виду нечто вроде этого:

В чем здесь отличие от известного анти-паттерна под названием Don’t Use Exceptions for Flow Control? Джошуа, с уважением к вам, вы ошибаетесь. В объектно-ориентированном программировании не существует такого понятия, как восстанавливаемое состояние. Исключение указывает на то, что выполнение цепочки вызовов методов прервано и пришло время подняться по цепочке и остановиться где-то. Но после исключения мы никогда не возвращаемся обратно.

Мы можем снова начать эту цепочку, но мы не возвращаемся после throw. Другими словами, мы ничего не делаем в блоке catch. Мы только сообщаем о проблеме и завершаем выполнение. Мы никогда не “восстанавливаемся!”

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

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-12-27 at 10:22

sixnines availability badge   GitHub stars