Ветвление кода без исключений

Использование исключений для управления потоком выполнения является достаточно распространённой практикой. Однако во многих статьях и книгах, посвящённым лучшим практикам, например в замечательной книге Джошуа Блоха “Java — Эффективное программирование“ (Effective Java, Joshua Bloch), даётся рекомендация не использовать исключения как способ ветвления кода.

Я думаю, что многие сталкивались примерно с такими примерами кода:

В приведённом выше примере в классе-сервисе происходит поиск объекта типа Order. Если он не найден, то будет выброшено исключение NoSuchElementException, а если идентификатор запрашивающего пользователя не соответствует идентификатору пользователя, создавшему заказ, то будет выброшено исключение AccessDeniedException. Метод в классе-контроллере в свою очередь каждое из этих исключений преобразует в соответствующий HTTP-ответ.

Может показаться, что перед нами абсолютно корректный и рабочий код. Код действительно рабочий, но, отнюдь, не совсем корректный из-за использования исключений. Оба случая, в которых будут выброшены исключения, являются предсказуемыми, более того, они не являются ошибками. Поэтому исключения в данном случае используются не по назначению. Стоит так же помнить о том, что при создании нового объекта-исключения в большинстве случаев собирается трассировка стека, на что требуются дополнительные ресурсы, что является роскошью при разработке проектов, ориентированных на высокие нагрузки и высокую скорость отклика.

Результат или исключение

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

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

Такой класс есть, например, в стандартной библиотеке классов Kotlin. Аналогичный подход просматривается в классе CompletableFuture и аналогах в реактивных фреймворках, например в Mono у Project Reactor.

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

Конкретные результаты выполнения методов

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

  • Пользователь получит информацию о заказе, если заказ существует и у пользователя есть доступ
  • Пользователю будет отказано в доступе к информации о заказе, если заказ существует, но у пользователя нет доступа к нему
  • Пользователю не будет предоставлена информация о заказе, если он не существует

Все три результата можно выразить в виде трёх классов, реализующих один интерфейс или абстрактный класс, например так:

Итоговая реализация может иметь несколько иной вид, поскольку в данном случае есть возможность использовать вложенные типы, применять шаблон “одиночка” (singleton) и изолированные (sealed) классы. Кроме этого классы, описывающие результат операции, могут содержать какую-то дополнительную информацию.

Теперь код примера будет выглядеть примерно следующим образом:

Выводы

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