The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:
Нужны ли нам вообще проверяемые исключения? Дискуссия завершена, не так ли? Но не для меня. В то время как большинство объектно-ориентированных языков их не имеют, и большинство программистов считает проверяемые исключения ошибкой Java, я верю в обратное - непроверяемые исключения - ошибка. Более того, я считаю, что несколько типов исключений - тоже плохая идея.
Давайте сначала разберемся, как я понимаю исключения в объектно-ориентированном программировании. Затем я сравню свое понимание с “традиционным” подходом, и мы обсудим различия. Итак, сначала мое понимание.
Предположим, есть метод, который сохраняет некоторые двоичные данные в файл:
public void save(File file, byte[] data)
throws Exception {
// save data to the file
}
Когда все идет по плану, метод просто сохраняет данные и возвращает управление. Когда что-то идет не так, он выбрасывает исключение Exception
, и нам нужно что-то с этим делать:
try {
save(file, data);
} catch (Exception ex) {
System.out.println("Sorry, we can't save right now.");
}
Когда метод указывает, что он throws
исключение, я понимаю, что метод не является безопасным. Он может иногда завершаться неудачно, и это моя ответственность либо 1) обработать эту неудачу, либо 2) объявить себя также небезопасным.
Я знаю, что каждый метод разработан с принципом одной ответственности. Это гарантирует мне, что если метод save()
не выполнится, это означает, что весь процесс сохранения не может быть завершен. Если мне нужно узнать причину этой неудачи, я буду разрывать цепочку исключения - просматривать стек цепочки исключений и трассировки стека, инкапсулированные в ex
.
Я никогда не использую исключения для управления потоком, что означает, что я никогда не восстанавливаю ситуации, когда возникают исключения. Когда возникает исключение, я позволяю ему подниматься до самого верхнего уровня приложения. Иногда я заново выбрасываю его, чтобы добавить больше семантической информации в цепочку. Поэтому мне не важно, что стало причиной возникновения исключения, выброшенного save()
. Я просто знаю, что метод не сработал. Мне этого достаточно. Всегда.
По той же причине мне не нужно различать между различными типами исключений. Мне просто не нужна такая иерархия типов. Исключение
достаточно для меня. Опять же, потому что я не использую исключения для управления потоком выполнения.
“Так я понимаю исключения.”
Согласно данной парадигме, я бы сказал, что мы должны:
Всегда используйте проверяемые исключения.
Никогда не выбрасывайте/используйте непроверяемые исключения.
Используйте только
Exception
, без каких-либо подтипов.Всегда объявляйте один тип исключения в блоке
throws
.Никогда не ловите без повторного выбрасывания; подробнее об этом можно узнать здесь.
Эта парадигма отличается от многих других статей, которые я нашел на эту тему. Давайте сравним и обсудим.
Runtime vs. API Exceptions
Oracle говорит, что некоторые исключения должны быть частью API (проверяемые), в то время как некоторые являются исключениями времени выполнения и не должны быть его частью (непроверяемые). Они будут задокументированы в JavaDoc, но не в сигнатуре метода.
Я не понимаю логики здесь, и уверен, что дизайнеры Java тоже не понимают ее. Как и почему некоторые исключения важны, а другие - нет? Почему некоторые из них заслуживают соответствующего положения в throws
блоке сигнатуры метода, а другие - нет? Каковы критерии?
У меня есть ответ. Введение проверяемых и непроверяемых исключений в Java было попыткой решить проблему методов, которые слишком сложны и запутаны. Когда метод слишком большой и выполняет слишком много действий одновременно (нарушает принцип единственной ответственности), нам определенно лучше позволить “скрыть” некоторые исключения (так называемые непроверяемые). Но это не настоящее решение. Это всего лишь временный патч, который наносит больше вреда, чем пользы – методы продолжают расти как по размеру, так и по сложности.
Непроверяемые исключения - это ошибка в проектировании Java, а не проверяемые.
Скрытие того факта, что метод может в какой-то момент завершиться с ошибкой, является ошибкой. Именно это делают непроверяемые исключения.
Вместо этого нам следует сделать этот факт видимым. Когда метод делает слишком много вещей, возникает слишком много точек отказа, и автор метода понимает, что что-то не так - метод не должен генерировать исключения в таких множественных ситуациях. Это приведет к рефакторингу. Наличие неотслеживаемых исключений ведет к беспорядку. Кстати, в Ruby, C#, Python, PHP и т.д. вообще не существует проверяемых исключений. Это означает, что создатели этих языков понимают ООП даже меньше, чем авторы Java.
Checked Exceptions Are Too Noisy
Ещё одним распространённым аргументом против проверяемых исключений является то, что они делают наш код более громоздким. Нам приходится использовать конструкцию try/catch
повсюду вместо того, чтобы сосредоточиться на основной логике. Божидар Божанов даже предлагает техническое решение для этой проблемы громоздкости.
Опять же, я не понимаю эту логику. Если я хочу сделать что-то, когда метод save()
завершается с ошибкой, я ловлю исключение и как-то обрабатываю ситуацию. Если я не хочу этого делать, я просто говорю, что мой метод также “бросает” исключение и не обращаю внимания на обработку исключений. В чем проблема? Откуда берется избыточность?
У меня тоже есть ответ здесь. Он основан на наличии непроверяемых исключений. Мы просто не всегда можем игнорировать ошибки, потому что используемые нами интерфейсы этого не позволяют. Вот и все. Например, класс Runnable
, который широко используется для многопоточного программирования, имеет метод run()
, который не должен ничего выбрасывать. Вот почему мы всегда должны перехватывать все исключения внутри метода и перевыбрасывать проверяемые исключения в виде непроверяемых.
Если бы все методы во всех интерфейсах Java были объявлены либо как “безопасные” (не генерируют исключения), либо как “небезопасные” (генерируют исключение Exception), все стало бы логичным и понятным. Если вы хотите оставаться “безопасными”, возьмите на себя ответственность за обработку ошибок. В противном случае, будьте “небезопасными” и позвольте вашим пользователям заботиться о безопасности.
Без шума, очень чистый код и очевидная логика.
Inappropriately Exposed Implementation Details
Некоторые говорят, что возможность поместить проверяемое исключение в throws
в сигнатуре метода, вместо того чтобы перехватывать его здесь и повторно выбрасывать новый тип, побуждает нас иметь слишком много несущественных типов исключений в сигнатурах методов. Например, наш метод save()
может объявить, что он может выбросить OutOfMemoryException
, даже если это, кажется, не имеет никакого отношения к выделению памяти. Но ведь он выделяет какую-то память, верно? Таким образом, такое переполнение памяти может произойти во время операции сохранения файла.
Опять же, я не понимаю логику этого аргумента. Если все исключения проверяются и у нас нет нескольких типов исключений, мы просто выбрасываем Exception
везде, и все. Зачем нам вообще заботиться о типе исключения? Если мы не используем исключения для управления потоком, мы этого не будем делать.
Если мы действительно хотим сделать наше приложение устойчивым к переполнению памяти, мы введем некоторый менеджер памяти, который будет иметь что-то вроде метода bigEnough()
, который скажет нам, достаточно ли нашей кучи для следующей операции. Использование исключений в таких ситуациях является полностью неподходящим подходом к управлению исключениями в ООП.
Recoverable Exceptions
Джошуа Блох в книге Effective Java говорит, что “следует использовать проверяемые исключения для восстанавливаемых условий и исключения времени выполнения для ошибок программирования”. Он имеет в виду примерно следующее:
try {
save(file, data);
} catch (Exception ex) {
// We can't save the file, but it's OK
// Let's move on and do something else
}
Как это отличается от известного анти-паттерна под названием Не используйте исключения для управления потоком? Джошуа, с всем уважением, ты не прав. В ООП нет такого понятия как восстанавливаемые условия. Исключение указывает на то, что выполнение цепочки вызовов от метода к методу прервано и пора подниматься вверх по цепочке и остановиться где-то. Но мы никогда не возвращаемся назад после исключения:
App#run()
Data#update()
Data#write()
File#save() <-- Boom, there's a failure here, so we go up
Мы можем начать эту цепочку снова, но мы не возвращаемся после throw
. Другими словами, мы ничего не делаем в блоке catch
. Мы только сообщаем о проблеме и завершаем выполнение. Мы никогда не “восстанавливаемся”!
All arguments against checked exceptions demonstrate nothing but a serious misunderstanding of object-oriented programming by their authors. The mistake in Java and in many other languages is the existence of unchecked exceptions, not checked ones.
Translated by ChatGPT gpt-3.5-turbo/35 on 2023-09-09 at 05:51