Рубрики
Без рубрики

Последовательное распространение и обработка ошибок в Java

Никогда не создавайте исключений. С тегами java, lang, tutorial, для начинающих.

Каждое приложение живет в реальном мире, а реальный мир не идеален. Так что даже идеальное, безошибочное приложение обречено иметь дело с ошибками.

Эта проблема существует с самого рождения первой компьютерной программы. И инженеры-программисты изобрели множество способов борьбы с ошибками.

Java традиционно использует следующие подходы, чтобы сигнализировать вызывающему абоненту о наличии ошибки:

  • возвращает специальное значение (чаще всего для этой цели используется значение ‘null’)
  • выдать исключение

Оба этих подхода имеют существенные недостатки.

Возвращаемое специальное значение отбрасывает информацию о фактической причине ошибки и раздувает код дополнительными проверками.

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

Итак, есть ли какой-либо альтернативный способ сообщить вызывающему абоненту об ошибках без упомянутых выше недостатков? Да! Функциональное программирование обеспечивает один из них.

Обратите внимание, что в следующем тексте я постараюсь избегать специфичной для FP терминологии. Это не делает подход менее функциональным, но упрощает понимание концепции для тех, кто еще не привык к FP-сленгу.

Контейнер Либо R> R>

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

Либо R> является контейнером общего назначения, не привязанным к распространению/обработке ошибок. R>

В коде это выглядит следующим образом:

   Either parseUUID(final String input) {
       ...
       // failure
       return Either.left(ErrorDetails.of("Unable to parse UUID"));
       ...
       // success 
       return Either.right(uuid);
   }

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

Но более глубокий взгляд раскрывает множество преимуществ:

  • Больше не нужно возвращать какое-то “специальное” значение.
  • Информация об ошибке по-прежнему доступна.
  • Поток выполнения не нарушается.

Приведенный выше код показывает “производящую” сторону, теперь давайте посмотрим, как выглядит “потребляющая” сторона:

   ...// Service interface
   Either getUserById(final UUID uuid);

   ...//Actual use
   return parseUUID(parameter).flatMapRight(service::getUserById); 

Этот подозрительно простой код содержит все необходимое для обработки ошибок:

  • Он возвращает правильный результат ошибки, если какой-либо шаг обработки возвращает ошибку.
  • Он немедленно прекращает обработку, как только произошла ошибка.
  • Это не нарушает поток выполнения, оператор return всегда выполняется и всегда возвращает значение вызывающей стороне.
  • Он применяет политику “либо обрабатывать ошибку, либо распространять ее”, что приводит к надежному коду.
  • Последовательное применение этого подхода приводит к чистому и удобочитаемому коду.

Специализируясь на узком варианте использования

Как можно было заметить, простой Либо R> является довольно подробным, когда используется для обработки ошибок. R>

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

Второй источник многословия и неудобств (для этой конкретной цели) заключается в том, что Либо R> является общим в том смысле, что его можно использовать для любых типов, а его API симметричен по отношению к обеим сторонам. R> является общим в том смысле, что его можно использовать для любых типов, а его API симметричен по отношению к обеим сторонам. Когда Либо R>

Таким образом, для более узкого случая обработки ошибок Либо R> может быть специализирован на Result type, который предполагает единый общий базовый тип для ошибок и имеет API, настроенный для обработки ошибок. R>

С помощью Result приведенный выше код может быть переписан следующим образом:

   ...

   Result parseUUID(final String input) {
       ...
       return Result.failure(ErrorDetails.of("Unable to parse UUID"));
       ...
       return Result.success(uuid);
   }

   ...// Service interface
   Result getUserById(final UUID uuid);

   ...
   return parseUUID(parameter).flatMap(service::getUserById); 

Теперь код менее подробный, в то время как все упомянутые выше свойства все еще присутствуют.

Адаптация существующего кода для использования результата

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

Для этой цели Result реализация в Reactive Toolbox Core предоставляет набор вспомогательных методов, которые позволяют обернуть традиционные методы в те, которые возвращают результат.

Приведенный ниже пример показывает, как можно использовать эти вспомогательные методы:

   interface PageFormattingService {
       Result format(final URI location);
   }

   private PageFormattingService service;

   private Result formatPage(final String requestUri) {
       return lift(URI::create)
               .apply(requestUri)
               .flatMap(service::format);
   } 

Послесловие

Эта статья (наряду с предыдущей ) представляет собой попытку описать некоторые основные концепции Reactive Toolbox Core библиотеки. Конечно, ни одна из этих концепций не является новой. Я просто пытаюсь создать библиотеку, которая обеспечивает удобное и последовательное применение этих концепций.

Я часто вижу целые статьи, посвященные “Java слишком стара и должна быть удалена и заменена современным языком”. Концепции, упомянутые выше, показывают, что это просто неверно. В рамках существующих функций Java можно писать современный, чистый и надежный код. Все, что необходимо, – это изменить привычки и подходы, а не язык. Интересно, что изменение подходов приносит больше пользы, чем изменение языков, потому что подходы применимы к нескольким языкам.

Оригинал: “https://dev.to/siy/consistent-error-propagation-and-handling-in-java-158j”