Статьи

Как бороться с исключениями

У меня недавно была беседа с другом, который является относительно молодым, но очень умным разработчиком программного обеспечения. Она спросила меня об обработке исключений. Вопросы указывали на советы и хитрости, и их список определенно есть. Но я сторонник контекста и мотивации того, как мы пишем программное обеспечение, поэтому я решил написать свои мысли об исключениях с такой точки зрения.
Исключения в программировании (с использованием Java в качестве этапа для нашей истории) используются для уведомления нас о том, что возникла проблема во время выполнения нашего кода. Исключения составляют особую категорию классов. Что делает их особенными, так это то, что они расширяют класс Exception, который, в свою очередь, расширяет класс Throwable. Будучи реализациями Throwable, мы можем «выбросить» их, когда это необходимо. Итак, как может произойти исключение? Экземпляры классов исключений вызываются либо из JVM, либо из раздела кода с использованием оператора throw. Это как, но почему?

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

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

Попытка объяснить суть этой проблемы разработки: метод / запрос запускается, чтобы что-то сделать, но он может потерпеть неудачу — как мы можем лучше всего уведомить вызывающего абонента, что он потерпел неудачу? Как мы передаем информацию о том, что произошло? Как мы помогаем клиенту решить, что делать дальше? Проблема с использованием исключений состоит в том, что мы «сдаемся», а не только это; мы делаем это «взрывным» способом, и клиенты / абоненты наших служб должны справляться с беспорядком .

Поэтому мой первый совет, когда речь идет об исключениях, поскольку они являются плохим событием — старайтесь избегать их . В разделах программного обеспечения, находящихся под вашим контролем, реализуйте дизайн, который затрудняет возникновение ошибок. Вы можете использовать функции вашего языка, которые поддерживают это поведение. Я считаю, что наиболее распространенным исключением в Java является исключение NullPointerException, и дополнительные функции могут помочь нам избежать их. Давайте рассмотрим, что мы хотим получить сотрудника с указанным идентификатором:

1
2
3
public Optional<Employee> tryGetEmployee(String employeeId) {
    return Optional.ofNullable(employeeService.getEmployee(employeeId));
}

Теперь намного лучше. Но помимо возможностей нашего языка, мы можем спроектировать наш код таким образом, чтобы затруднять возникновение ошибок. Если мы рассмотрим метод, который может принимать только положительные целые числа в качестве входных данных, мы можем настроить наш код, так что крайне маловероятно, что клиенты ошибочно передадут неверный ввод. Сначала мы создаем класс PositiveInteger:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class PositiveInteger {
  private Integer integerValue;
  
  public PositiveInteger(Integer inputValue) {
     if(inputValue <= 0) {
        throw new IllegalArgumentException("PositiveInteger instances can only be created out of positive integers");
     }
     
     this.integerValue = inputValue;
  }
  
  public Integer getIntegerValue() {
     return integerValue;
  }
}

Тогда для метода, который может использовать только положительное целое число в качестве входных данных:

1
public void setNumberOfWinners(PositiveInteger numberOfWinners) { … }

Это, конечно, простые примеры, и я утверждал, что суть проблемы в том, что иногда возникают проблемы, и тогда мы должны информировать клиентов о том, что произошло. Допустим, мы получаем список сотрудников из внешней серверной системы, и все может пойти не так. Как справиться с этим?
Мы можем установить наш объект ответа на GetEmployeesResponse, который будет выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class GetEmployeesResponse {
  private Ok ok;
  private Error error;
 
   
  class Ok {
    private List<Employee> employeeList;
    ...
  }
 
  class Error {
    private String errorMessage;
    ...
  }
}

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

Как упоминалось ранее, класс Exception расширяет класс Throwable. Все исключения являются подклассами класса исключений. Исключения могут быть отнесены к категории проверенных и непроверенных исключений. Это просто означает, что некоторые исключения, проверенные, требуют от нас указать во время компиляции, как приложение будет вести себя в случае возникновения исключения. Непроверенные исключения не требуют от нас обработки времени компиляции. Для создания таких исключений вы расширяете класс RuntimeException, который является прямым подклассом Exception. Старый и распространенный принцип, когда речь идет о проверенном и непроверенном, заключается в том, что исключения времени выполнения используются для оповещения о ситуациях, которые приложение обычно не может предвидеть или восстанавливать, в то время как проверенные исключения представляют собой ситуации, которые хорошо ожидаемое приложение должно ожидать и восстанавливать.

Ну, я сторонник использования только исключений во время выполнения . И если я использую библиотеку, у которой есть метод с проверенным исключением, я создаю метод-оболочку, который превращает его в среду выполнения. Почему не проверены исключения тогда? Дядя Боб в своей книге «Чистый код» утверждает, что они нарушают принцип Open / Closed , поскольку изменение подписи с помощью нового объявления throws может иметь последствия на многих уровнях нашей программы, вызывающей метод.

Теперь, проверено или не проверено, поскольку исключения — это конструкция, которая дает нам представление о том, что пошло не так, они должны быть как можно более конкретными и информативными о том, что произошло. Поэтому старайтесь использовать стандартные исключения, другие поймут, что случилось проще. Видя исключение NullPointerException, причина понятна для всех. Если вы делаете свои собственные исключения, делайте это разумным и конкретным. Например, исключение ValidationException позволяет мне узнать, что определенная ошибка проверки прошла, а исключение AgeValidationException указывает на конкретный сбой проверки. Будучи конкретным, позволяет не только диагностировать произошедшее, но и определять другое поведение в зависимости от того, что произошло (тип исключения). Вот почему вы всегда должны сначала поймать самое конкретное исключение! Итак, вот еще один распространенный совет, который предписывает не ловить «Исключение». Это верный совет, которому я иногда не следую. В границах моего API (скажем, конечные точки моей службы REST) ​​у меня всегда есть общие предложения Exception catch. Я не хочу никаких сюрпризов и чего-то, что мне не удалось предсказать или предотвратить в моем коде, чтобы потенциально раскрыть вещи для внешнего мира.

Будьте описательными, но и предоставляйте исключения в соответствии с уровнем абстракции . Попробуйте создать иерархию исключений, предоставляющих семантическую информацию на разных уровнях абстракции. Если из нижних уровней нашей программы выдается исключение, такое как исключение, связанное с базой данных, оно не должно предоставлять детали вызывающей стороне нашего API. Перехватите исключение и сгенерируйте более абстрактное, которое просто сообщает вызывающим, что их попытка не удалась. Может показаться, что это противоречит общему подходу «ловить только тогда, когда вы можете справиться», но это не так. Просто в этом случае наша «обработка» вызывает новое исключение. В этих случаях делайте всю историю исключения доступной от броска до броска, передавая исходное исключение в конструктор нового исключения.

Слово «ручка» использовалось много раз. Что это означает? Исключение считается обработанным, когда оно «поймано» в нашем знакомом предложении catch. Когда выдается исключение, сначала оно будет искать обработку исключения в коде, откуда оно происходит, если оно не найдено, оно будет переходить в контекст вызова метода, к которому оно относится, и так далее, пока не будет найден обработчик исключения или программа будет прекращено.

Еще один приятный момент, который мне нравится от дяди Боба, заключается в том, что блоки try-catch-finally определяют область действия программы. И помимо лексической области мы должны думать о ее концептуальной области, трактовать блок try как транзакцию . Что нам делать, если что-то идет не так? Как мы можем оставить нашу программу в действительном состоянии? Не игнорируйте исключения! Я предполагаю, что многие часы несчастья для программистов были вызваны молчаливыми исключениями. Улов и, наконец, блок — это место, где вы будете убирать. Обязательно дождитесь, пока у вас будет вся информация, чтобы правильно обработать исключение. Это может быть привязано к принципу « рано поймать поздно» . Мы бросаем рано, поэтому мы не выполняем операции, которые мы должны откатить позже из-за исключения, и мы ловим поздно, чтобы иметь всю информацию для правильной обработки исключения. И, между прочим, когда вы ловите исключения, регистрируйте их только тогда, когда вы их разрешаете, иначе одно событие исключения вызовет беспорядок в ваших журналах. Наконец, для обработки исключений я лично предпочитаю создать службу обработки ошибок, которую я могу использовать в различных частях моего кода, и предпринимать соответствующие действия в отношении ведения журнала, повторного использования, очистки ресурсов и т. Д. Он централизует мое поведение по обработке ошибок, избегает кода повторение и помогите мне получить более высокую оценку того, как ошибки обрабатываются в приложении.

Итак, теперь, когда у нас достаточно контекста, парадоксов, правил и их исключений, мы можем подвести итог:

  • Старайтесь избегать исключений. Используйте языковые особенности и правильный дизайн, чтобы добиться этого
  • Используйте исключения времени выполнения, оберните методы проверенными исключениями и превратите их во время выполнения
  • Попробуйте использовать стандартные исключения
  • Сделайте ваши исключения конкретными и описательными
  • Сначала поймать самое конкретное исключение
  • Не лови на исключение
  • Но ловите Exception на границах вашего API. Иметь полный контроль над тем, что выходит в мир
  • Создайте иерархию исключений, которая соответствует уровням и функциям вашего приложения
  • Бросайте исключения на должном уровне абстракции. Поймай исключение и брось более высокий уровень при переходе от слоя к слою
  • Передайте полную историю исключений при перебрасывании, предоставив исключение в конструкторе нового
  • Думайте о блоке try-catch-finally как о транзакции. Убедитесь, что вы оставляете вашу программу в правильном состоянии, когда что-то идет не так
  • Поймать исключение, когда вы можете справиться с этим
  • Никогда не имейте пустых предложений поймать
  • Записать исключение при обработке
  • Иметь глобальную службу обработки исключений и выработать стратегию обработки ошибок

Вот и все! Продолжай и будь исключительным!

Смотрите оригинальную статью здесь: Как бороться с исключениями

Мнения, высказанные участниками Java Code Geeks, являются их собственными.