Pull to refresh

Comments 16

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


Суть в том, что если вы не словили что-то в main потоке, то в худшем случае упадет все приложение. И вы об этом узнаете (хотя возможно в логах и будет пусто). А если упадет какой-то поток — то вы вообще не узнаете ничего. Типовой случай — callback в потоке, созданном фреймворком.

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


Четвертый совет дает неверные аргументы и игнорирует один важный случай.
Исключения — это структурный переход вперед-вверх, что является полезной разновидностью goto, такой же как return или continue.
Но они непригодны для обычного потока управления, так как очень дороги в плане потребления ресурсов.
В языках, где это не так (питон) исключения используются куда шире.
Есть один случай когда исключение можно использовать в обычном потоке управления и в яве: это прерывание выполнения задачи. Здесь раскрутка стека в плюс а потребление ресурсов не в минус.

Но они непригодны для обычного потока управления, так как очень дороги в плане потребления ресурсов.

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


Java движется в сторону удешевления исключений, например, в Java 7 добавили конструктор Throwable(String message, Throwable cause, boolean enableSuppression,boolean writableStackTrace), который даёт возможность опустить запись стектрейса. А для тех, кому он нужен и для этого создают исключение, в Java 9 добавили StackWalker API

Очень односторонне. Не знаток java, но активно работал с исключениями в Delphi. Разумный подход такой:

  1. Логгируем там, где есть максимум дополнительной информации, которую мы можем записать в лог. То есть не просто «файл не открыт», а файл пытались открыть с такими-то параметрами доступа. Не просто — не удалось преобразование такой-то строки в число, а значение такого текстового поля не числовое. Такой подход дает сообщения об ошибке понятные для пользователей.
  2. Реагируем на ошибку обычно на этом же уровне. Но если нужна ещё и реакция на более высоких уровнях — меняем тип исключения на «логгировать не надо, но ошибка в файлах» или «логгировать не надо, но ошибка во вводе полей».
  3. После реакции меняем тип исключения на самое общее «логгировать не надо, завершаем работу функции». Его назначение — просто быстрый выход из середины кода.

Особенно удобен такой подход при обслуживании всяких непредвиденных исключений типа деления на ноль или обращения через нулевой указатель.

Сталкивался со случаем управления потоком исполнения через исключения, и это дико не удобно. Получается есть 2 потока и множество не явных переходов из первого во второй. И ещё если ты не автор этого кода, то это просто ужас.


А ситуация с catch (Exception e) — это вообще из ряда вон выходящее. Хорошо если такая конструкция встречается в 1-2 местах на проекте и она обоснована, но вот в качестве архитектурного шаблона — это издевательство. Викторина какая-то, угадай что я хотел обработать...


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

Насчет использования исключений в стиле goto не совсем согласен (ошибка 4), есть % тех случаев, которые просто требуют именно выброса исключений.
Как пример: Вы трассируете некую структуру данных (предположительно неограниченную — граф, дерево или список списков) и создаете собственную структуру данных с ограничением: не более N узлов.
Делать постоянные проверки на останов — снижение быстродействия более чем в 10 раз (такой код уже не будет оптимизироваться JIT даже если вы гуру в Java), не говоря о значительной потере читаемости такого кода, в котором везде и всюду if-else натыканы. С исключениями работает почти мгновенно. Разумеется исключение должно при этом иметь адекватное название, например TraceNodeLimitExceededException.
Так что ошибка 4 является ошибкой далеко не всегда.
Правильнее было бы назвать — рекомендация: "Не используйте исключения для управления ходом исполнения приложения, если нет значительной потери в быстродействии или читаемости кода."


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

В описанном Вами случае использование исключения вполне обосновано. Скорее всего получится код наподобие следующего:


MyDataStructure result;
try { 
    result = traceSomeDataStructure();
} catch(TraceNodeLimitExceededException e) {
    //здесь можно написать в лог 
}

doSomething(result);

В блоке catch происходит только обработка ошибки (которой в данном случае может не быть), и код продолжает выполнение дальше.


В пункте 4 же речь идет скорее о случае, когда в catch выполняется логика приложения. Таким образом try-catch выполняет функцию if-else.
Например вот так:


try { 
    sendMessageToA();
} catch(SendException e) {
    sendAnotherMessageToB();
}
try { 
    return readTraverseAndDoManyChecks(params);
} catch(NotFoundException ignored) {
    // no logging needed at DEBUG-ERROR levels
    return defaultValueIfNotFoundOrNotValid(params);
}

Вы предлагаете это все упаковать в Optional, что бы потом делать orElseGet.
А сам Optional будет получаться как куча проверок внутри метода readTraverseAndDoManyChecks с рекурсивными проверками, что вот тут-то мы уже получили то, что нужно и можно результат вернуть.
И не забывай, что исключение можно создать с отключенным стектрейсом. Скорость работы в этом случае вообще не страдает.

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

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

Ошибка одна — перехват исключений. В подавляющем большинстве случаев его в бизнес-коде вообще не должно быть. Практически всегда обработка исключений — это удел абстракций верхнего уровня, где как правило нет особой необходимости сильно различать тип эксепшна. К сожалению в Java checked exceptions обязуют перехватывать непосредственно в вызывающем методе, хотя практически всегда это не его ответственность. В частности в Java все исключения доступа к ресурсам checked: IOException, SQLException, JMSException, etc… Архитекторы посчитали, что вызывающий метод во что бы то ни стало просто обязан позаботиться о том, что если вдруг что-то пойдет не так. Реально же бизнес-код вообще не должен заботиться об ошибке доступа к ресурсам также как и не должен заботиться об InterruptedException или OutOfMemoryError — это должны компоненты верхнего уровня универсальным способом, например откатить транзакцию.

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

Throws приходится пробрасывать через весь стек методов. При движении снизу вверх количество эксепшнов в throws будет только расти. Ну и как правило верхним уровням абсолютно пофиг что за ошибка там возникла — у него как правило стоит один универсальный обработчик на все типы эксепшнов.


Есть расхожий паттерн о том, что эксепшны низких уровней нужно заворачивать в собственные более внятные: например при чтении конфигурации SQLException или IOException завернуть в свой ConfigurationException. Это плохо, потому как затрудняет абстракциям верхнего уровня получить внятный доступ к причине (в каком из цепочки getCause() он лежит?). В случае же, когда этого не требуется и абстракции верхнего уровня абсолютно пофиг на тип эксепшна, нет никакого смысла в заворачивании.


Checked должны быть эксепшны, связанные с бизнес-логикой: валидацией данных, некорректным состоянием, etc. Эксепшны, связанные с ресурсами должны быть только unchecked, и это ошибка дизайна Java API.

Допустим, что убираем checked. Вообще никаких исключений не видно, которые мог бы выбросить фреймворк, например для работы с http.
Что именно ловить? Checked исключения — это вариант возвращаемого значения с приятным бонусом в виде неявного завершения блока кода, вместо проверок возвращаемого методом кода (результата) — ловишь исключение. Это не только код упрощает, но и избавляет от нужды читать список констант (почему все упало) и пользоваться какими-то левыми инструментами, что бы получить детали.


А насчет заворачивания исключений многократно могу лишь сказать так: руки кривые и излишнее проектирование. Не каждый уровень бизнес-логики вообще требует какой-либо работы с исключениями.
Исключения не везде нужны. В лучшем случае в 1% функционала его требует обязательно, все остальное — проектирование ради проектирования.


Вот чего не хватает в Java, так это возможности сказать, что любые исключения в этом методе считать unchecked — как раз избавится от ручного управления списком throws. Современные IDE и компиляторы вполне умны, что бы понять какое исключение может быть в вызываемом методе и передать его выше. Для этого вроде как даже Java Reflections достаточно, информация присутствует.

Checked исключения — это вариант возвращаемого значения с приятным бонусом в виде неявного завершения блока кода, вместо проверок возвращаемого методом кода (результата) — ловишь исключение.

И кто вам мешает делать то же самое с unckecked? Разница лишь в том, что вас не обязуют это делать непосредственно в вызывающем методе. Если хотите, unchecked эксепшны — это вариант возвращаемого значения с неявным завершением блока, добавляемый для всех методов по умолчанию. Но основное назначение эксепшнов — это именно раннее завершение блока, а вот как раз бонус — это возвращаемое значение. Кроме того, когда требуется именно возврат определенного осмысленного значения, лучше использовать монады типа Optional или Try — для эксепшнов осмысленное значение редко выходит за рамки типа эксепшна и сообщения со стектрейсом.


А насчет заворачивания исключений многократно могу лишь сказать так: руки кривые и излишнее проектирование.

То есть архитектурный леер должен выставлять наружу делати реализации? :) Для самостоятельного осмысления требуется расставить эксепшны в throws:


interface ConfigReader {
  Properties readConfig() throws ???;
}
class SQLConfigReader implements ConfigReader {
  Properties readConfig() throws SQLException;
}
class FileConfigReader implements ConfigReader {
  Properties readConfig() throws IOException;
}
class URLConfigReader implements ConfigReader {
  Properties readConfig() throws MalFormedURLException, IOException;
}

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

Справедливо. Однако Java Api просто пронизан checked-исключениями. То есть либо перехватывайте и обрабатывайте, либо перехватывайте и врапьте, либо извольте отписаться в throws. И если уж затронули тему Java Api, то почему MalformedURLException и ParseException checked, а NumberFormatException unchecked, хотя все они возникают при парсинге текста.


Вот чего не хватает в Java, так это возможности сказать, что любые исключения в этом методе считать unchecked — как раз избавится от ручного управления списком throws.

И это будет последним костылем перед тем, как полностью перейти к unchecked.


Современные IDE и компиляторы вполне умны, что бы понять какое исключение может быть в вызываемом методе и передать его выше.

class MyService {
  void init(ConfigReader configReader) {
    // configReader -- это интерфейс.
    // В какой конкретный класс смотреть компилятору,
    // чтобы определить, какие исключения тут могут быть выкинуты?
    configReader.readConfig();
  }
}

Проблема в том, что вы можете думать, что обработали все checked ошибки и все будет хорошо, но хорошо не будет, так как многие кидают еще и runtime и заворачивают как угодно одно в другое, например, RMI, что говорить о сторонних библиотеках. Поэтому большого смысла в checked ошибках нет, только если вы сами сделали апи и уверены, что ничего лишнего оттуда не вылетит, а если вылетит, то эта ошибка фатальная. Вобщем, если не хотите чтобы все упало, то придется писать catch(Throwable e) или изучать исходники.


В дотнете, например, нет checked ошибок и нет создаваемого ими ложного чувства спокойствия.

Спасибо за статью.
То ли я чего-то не понял в П7, то ли совсем не согласен.
Вот типовой пример при работе с репозиторием:


// repo layer
public Document getDocument(String id) throws dbVendor.UnknownIdException {
   return client.getDocument(id);
}
// service layer
public Client getClient(String id) throws myService.ClientNotExistsException {
   try {
       return convert(getDocument(id));
   } catch (dbVendor.UnknownIdException ex) {
       throw new myService.ClientNotExistsException(ex, id);
   }
}
// API layer
public APIResponse getClientById(String id) {
   try {
        return getClient(id);
   } catch (myService.ClientDoesNotExistsException ex) {
        return APIResponse.notFound(id);
   }
}

Если dbVendor.UnknownIdException не превратить в myService.ClientNotExistsException, то вы нарушите не только инкапсуляцию, но и ваш сервисный слой потеряет всякий смысл и АПИ слой будет напрямую зависеть от репозитария.

Sign up to leave a comment.

Articles