Каждое приложение живет в реальном мире, а реальный мир не идеален. Так что даже идеальное, безошибочное приложение обречено иметь дело с ошибками.
Эта проблема существует с самого рождения первой компьютерной программы. И инженеры-программисты изобрели множество способов борьбы с ошибками.
Java традиционно использует следующие подходы, чтобы сигнализировать вызывающему абоненту о наличии ошибки:
- возвращает специальное значение (чаще всего для этой цели используется значение ‘null’)
- выдать исключение
Оба этих подхода имеют существенные недостатки.
Возвращаемое специальное значение отбрасывает информацию о фактической причине ошибки и раздувает код дополнительными проверками.
Исключения довольно дороги по сравнению с обычным потоком выполнения и затрудняют отслеживание потока и проверку правильности. Некоторые библиотеки и фреймворки, как правило, злоупотребляют исключениями вплоть до того, что делают их частью обычного процесса выполнения, что является безумием.
Итак, есть ли какой-либо альтернативный способ сообщить вызывающему абоненту об ошибках без упомянутых выше недостатков? Да! Функциональное программирование обеспечивает один из них.
Обратите внимание, что в следующем тексте я постараюсь избегать специфичной для FP терминологии. Это не делает подход менее функциональным, но упрощает понимание концепции для тех, кто еще не привык к FP-сленгу.
Контейнер Либо R> R>
Идея состоит в том, чтобы использовать контейнер для возвращаемого значения вместо обычного значения. Контейнер является особенным: хотя он объявлен для двух типов, на самом деле он одновременно содержит только одно значение первого или второго типа.
Либо R>
является контейнером общего назначения, не привязанным к распространению/обработке ошибок. R>
В коде это выглядит следующим образом:
EitherparseUUID(final String input) { ... // failure return Either.left(ErrorDetails.of("Unable to parse UUID")); ... // success return Either.right(uuid); }
На самом деле, это не так уж сильно отличается от обычного “сделайте что-нибудь и верните результат в случае успеха или выдайте исключение, если есть ошибка”.
Но более глубокий взгляд раскрывает множество преимуществ:
- Больше не нужно возвращать какое-то “специальное” значение.
- Информация об ошибке по-прежнему доступна.
- Поток выполнения не нарушается.
Приведенный выше код показывает “производящую” сторону, теперь давайте посмотрим, как выглядит “потребляющая” сторона:
...// Service interface EithergetUserById(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
приведенный выше код может быть переписан следующим образом:
... ResultparseUUID(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 { Resultformat(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”