Существует много известных лучших практик по обработке исключений в изоляции. Я достаточно хорошо знаю, что нужно делать, а что нет, но все становится сложнее, когда дело доходит до лучших практик или шаблонов в больших средах. «Бросай рано, лови поздно», - слышал я много раз, и это все еще смущает меня.
Зачем мне бросать рано и ловить поздно, если на низкоуровневом уровне выдается исключение нулевого указателя? Почему я должен ловить это на более высоком уровне? Мне не имеет смысла отлавливать низкоуровневое исключение на более высоком уровне, например на бизнес-уровне. Кажется, это нарушает заботы каждого слоя.
Представьте себе следующую ситуацию:
У меня есть сервис, который рассчитывает фигуру. Чтобы вычислить показатель, сервис обращается к репозиторию, чтобы получить необработанные данные, и к некоторым другим сервисам, чтобы подготовить расчет. Если что-то пошло не так на уровне извлечения данных, почему я должен выбросить DataRetrievalException на более высокий уровень? В отличие от этого, я бы предпочел заключить исключение в значимое исключение, например, в CalculationServiceException.
Зачем бросать рано, зачем ловить поздно?
источник
Ответы:
По моему опыту, лучше всего генерировать исключения в том месте, где происходят ошибки. Вы делаете это, потому что именно в этот момент вы больше всего знаете о том, почему возникло исключение.
Поскольку исключение раскручивает резервные копии слоев, перехват и перебрасывание - хороший способ добавить дополнительный контекст к исключению. Это может означать создание другого типа исключения, но при этом следует включать исходное исключение.
В конце концов, исключение достигнет уровня, на котором вы сможете принимать решения о потоке кода (например, запросить у пользователя действие). Это точка, где вы должны наконец обработать исключение и продолжить нормальное выполнение.
С практикой и опытом работы с вашей кодовой базой становится довольно легко судить, когда добавлять дополнительный контекст к ошибкам, и где, на самом деле, наиболее целесообразно фактически обрабатывать ошибки.
Поймать → Ретроу
Сделайте это там, где вы можете добавить полезную информацию, которая избавит разработчика от необходимости прорабатывать все уровни, чтобы понять проблему.
Поймать → Ручка
Сделайте это, когда вы сможете принять окончательное решение о том, что является подходящим, но другим потоком выполнения через программное обеспечение.
Поймать → Ошибка возврата
Несмотря на то, что существуют ситуации, когда это уместно, перехват исключений и возвращение вызывающей стороне значения ошибки следует рассмотреть для рефакторинга в реализацию Catch → Rethrow.
источник
NullPointerException
? Почему бы не проверитьnull
и не сгенерировать исключение (может бытьIllegalArgumentException
) раньше, чтобы вызывающий точно знал, кудаnull
поступило плохое ?» Я полагаю, что это было бы то, что предложила бы часть «бросить рано».Вы хотите вызвать исключение как можно скорее, потому что это облегчает поиск причины. Например, рассмотрим метод, который может завершиться с определенными аргументами. Если вы проверили аргументы и потерпели неудачу в самом начале метода, вы сразу знаете, что ошибка в вызывающем коде. Если вы подождете, пока аргументы понадобятся, прежде чем потерпеть неудачу, вы должны следить за выполнением и выяснить, есть ли ошибка в вызывающем коде (неверный аргумент) или метод имеет ошибку. Чем раньше вы выбросите исключение, тем ближе оно к его основной причине и тем легче будет понять, где что-то пошло не так.
Причина, по которой исключения обрабатываются на более высоких уровнях, заключается в том, что нижние уровни не знают, как следует поступить с ошибкой. Фактически, может быть несколько подходящих способов обработки одной и той же ошибки в зависимости от того, что является вызывающим кодом. Взять, к примеру, открытие файла. Если вы пытаетесь открыть файл конфигурации, а его нет, игнорирование исключения и продолжение настройки по умолчанию могут быть подходящим ответом. Если вы открываете личный файл, который жизненно важен для выполнения программы, и его почему-то не хватает, возможно, единственный вариант - закрыть программу.
Упаковка исключений в правильные типы является чисто ортогональной задачей.
источник
Другие суммировали довольно хорошо, почему бросать рано . Позвольте мне сконцентрироваться на том, зачем ловить позднюю часть вместо этого, для которой я не видел удовлетворительного объяснения моего вкуса.
ТАК ПОЧЕМУ ИСКЛЮЧЕНИЯ?
Похоже, что возникла путаница, почему исключения существуют в первую очередь. Позвольте мне поделиться здесь большим секретом: причина исключений и их обработка ... АБСТРАКЦИЯ .
Вы видели такой код:
Это не то, как исключения должны быть использованы. Код, подобный приведенному выше, существует в реальной жизни, но он скорее аберрация и действительно исключение (каламбур). Например, определение деления , даже в чистой математике, является условным: всегда «код вызывающей стороны» должен обрабатывать исключительный случай нуля, чтобы ограничить входную область. Это ужасно. Это всегда боль для звонящего. Тем не менее, для таких ситуаций естественным путем является паттерн check-then-do :
В качестве альтернативы вы можете использовать полную команду в стиле ООП, например:
Как видите, код вызывающей стороны несет бремя предварительной проверки, но не выполняет обработки исключений после. Если
ArithmeticException
когда-либо приходит от вызоваdivide
илиeval
, то именно ВЫ должны обрабатывать исключения и исправлять ваш код, потому что вы забылиcheck()
. По тем же причинам пойматьNullPointerException
почти всегда неправильно.Теперь есть некоторые люди , которые говорят , что они хотят видеть исключительные случаи в / метод сигнатуры функции, то есть явно продлить выходной домен . Именно они предпочитают проверенные исключения . Конечно, изменение домена вывода должно заставить любой код прямого вызова адаптироваться, и это действительно будет достигнуто с проверенными исключениями. Но вам не нужны исключения для этого! Именно поэтому у вас есть
Nullable<T>
общие классы , классы регистра , алгебраические типы данных и типы союзов . Некоторые OO люди могут даже предпочесть возвращатьсяnull
для простых ошибок, таких как это:Технически исключения могут быть использованы для целей, подобных описанным выше, но здесь есть смысл: исключений для такого использования не существует . Исключения составляют про абстракция. Исключение составляют косвенные указания. Исключения позволяют расширять «конечный» домен, не нарушая прямых клиентских контрактов и откладывая обработку ошибок до «где-то еще». Если ваш код генерирует исключения, которые обрабатываются в прямых вызывающих программах одного и того же кода, без каких-либо промежуточных уровней абстракции, то вы делаете это НЕПРАВИЛЬНО
КАК ПОЗДАТЬ ПОЗДНО?
Итак, мы здесь. Я изложил свой способ показать, что использование исключений в вышеупомянутых сценариях - это не то, как исключения должны использоваться. Однако существует подлинный вариант использования, где абстракция и косвенность, предлагаемые обработкой исключений, являются обязательными. Понимание такого использования поможет также понять рекомендацию с опозданием на вылов .
Этот вариант использования: Программирование против абстракций ресурсов ...
Да, бизнес-логика должна быть запрограммирована против абстракций , а не конкретных реализаций. Код «разводки» IOC верхнего уровня создаст конкретные реализации абстракций ресурса и передаст их бизнес-логике. Здесь нет ничего нового. Но конкретные реализации этих абстракций ресурсов могут потенциально генерировать свои собственные специфические для реализации исключения , не так ли?
Так кто же может обработать эти специфичные для реализации исключения? Можно ли вообще обрабатывать какие-либо специфичные для ресурса исключения в бизнес-логике? Нет, это не так. Бизнес-логика запрограммирована против абстракций, что исключает знание этих специфических для реализации деталей исключений.
«Ага!», Вы можете сказать: «Но именно поэтому мы можем создавать подклассы исключений и создавать иерархии исключений» (см. Мистер Спринг !). Позвольте мне сказать вам, что это ошибка. Во-первых, в каждой разумной книге об ООП говорится, что конкретное наследование является плохим, однако этот основной компонент JVM, обработка исключений, тесно связан с конкретным наследованием. По иронии судьбы, Джошуа Блох, возможно, не написал бы свою книгу «Эффективное Java» прежде, чем он смог бы получить опыт работы с рабочей JVM, не так ли? Это скорее книга «извлеченных уроков» для следующего поколения. Во-вторых, и что еще более важно, если вы поймали исключение высокого уровня, то как вы собираетесь его обрабатывать?
PatientNeedsImmediateAttentionException
: мы должны дать ей таблетку или ампутировать ее ноги !? Как насчет оператора switch для всех возможных подклассов? Там идет ваш полиморфизм, там идет абстракция. Вы поняли.Так, кто может обращаться с исключениями конкретного ресурса? Это должен быть тот, кто знает конкременты! Тот, кто создал ресурс! Код "проводка" конечно! Проверь это:
Бизнес-логика закодирована против абстракций ... НЕТ ОБРАБОТКИ БЕТОННЫХ РЕСУРСОВ!
Между тем где-то еще конкретные реализации ...
И, наконец, код подключения ... Кто обрабатывает конкретные исключения ресурсов? Тот, кто знает о них!
Теперь терпите меня. Приведенный выше код является упрощенным. Вы можете сказать, что у вас есть корпоративное приложение / веб-контейнер с несколькими областями управляемых ресурсов контейнера IOC, и вам нужны автоматические повторные попытки и повторная инициализация ресурсов области сеанса или запроса и т. Д. Логика разводки в областях более низкого уровня может быть предоставлена абстрактным фабрикам для создавать ресурсы, поэтому не зная точных реализаций. Только области более высокого уровня действительно будут знать, какие исключения могут генерировать эти ресурсы более низкого уровня. Теперь держись!
К сожалению, исключения допускают только косвенное обращение к стеку вызовов, и разные области действия с разным количеством элементов обычно работают в нескольких разных потоках. Нет способа общаться через это, за исключением. Нам нужно что-то более мощное здесь. Ответ: асинхронная передача сообщений . Поймать каждое исключение в корне области нижнего уровня. Ничего не игнорируйте, не позволяйте ничему ускользнуть. Это закроет и удалит все ресурсы, созданные в стеке вызовов текущей области. Затем распространяйте сообщения об ошибках в области выше, используя очереди / каналы сообщений в процедуре обработки исключений, пока не достигнете уровня, на котором известны конкреции. Это парень, который знает, как справиться с этим.
СУММА СУММАРУМ
Таким образом, согласно моей интерпретации, ловить поздно означает ловить исключения в наиболее удобном месте, где вы больше не нарушаете абстракцию . Не лови слишком рано! Поймать исключения на уровне, где вы создаете конкретные исключения, выбрасывая экземпляры абстракций ресурса, уровень, который знает сокращения абстракций. Слой проводки.
НТН. Удачного кодирования!
источник
WrappedFirstResourceException
илиWrappedSecondResourceException
требование "проводного" уровня, чтобы заглянуть внутрь этого исключения, чтобы увидеть основную причину проблемы ...FailingInputResource
исключение будет результатом операции сin1
. На самом деле, я думаю, что во многих случаях правильный подход состоит в том, чтобы уровень проводки передавал объект обработки исключений, и чтобы бизнес-уровень включал объект,catch
который затем вызываетhandleException
метод этого объекта . Этот метод может перебрасывать или предоставлять данные по умолчанию, или выдавать подсказку «Abort / Retry / Fail» и позволить оператору решать, что делать и т. Д. В зависимости от того, что требуется приложению.UnrecoverableInternalException
, как код ошибки HTTP 500.doMyBusiness
метод. Это было сделано для краткости, и вполне возможно сделать его более динамичным. ТакойHandler
класс может быть создан с некоторыми ресурсами ввода / вывода и иметьhandle
метод, который получает класс, реализующийReusableBusinessLogicInterface
. Затем вы можете объединить / настроить для использования различных реализаций обработчика, ресурсов и бизнес-логики на уровне проводки где-то над ними.Чтобы правильно ответить на этот вопрос, давайте сделаем шаг назад и зададим еще более фундаментальный вопрос.
Почему у нас есть исключения в первую очередь?
Мы бросаем исключения, чтобы вызывающий наш метод знал, что мы не можем сделать то, что нам было предложено. Тип исключения объясняет, почему мы не могли делать то, что хотели.
Давайте посмотрим на некоторый код:
Этот код, очевидно, может генерировать исключение нулевой ссылки, если
PropertyB
равно null. В этом случае мы могли бы сделать две вещи, чтобы «исправить» ситуацию. Мы могли бы:Создание PropertyB здесь может быть очень опасным. По какой причине этот метод должен создавать PropertyB? Конечно, это нарушит принцип единственной ответственности. По всей вероятности, если PropertyB здесь не существует, это означает, что что-то пошло не так. Метод вызывается для частично сконструированного объекта, или PropertyB был установлен равным нулю. Создавая здесь PropertyB, мы могли бы скрывать гораздо более серьезную ошибку, которая может укусить нас позже, такую как ошибка, приводящая к повреждению данных.
Если вместо этого мы позволим всплыть нулевой ссылке, мы сообщим разработчику, который вызвал этот метод, как можно скорее, что-то пошло не так. Важное предварительное условие вызова этого метода было пропущено.
Таким образом, в действительности, мы бросаем рано, потому что это разделяет наши проблемы намного лучше. Как только произошла ошибка, мы сообщаем об этом разработчикам.
Почему мы «задерживаемся поздно» - это отдельная история. Мы действительно не хотим ловить поздно, мы действительно хотим поймать, как только мы знаем, как правильно решить проблему. Иногда это будет пятнадцатью уровнями абстракции позже, а иногда это будет в точке создания.
Дело в том, что мы хотим перехватить исключение на уровне абстракции, что позволяет нам обрабатывать исключение в точке, где у нас есть вся информация, необходимая для правильной обработки исключения.
источник
if(PropertyB == null) return 0;
Бросьте, как только увидите что-то стоящее, чтобы не ставить объекты в недопустимое состояние. Это означает, что, если нулевой указатель был передан, вы проверяете его заранее и бросаете NPE, прежде чем у него появится шанс опуститься до низкого уровня.
Поймайте, как только вы узнаете, что нужно сделать, чтобы исправить ошибку (обычно это не то, куда вы добавляете, иначе вы можете просто использовать if-else), если был передан неверный параметр, то слой, предоставивший параметр, должен иметь дело с последствиями ,
источник
Действующее бизнес-правило: «Если программное обеспечение более низкого уровня не может рассчитать значение, тогда ...»
Это может быть выражено только на более высоком уровне, в противном случае программное обеспечение более низкого уровня пытается изменить свое поведение, основываясь на своей собственной корректности, которая заканчивается только узлом.
источник
Прежде всего, исключения для исключительных ситуаций. В вашем примере, никакие цифры не могут быть рассчитаны, если исходные данные отсутствуют, поскольку они не могут быть загружены.
Исходя из моего опыта, хорошей практикой является абстрагирование исключений при прохождении вверх по стеку. Обычно точки, в которых вы хотите это сделать, - это случай, когда исключение пересекает границу между двумя слоями.
Если при сборе необработанных данных на уровне данных произошла ошибка, создайте исключение, чтобы уведомить того, кто запросил данные. Не пытайтесь обойти эту проблему здесь. Сложность обработки кода может быть очень высокой. Также уровень данных отвечает только за запрос данных, а не за обработку ошибок, возникающих при этом. Это то, что означает «бросить рано» .
В вашем примере ловящий слой - это сервисный слой. Сам сервис представляет собой новый уровень, расположенный над уровнем доступа к данным. Таким образом, вы хотите поймать исключение там. Возможно, ваш сервис имеет некоторую инфраструктуру отработки отказа и пытается запросить данные из другого хранилища. Если это также не удается, оберните исключение в то, что понимает вызывающий сервис (если это веб-сервис, это может быть ошибкой SOAP). Установите исходное исключение как внутреннее исключение, чтобы последующие слои могли регистрировать именно то, что пошло не так.
Ошибка службы может быть отслежена уровнем, вызывающим службу (например, UI). И это то, что означает «поймать поздно» . Если вы не можете обработать исключение на нижнем уровне, сбросьте его. Если самый верхний слой не может обработать исключение, обработайте его! Это может включать ведение журнала или его представление.
Причина, по которой вы должны перебрасывать исключения (как описано выше, заключая их в более общие исключения), заключается в том, что пользователь, скорее всего, не сможет понять, что произошла ошибка, потому что, например, указатель указывает на недопустимую память. И ему все равно. Он заботится только о том, чтобы эта цифра не могла быть рассчитана службой, и это та информация, которая должна быть ему показана.
Идя дальше, вы можете (в идеальном мире) полностью исключить
try
/catch
код из пользовательского интерфейса. Вместо этого используйте глобальный обработчик исключений, который может понять исключения, которые могут быть выброшены нижними уровнями, записать их в некоторый журнал и обернуть их в объекты ошибок, которые содержат значимую (и, возможно, локализованную) информацию об ошибке. Эти объекты могут быть легко представлены пользователю в любой форме (окна сообщений, уведомления, тосты сообщений и т. Д.).источник
Как правило, создание исключений на ранней стадии является хорошей практикой, поскольку вы не хотите, чтобы нарушенные контракты проходили через код дальше, чем это необходимо. Например, если вы ожидаете, что определенный параметр функции будет положительным целым числом, вы должны применить это ограничение в точке вызова функции, а не ждать, пока эта переменная не будет использована где-то еще в стеке кода.
Я не могу комментировать, потому что у меня есть свои правила, и они меняются от проекта к проекту. Однако я стараюсь разделить исключения на две группы. Один предназначен только для внутреннего использования, а другой - только для внешнего использования. Внутреннее исключение перехватывается и обрабатывается моим собственным кодом, а внешнее исключение предназначено для обработки любым кодом, вызывающим меня. Это в основном форма перехвата вещей позже, но не совсем, потому что она дает мне возможность отклоняться от правила, когда это необходимо во внутреннем коде.
источник