Pull to refresh

Comments 86

Очень спорные утверждения.
У меня вот какие аргументы против:


  • На каждом уровне приложения прийдется ловить 100500 видов исключений к которым код не имеет никакого отношения.
  • Нарушение "single responsibility principle" и как следствие — трудночитаемый код.
  • "Сьеденные" и неправильно обработанные исключения на определенном уровне приложения приводят к гораздо большим потерям времени, чем пройтись по стеку исключения и понять в чем проблема.
  • куча болйлерплэйта, который нужно менять, поддерживать, пробрасывать при любом изменении на более низких уровнях.
  • Когда код что-то вызывает, он определенно имеет отношение к тому, что вызвал. И к ошибкам этого в том числе.
  • Обработка сбоя операции — это не нарушение единственности ответственности. Сбой операции — один из возможных исходов операции и он входит в контракт этой операции. И корректное обращение со всеми исходами — это часть ответственности вызывающего. Что-то солид стали как-то неправильно цитировать.
  • Как раз для избежания бойлерплейта (который я назвал шаблонным кодом) я и написал утилиту. Поскольку обрабатывать ошибки надо, но загромождать код не хочется.
    К слову, на скрине показан случай, когда игнорирование ошибки делает код не просто бесполезным, но и опасным.
Когда код что-то вызывает, он определенно имеет отношение к тому, что вызвал. И к ошибкам этого в том числе.

Ну вот вызываю я IStorage::save(), и чего, какие ошибки ловить? Дисковые, сетевые, ошибки прожига CD?

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

обобщенные ошибки сохранения

Например?

Для приведенных вами случаев:
НашСкладНедоступенОшибка
СохранениеПрервалосьОшибка
Более общее — ОшибкаСохраненияНаСкладе.

Ну так чтобы сделать что-то кроме "упасть и записать в лог", надо знать причину, почему он недоступен, почему сохранение прервалось. А такие абстрактные ошибки ничем не отличаются от RuntimeException. А ConnectionError к чему относить, это склад недоступен или сохранение прервалось? А если мы хотим сохранить, и склад недоступен, это ведь тоже означает, что сохранение прервалось, почему это разные исключения?


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

UFO just landed and posted this here

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


ConnectionError обрабатывается в зависимости от контекста. Для абстракции склада нас не интересует какого типа ошибка произошла в реализации (уборщицу будут наказывать люди после просмотра логов, а не ваш сервис), нас интересует, что определённого типа операции нам временно/постоянно недоступны.
Склад недоступен — это когда даже начать операцию нельзя, например сервис вообще не отвечает. Операция прервана — значит взаимодействие есть, но состояние склада вызывает вопросы: можно ли к нему обращаться в следующий раз.


Ошибку соединения по сети и ошибку прожига CD обрабатывать надо по-разному

Как по разному? У вас склад не работает и так, и эдак. То, что потом, посмотрев логи, вы позвоните админу/уборщице с приказом восстановить сеть или тому же админу поручите заменить/прочистить диски в устройстве — это дело дестятое, организационное и к вашему сервису уже не имеет отношения, никак в данную секунду ему не поможет.

Вы поставлены перед фактом: сейчас сохранить не могу, нет доступа к хранилищу. И дальше уже ваша ответственность на основе этих сведений принимать решение.

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


нас интересует, что определённого типа операции нам временно/постоянно недоступны

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


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

Ну так я ее уже начал вызовом функции save().


Как по разному? У вас склад не работает и так, и эдак.
это дело десятое, никак в данную секунду ему не поможет.

Ну так и какой тогда смысл в исключениях в сигнатуре, если мы все равно ничего не можем с ними сделать?

на основе этих сведений можно только упать с записью в лог

Зачем же падать? Может у вас это сохранение было опционально, или имеется другой резервный склад, чуть поодаль.
К тому же если просто упасть, то вы не освободите коннект, ваш поток завершится, сокет будет висеть. Если поток был из пула и произойдёт ещё ~64к запусков потока и попыток подключиться — ваше приложение вообще перестанет отвечать, если к тому времени оно ещё будет живо. Так что падать с лапками кверху — это самое глупое, что можно сделать.


Для сети можно то же самое действие повторить

По идее это ваш склад должен делать сам.


Исключения в сигнатуре — чтобы явно видеть, на что рассчитывать, когда зовёшь метод. Из моей практики, не называя имён: сервис не объявил вообще никаких исключений, ни проверяемых, ни не-. В документации по этому поводу тишина. Сервис корный, очень системный. В итоге вызвав этот метод, я получаю ошибки только после тяжёлого развёртывания на стенде. Какого х* спрашивается?
Причем из логики (кода) сервиса становится понятно, что кинуть рантайм-эксепшн, если в системе не настроены кое-какие параметры — в порядке вещей. И эти параметры не в проперти-файлах, а нетривиально задаются отдельно. У меня вопрос, я методом тыка должен об этом узнавать?

Да, ещё момент: игнорирование ошибок в методе — это пренебрежение к пользователю вашего метода. Вы экономите своё время за счёт времени вашего контрагента. Он точно рад не будет.

В итоге вызвав этот метод, я получаю ошибки только после тяжёлого развёртывания на стенде. Какого х* спрашивается?

Так и должно быть. На стенде нет чего-то, что нужно приложению для правильной работы — приложение не должно продолжать работу.


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

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


игнорирование ошибок в методе — это пренебрежение к пользователю вашего метода.

Так не надо их игнорировать. Вон с Either говорят, компилятор все проверяет.

Имелось в виду игнорирование объявления ошибок в сигнатуре.

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

Ну во-первых, OutOfMemoryError а не эксепшн. Это ошибка ДЖВМ, я уже несколько раз спорил о том, можно ли вообще что-то делать в ДЖВМ, когда происходят именно Эрроры. На мой взгляд, в случае с OutOfMemoryError продолжать категорически нельзя.
Впрочем, его-то как раз ловить не надо, пусть падает сам.
Но если вдруг случайно поймали — в томкате например, надо звать java.lang.System#exit прям сразу после лога.

Ну если, например, ваш код вызвал new long[Integer.MAX_VALUE], то именно тут можно и поймать OOM, и как-то обработать (запросить меньше?), и пойти выполняться дальше.

Когда код что-то вызывает, он определенно имеет отношение к тому, что вызвал. И к ошибкам этого в том числе.

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


public class DataOutputStream extends FilterOutputStream implements DataOutput {
    public final void writeInt(int v) throws IOException {
        out.write((v >>> 24) & 0xFF);
        out.write((v >>> 16) & 0xFF);
        out.write((v >>>  8) & 0xFF);
        out.write((v >>>  0) & 0xFF);
        incCount(4);
    }
}

Здесь метод writeInt() вызывает OutputStream.write() аж 4 раза, при этом ни нет одного обработчика! Таким же образом любой код вправе сохранить "оптимистичную" модель программирования, не заботясь о завершении каждой "небезопасной" операции, и не замусоривая код обработчиками и логикой, которая не относится к этому методу. Здесь обязуют декларировать checked IOException, однако если бы тут был RuntimeException декларация отсутствовала бы. И такой код вполне себе имеет право на существование.


Сбой операции — один из возможных исходов операции и он входит в контракт этой операции. И корректное обращение со всеми исходами — это часть ответственности вызывающего.

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


Во-вторых, вы никогда не сможете обработать все ошибочные исходы, ибо их множество в общем случае вычислимо, но никогда не определено ни одним контрактом. Я имею ввиду всевозможные RuntimeException, которые могут возникнуть в вызываемом коде, особенно если имеете дело со сторонней библиотекой/фреймворком, где под капотом подмешано большое количество различных технологий. И это множество зависит от многих факторов, многие из которых определяются не кодом, а runtime-конфигурацией системы: для одного и того же интерфейса может быть куча разных подключаемых провайдеров, код может работать в транзакции, вызывать remote-методы, делать RPC, стучаться в базу — и везде есть чему вальнуться. Поскольку конфигурация динамическая, на этапе проектирования нет возможности определить все возможные варианты сбоев.


В-третьих, сильно завязывать бизнес логику на исключения — это заведомо плохой подход. Exception — это именно исключительная ситуация, которая говорит о невозможности выполнения операции и требует раннего завершения всей цепочки вызовов. Если аварийное завершение — это часть вашего контракта и предусмотрена бизнес логикой, то и возвращайте его как обычное значение. Например вместо NotFoundException или NullPonterException возвращайте обычный Optional или на худой конец null.

  1. writeInt(int v) не очень удачный пример, потому что здесь однотипные операции с одной и той же ошибкой могут завершиться. И прокидывает наверх потому что это чистый агрегат/делегатор. Здесь нет никакой особой логики внутри.


  2. Это ошибочное мнение.
    Некоторые так не считают: например NoRegrets, mwizard

  3. RuntimeException потому так и называется, что он не предназначен для обработки бизнес-логикой. Это теперь уже мы имеем корявое наследие говно-кода в виде разных его обёрток, которые приходится учитывать при обработке ошибок. Исходно для бизнес-ошибок RuntimeException не предназначен. RuntimeException — это ошибка в программе (например NPE), когда надо просто падать.


  4. Если в сигнатуре метода прописаны основные ошибки, которые он генерит, то задача перебора становится реальной. Вы можете посмотреть на аналогию, бывшую в C++: если вы заявляете, что метод генерит только предопределённые исключения, то при попытке выкинуть что-то иное происходит системный сбой, обычно с немедленным завершением работы. И это логично, потому что все нижележащие ошибки метод должен был обработать сам. А то, что не может — объявляет явным образом в своей сигнатуре, то бишь контракте. То же самое происходит по логике вещей в Яве: вы явно выкидываете проверяемые исключения, а непроверяемые — это всегда должен быть только результат системного сбоя / ошибки программирования. Использование RuntimeException, означающего сбой, для целей бизнес-логики — это дизайнерский баг (или баг в мозгах).


  5. Итак, ошибки, ожидаемые от сервиса, известны, они объявлены. Тогда подключая соответсвующую либу/фреймворк, вы обязаны эти ошибки обработать. В свою очередь, наружу вы выставляете только то, что надо знать и уметь вашему клиенту, в том числе типы ошибок. И они не должны вообще никак повторять внутренние ошибки внешних либ. Иногда он даже не сможет определить тип этих ошибок, потому что ему соответствующие классы недоступны. И они не должны быть доступны. Потому что это не его дело знать, что произошло внутри, об этом должен был позаботиться непосредственный вызывающий — вы.


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


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


RuntimeException — это ошибка в программе (например NPE), когда надо просто падать.

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


Итак, ошибки, ожидаемые от сервиса, известны, они объявлены.

Ну да ну да. Всевозможные RuntimeException-ы особенно объявлены...


а непроверяемые — это всегда должен быть только результат системного сбоя / ошибки программирования.

NumberFormatException при Integer.parseInt, когда пользователь ввел строку вместо числа — это ошибка программирования или системный сбой? В самой Java API использование checked/unchecked много где неконсистентно. Почему парсинг URL выдает checked MalFormedURLException, а парсинг числа unchecked NumberFormatException?

Ага, классический пример. Если надо расширить контракт — расширяйте, в смысле берите новый расширенный контракт и реализуйте. Потому что от вашей новой реализации на старом контракте никто не хочет неожиданностей. Они не заявлены явно, не надо их создавать неявно. Выйдут новые версии клиентов нового контракта — тогда всё будет ок. А ломать логику через коленку не надо.


Обычно приводят список, который вдруг стал работать по сети. Но, во-первых, это как раз нарушение S в SOLID — вообще то грубейшая ошибка. Да ещё строить на этом нарушении какие-то доказательства, хуже не придумаешь.


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


В самой Java API использование checked/unchecked много где неконсистентно.

Ява с точки зрения API хороша, но далеко не идеальна. Меня, например, удивляет, почему разработав сносную библиотеку коллекций, сановцы не продумали создание отдельных интерфейсов для немутабельных версий. Зачем делать эти грязные хаки в виде выброса UnsupportedOperationException в модифицирующих методах списков, множеств и т.п? Нельзя было для такого случая просто не применять эти методы, т.е. создать отдельные ридонли-интерфейсы?

По поводу коллекций — так исторически сложилось, а исправить уже нет возможности. В некоторых API immutable collection заменяет Enumeration, но особо не прижилось.


А теперь по поводу checked exceptions вопрос, вы реально топите за то, чтобы все exceptions были checked и пришлось бы писать подобный код?


Map<String,Double> calculateAmount(List<MyBean> mybean) throws ElementDoesNotExistException, CannotCalculateAmount, CouldNotCollectAmountPct, NullPointerException
{
    Map<String,Double> hashMap = new HashMap<>();
    for (int i = 0; i < arrayList.size(); i++) {
        MyBean mybean;
        try {
            mybean = arrayList.get(i);
        } catch (IndexOutOfBoundException e) {
            throw new ElementDoesNotExistException(e);
        }
        double amountPct;
        try {
            amountPct = mybean.getAmount()/arrayList.size();
        } catch (DivisionByZeroException e) {
            throw new CannotCalculateAmount(e);
        }
        try {
            hashMap.put(mybean.getId(), amountPct);
        } catch (UnsupportedOperationException | ClassCastException | NullPointerException | 
IllegalArgumentException e) {
            throw new CouldNotCollectAmountPct(e);
    }
    return hashmap;
}

Это полный контракт данной операции, разве что отсутствует OutOfMemoryError и StackOverflowError.


Если надо расширить контракт — расширяйте, в смысле берите новый расширенный контракт и реализуйте. Потому что от вашей новой реализации на старом контракте никто не хочет неожиданностей.

Боюсь, с таким ригидным подходом до сих пор бы не существовало бы ни Spring, ни JavaEE, ни других фреймворков и технологий в экосистеме. Более слабая и "оптимистичная" парадигма наиболее продуктивна — контракт никогда не отражает всех нюансов, поэтому нужно быть готовым ко всему, но особо на этом не зацикливаться.

Не сказать, чтобы я прям топил, но в своих проектах применяю, обычно это не так страшно смотрится почему-то, один-два типа хватает слихвой. Если нет — то наибольший общий знаменатель вполне подходит.
Ваш пример гипертрофирован. Вполне достаточно объявить что-то типа CalculateAmountException — общий предок CannotCalculateAmount и CouldNotCollectAmountPct. Хотя даже эти случаи у вас надуманны.


Если происходит IndexOutOfBoundException, DivisionByZeroException, то их очевидно здесь ловить не надо. Это ошибка программирования, если они происходят, а значит продолжать в принципе небезопасно. mybean.getId() должен быть проверен на нуль ещё до какого-либо использования: mybean.getId().getClass()
Из UnsupportedOperationException | ClassCastException | NullPointerException |
IllegalArgumentException может произойти только NullPointerException, но его проверили выше.
В общем здесь максимум одно проверяемое исключение кидается. Потенциально — вообще без.


Насчёт JavaEE — спорно, они как раз сначала долго всё согласуют, но надо сказать, в итоге у них получается хорошо в последнее время.
А в Spring я вот прям сейчас борюсь с последствиями излишней гибкости.


Кстати, по поводу сервиса и его переезда на RPC: сделайте корректную обработку ошибок, если коннект валится — то всё, это как нуллпойнтер в локальной версии. То есть сервис неработоспособен. В целом конечно всё от ситуации зависит, но в изначальной логике всегда должен быть какой-то канал сообщения ошибок. Иначе она не расширяема по дизайну.

Если происходит IndexOutOfBoundException, DivisionByZeroException, то их очевидно здесь ловить не надо. Это ошибка программирования, если они происходят, а значит продолжать в принципе небезопасно

Мой пример именно из серии, если бы язык требовал строгое соблюдение контракта, а все исключения были бы проверяемыми. Контракт с List.get() и a/b может кидать исключения, соответственно, следуя вашей идее, вы обязаны его перехватить, даже если по логике он никогда не возникает (List-то об этом не знает!). И в этом случае кинуть какой-нибудь WTFException, который тоже должен являться частью контракта. Ну либо определить IndexOutOfBoundException как возможную ситуацию в контракте метода, выставляя наружу детали реализации. Более того, любой доступ к полям объекта может выдать NPE, поэтому и их тоже по идее нужно заворачивать в exception или пользоваться Optional вместо значения. И это мы еще не коснулись вычислимости типов, где у Java не такие большие возможности, и при каждом кастинге ставить catch ClassCastException. В итоге это все закончится паранойей и шизофренией, которая совершенно не нужна для реальных бизнес задач. Ваш контракт будет нагружен кучей ненужного дерьма, которое не имеет отношения к решаемой задаче и никогда никем не будет испльзоваться.


Кстати, по поводу сервиса и его переезда на RPC: сделайте корректную обработку ошибок, если коннект валится — то всё, это как нуллпойнтер в локальной версии.

Так и сделано. Кидается специальное RuntimeException, которое перехватывается каким-то там верхним обработчиком. А бизнес коду абсолютно наплевать — это именно исключительная ситуация, которая не должна происходить в работающей системе. Бизнес код не знает что делать в этом случае и поэтому не должен ставить никаких обработчиков на этот счет. Поэтому и нагружать контракт различными деталями реализации не есть хорошо.

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


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

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

UFO just landed and posted this here
а как было бы с сильной системой типов?
UFO just landed and posted this here

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

Иногда лучше, чтобы приложение упало, чем написало что-то в лог и побежало дальше (чтобы упасть позже).

  • здравствуйте, я функция создания директории. Не могу создать папку tmp, напишу об этом в лог и тихо завершусь
  • здравствуйте, я простая функция, которая собирает путь из нескольких кусков. Предыдущая функция ничего не вернула, поэтому я верну "/"
  • здравствуйте, я функция очистки папки временных файлов, и мне тут передали путь к ней...
— прав, чтобы удалить все файлы в корне, мне, к счастью, не хватило… (или хватило, но не на всё), но я тоже это запишу в лог, и молча завершусь, как у нас тут принято
— а мне не хватило места на диске, так как его кто-то занял, и никто не почистил. Файл я записать не могу, поэтому тоже запишу в лог, и молча завершусь
— а я пробую прочитать файл, а он какой-то недописанный…

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

Речь ведь идёт именно о корректной обработке ошибок. А решение о том, как корректно обработать ошибку можно только вблизи от места возникновения.
Определить, что делать, есть у вас поток может завершиться из-за сбойной абстрактной задачи — очень трудно, это почти эвристика, которая не приносит результата. Что делать тогда, на данном примере? Поток завершить? А если его никто не перезапустит? Или продолжить? — и запустить ещё две тысячи таких сбоев в работу, которые будут засирать логи, жрать память, грузить процессор, отбирать коннекты в из пула и расходовать файловые дескрипторы, пока вся система целиком не ляжет? Как определить, что делать на вершине стека, если лень было принять решение на месте.

Вернуться к состоянию приложения, которое предшествовало вызову функции, в которой возможен сбой. Наоборот, изнутри непонятно, что делать дальше — это и есть исключительная ситуация.
>А решение о том, как корректно обработать ошибку можно только вблизи от места возникновения.
Категорически нет. Далеко не всегда в месте возникновения есть информация, как обработать ошибку. Условно — не найден файл. То место, где его не нашли — это нижний уровень, и там не известно, что это за файл, может ли приложение работать без него, или же это вообще нормальная ситуация, и его просто создадут уровнем выше.

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

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

В частности, я для себя пришел к тому же примерно выводу, какой описан ниже у mwizard — т.е. выкинуть все throws, и заменить их явно на Either или его подвид Try, чтобы в сигнатуре метода было четко видно, что он может не вернуть результат, или вернуть вместо него ошибку. Этот способ тоже не без недостатков (в первую очередь наверное вызванных слабой системой типов Java), но для меня он лучше. В частности, потому что такие методы их вызовы лучше компонуются друг с другом — вы можете вычислить тип результата, чего вы не можете сделать для метода, который throws вместо результата.

Через несколько не стоит — можно через один, можно не протаскивать, а выправить ситуацию, если это возможно.

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

Правильно написано, не надо доводить до исключений, потому что исключения — это для ситуаций, которые нельзя исправить и невозможно предусмотреть (OOM, повреждение стека, невозможное состояние конечного автомата, исполнение недоступного кода, невозможность прочитать обязательный файл и т.д.).


Если исключение является штатной ситуацией, то это значит лишь, что функция, которая его может выбросить, должна его на самом деле возвращать. Например, если функция валидирует инпут, и какие-то из комбинаций аргументов запрещены. Для того, чтобы удобно возвращать такие вещи, и отделять исключения от ошибок валидации, есть алгебраический тип Either, который может быть либо "нормальным значением" (Right), либо "значением-ошибкой" (Left).


Функции, которые могут возвращать такие ошибки валидации, должны возвращать Either, а вызывающая сторона должна всегда распаковывать Either и смотреть, была ли ошибка, и если была, то уведомлять об этом пользователя/отправлять 4хх-статус по HTTP/печатать матюки в лог и т.д., но не падать, т.к. было бы глупо падать от неправильного пользовательского инпута или от неправильно присланного пакета по сети.


А вот если нельзя открыть файл, или сменить директорию, или записать файл, или удалить файл, или выделить память под объект, или открыть сокет — то лучше упасть сразу же, согласно парадигме fail fast, чтобы потом, при разборе полетов, сразу увидеть причину, и чтобы некорректно работающая программа не успела наломать дров в чем-либо еще.


tl;dr — исключения нельзя исправлять и нежелательно ловить, т.к. программа, в которой возникло исключение, уже находится в неверном состоянии. Пусть они обрушат программу. Если исключения ожидаются, это value-объекты, которые должны возвращаться штатно через return.

Что-то классдоадеры не падают, когда по конкретному пути не могут найти конкретный класс. И исключение кидают. Но их предусмотрительные дети пробуют поискать в другом месте.

Функции, которые могут возвращать такие ошибки валидации, должны возвращать Either, а вызывающая сторона должна всегда распаковывать Either и смотреть, была ли ошибка, и если была, то уведомлять об этом пользователя/отправлять 4хх-статус по HTTP/печатать матюки в лог и т.д., но не падать, т.к. было бы глупо падать от неправильного пользовательского инпута или от неправильно присланного пакета по сети.


я перечитал 2-3 раза но не понял мысль.
«Функции, которые могут возвращать такие ошибки валидации, должны возвращать Either, а вызывающая сторона должна всегда распаковывать Either и смотреть, была ли ошибка»

чем это отличается от броска исключения и последующим анализом? И что значит «была ли ошибка»? Если мы попали в блок catch — была.
По-моему, смысл встроенного в язык механизма исключений в том что на ошибку можно отреагировать там где это представляется уместным. В случае Either мы возвращаемся к си-стайлу кодов возврата, где на каждом уровне нужно писать шаблонный повторяющийся код типа
doSmth()
.map()
.onError()

притом такой механизм не обладает полнотой возможностей уже встроенного механизма исключений, например он не может организовать ловлю подклассов ошибок, приоритет ошибок и их ордеринг. Например в глубине коллстэка стало оказалось что вот есть ошибка, такого юзера нет и ему нужно отказать в доступе — можно сразу бросить ResponseStatusException и всё, где-то на верхнем уровне он будет пойман и преобразован сразу в ответ. Зачем тащить его через Either по всему коллстэку? Возможна также и обратная ситуация, когда компонент в глубине коллстэка не знает что делать с ошибкой. В общем не понял, зачем эти Either

Это отличается тем, что Either вы обязаны либо распаковать, либо передать в Either-aware функцию. Вы не можете просто передать Either<E, int> в функцию, которая хочет int, потому что ваше приложение просто не соберется. Вы не можете получить из Either одновременно и Left, и Right-значение, так как у Either есть только одно, и вам предстоит как раз узнать, какое. Почему именно так, видно на примере ошибки дизайнеров golang, которые распаковывают результаты функций в err и value автоматически, благодаря чему err можно смело игнорировать, а в value может быть мусор (и вы об этом не узнаете).


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


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


Что касается ловли подклассов — вы можете в Left-части Either передавать экземпляры классов с произвольной иерархией и тестировать их каким-нибудь instanceof на принадлежность к базовому классу. То же касается приоритета и порядка.


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


Если компонент в глубине коллстека не знает, что делать с ошибкой, то ошибка ли это? Почему компонент в глубине стека считает, что если пользователя нет в БД, то это ошибка, которую он обязан выбросить, а не просто вернуть пустой Maybe вызывающей стороне, и пусть уже бизнес-логика с этим разбирается?

Любая функция может и вернуть результат, и сфейлиться по куче разных причин. Иными словами тип результата является суммой всех этих исходов. К слову сказать, в C, где код возврата является числом от 0 до 255, это так и есть. Заменив числа на типы получится что-то в роде:
interface Result<T> extends T, InvalidArgumentError, HttpError, ...;
Чтобы не перечислять все возможные ошибки можно указать их общего предка:
interface Result<T> extends T, Exception
В любом случае, работа с подобными значениями требует учитывать все такие варианты, что приводит к комбинаторному взрыву по сложности.


Основная идея, которая стоит за Either, состоит в том, чтобы отделить единственный happy path потока вычислений, когда идёт все по плану, от sad path, как следствие бесчисленного множества всевозможных отклонений:
interface Either<Exception, T> { Exception left; T right; }
Определив такую структуру для наших результатов, мы можем далее фонусироваться только на happy path (Right), отбрасывая все остальные ситуации налево (Left) и обращаясь туда лишь от случая к случаю. Такое разделение сильно упрощает нам жизнь.


С другой стороны, тот факт, что при вызове любой, даже самой простой, функции что-то может пойти не так является довольно очевидным (банально — закончилась память). Поэтому по-хорошему типом результата везде должен быть Either. Но, поскольку сей факт является очевидным и всеобщим, то практической пользы от его доказывания при статичном анализе нет ни какой. Что лучше: писать map() или ставить на конце инструкции точку с запятой; — это тоже вкусовщина.


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

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


а чем это тогда отличается от checked исключений, которые обязательно либо обработать либо пробросить дальше, либо обернуть и пробросить?

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


да, вы выше писать что можно разделять fail-fast и Either когда штатно. Проблема в том что если исключения это более менее устоявщийся механизм (хотя и там есть споры и разные мнения) то с этим разделением становится ещё более весело, и можно встретить код примерно такого типа:

try {
      getUser()
           .left(...)
           .right(err -> TimeoutError)      // та же ошибка таймаута но кто-то обернул в спец ошибку и поместил в Either

} catch(ReadTimeoutException e) {  // та же ошибка но кто-то другой посчитал что это fail-fast
}


и таких примеров достаточно, потому что все люди умные же, и точно так же как вы уверены что это fail-fast/usual error, кто-то другой абсолютно так же уверен что это usual error/fail-fast. Итого вместо 1-го подхода появляется месиво из двух.
Ещё появляются колбаса типа

getUserPrivilleges()
.left()
.right({
reportAccessToAuditLogs()
.left()
.right({ err ->

doSmth()
})
})

и тд. В реале эта колбаса часто становится огромной и внутри каждого блока своя колбаса.

Если компонент в глубине коллстека не знает, что делать с ошибкой, то ошибка ли это?


ну скажем логгер пишет на диск и бросает ошибку доступа т.к. нет прав писать в эту директорию. Логгер не может решить эту проблему сам, может только кинуть наверх.

Почему компонент в глубине стека считает, что если пользователя нет в БД, то это ошибка, которую он обязан выбросить, а не просто вернуть пустой Maybe вызывающей стороне, и пусть уже бизнес-логика с этим разбирается?


ну допустим это микросервис, чья задача возвращать профайлы юзеров. Если юзера нет, он просто должен вернуть другому сервису этот факт, ошибку 404. В этом случае можно прокинуть ошибку через 10 уровней стэка и пусть её обработают в конце. В случае Either нужно заниматься этой ошибкой все 10 уровней.
UFO just landed and posted this here

Именно так. В отличие от go, где err и value извлекаются из результата функции автоматически при ее вызове, и про err элементарно забыть, вы не сможете провернуть этот же фокус с Either. У Either есть только одно значение — это либо left-значение (ошибка), либо right-значение (результат), и вы не можете извлечь оба сразу. Вам нужно статически решить, что вы делаете, и обработать оба варианта, иначе ваша программа не соберется.


Ну или передать Either как есть в функцию, которая явно принимает аргументом конкретный типоэкземпляр Either.

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

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

Начать с того — а что есть ошибка? Как их классифицировать?

Некорректность входных данных — это ошибка, ради которой стоит останавливать весь процесс? Всегда? Точно? А может есть ситуации, когда некорректные данные можно просто заменить дефолтным значением, выдать предупреждение и продолжить работу?

Естественно, что есть ситуации, когда дальнейшая работа становится небезопасной и/или непредсказуемой. Или просто невозможной. В этом случае все просто — остановка.

Но вот ситуация — 30 000 000 объектов для обработки. И при обработке одного возникает ошибка. Что делаем дальше? Останавливаемся совсем, или пишем в лог что не можем обработать данный объект и пытаемся обработать остальные? Строго говоря, тут тоже ответ неоднозначный. Если все объекты независимы — скорее продолжить работу. А если нет? Тогда может дойти до отката всего что обработали ранее в начальное состояние и только потом завершение.

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

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

Ну как-то так…

Очередная гениальная идея ничем не обоснованная кроме "ощущением" автора :), цель которой решить проблемы багов в программах, а результат ещё больше багов — потому что все выше описанное приводит к усложнению. Программист(99%) допускает баги когда не понимает как работает код, соответственно чем сложнее код тем меньше шансов что код поймут коректно.
Про исключение уже все давно расписано и объяснено — перехватывайте их в том месте где вы понимаете как восстановиться работу программы. Что касается многопользовательских сетевых приложений — в большинстве случаев место перехвата это верхняя точка, а действие по восстановление работы программы это возвращение ошибки в ответ на запрос.

Программист(99%) допускает баги когда не понимает как работает код, соответственно чем сложнее код тем меньше шансов что код поймут коректно.


насчет идеи автора не скажу, но как же это точно что порой пытаются внедрить какие-то эксклюзивные подходы, вроде как с благими целями но код становится настолько нечитабельным, захламлённым концепциями и отвратительным, что способствует ошибкам, только уже по другим причинам. «За что боролись на то и напоролись». Ни один ЯП или подход не отфильтрует логические ошибки, допущенные потому что код является месивом

В статье есть два момента.


  1. Терминологический. Нигде не отражён в русскоязычных источниках.
  2. Описание тулзы для выполнения рутинных операций, скрывая бойлерплейт-код.

Всё остальное вы придумали.

Как один из наиболее близких по принципам язык

UFO just landed and posted this here

Возможно, вы сможете предложить лучшие практики

Во-первых, всё же не понятно, при чём тут C#, в котором обработка исключений сделана иначе. В частности, нет необходимости указывать возможные ошибки, вылетающие из метода.

Во-вторых, все эти призывы обработать ошибку максимально близко к месту возникновения хороши только для однослойных приложений. Более-менее объёмное приложение в современном мире может содержать десятки слоёв. И вот, скажите, как обрабатывать ошибку записи в БД при обработке веб-запроса? Вот вам примерная схема слоёв сайта:
Browser — SPA (view-controller-service-ajax) — WebServer — WebController — ValidationLayer — BusinesLayer — DataAccessLayer — StorageService — DBConnectionsLayer — DataBaseApp — SQL.

Допустим, ошибка вылетает при обработке запроса внутри БД.
Каждый слой — это не один класс, часто это несколько библиотек, работающих совместно. Часть кода наша, часть системная/сторонняя.
Точка возникновения исключения — ошибка при исполнении sql-кода.

Где ловить? Как решение принимать? Что пользователь увидит?
А если наш BusinesLayer должен обработать как запросы из браузера, так и от интеграционного сервиса на шине?

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

На примере выше, возможны такие варианты:
StorageService логгирует все ошибки, для части известных ошибок возможна повторная обработка (например таймаут подключения), всё остальное выкидывается наверх.
DataAccessLayer кроме логгирования может обработать ошибки конкурирующих запросов и повторить неуспешные (в зависимости от типа запроса и типа ошибки), необработанное всё же выкидывается наверх.
BusinesLayer, например, может принимать решение о том, повторять ли запрос, прервать ли цепочку обработки (например, при ошибке на 5-м из 10 элементов, продолжить или прервать и откатить предыдущие)
WebController может часть ошибок оборачивать в что-то, понятное пользователю (та же ошибка конкуренции может приводить к сообщению «кто-то уже поменял запись, повторить запрос?»), а всё неопознанное превратится в «ошибка на сервере, код ошибки ###, сообщите его администратору».
А в SPA слои должны аккуратно прокинуть ошибку до того view, который покажет ошибку пользователю в красивом виде.

Как это всё сделать на .net без всяких извращения, я знаю. А как ваш обработчик поможет такое сделать в java?

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

ну, т.е. лирические отступления тут вообще не причём, у вас получился лишь сахарок вокруг try..catch?

Собственно да, об этом статья. Хотя дискуссия в итоге широкая получилась.
Лирика — это про сложность обработки ошибок и желание хоть как-то упростить.

Так ведь по сути, насколько я понял, вы лишь немного упрощаете синтаксис?
Так может и статью стоило соответственно озаглавить и убрать из неё лишнюю спорную лирику?
Отдельно про последний абзац с отсылкой на доку по java. Интересно, сколько лет этому чудесному утверждению:
Here's the bottom line guideline: If a client can reasonably be expected to recover from an exception, make it a checked exception. If a client cannot do anything to recover from the exception, make it an unchecked exception.
и сколько гигаватт энергии ушло у разработчиков на борьбу с этим подходом?
Моя личная претензия: почему код, который я вызываю, сам решает, смогу ли я продолжить работу после ошибки?

Вообще там указано, что это для 8 версии применимо

Это как бы намекает, что начиная с 9-й версии это уже не аргумент?
Т.е. даже без уточнения истории этого утверждения, оно не является правилом уже лет 6 как?

Я не комментирую источник, я с ним никак не связан.

А к чему тогда вот это:
Последам обсуждения в комментариях, ссылка на самый авторитетный источник, где все бессмысленные споры прерываются ясным и четким разъяснением: docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html

К тому, что по поводу дизайна исключений есть мнение первоисточника, на которое я и сослался. Как оно зависит от версии — не знаю. На мой взгляд, никак не зависит. 8 версия сегодня самая актуальная.

Понятно, что сослались, но:
а) не факт, что это мнение верно
б) не факт, что оно актуально.
Иначе говоря, всякое мнение может быть верным в определённой ситуации.

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

Это мнение создателя языка, тех кто создал исключения. Как оно может быть неверным?
Я джавист, и теперь я крою матом тех, кто этот концепт нарушает.
А Джавад вообще не обычный язык. Если бы известную ракету контролировала в своё время не Ада, а что-то вроде Явы, она бы не развалилась в воздухе.

Знаете, ведь, поговорку: «Не ошибается только тот, кто ничего не делает»? :)

А Джавад вообще не обычный язык. Если бы известную ракету контролировала в своё время не Ада, а что-то вроде Явы, она бы не развалилась в воздухе.
— докажете?

Да и вообще, у вас как-то слишком жёстко получается — «есть два мнения: моё и неправильное» :)

Ну почему же, я потому и участвую в обсуждении, что мне интересно мнение и мотивация противной стороны.
Я на самом деле давно хотел понять, чем вызвана такая нелюбовь некоторых ява-девелоперов к явным исключениям. Я очень удивлялся, когда впервые в проекте увидел требования наподобие: у нас только непроверяемые исключения. Что? — подумал я. А в нашем прекрасном универе учившие нас прекрасные архитекторы — они что дураки все что ли? Эти практикующие в больших и сложных проектах архитекторы, они глупее каких-то доморощенных разработчиков?


Собственно, я и решил выяснить, а какого собственно чёрта. Кто эту ерунду распространяет. А может и не ерунда?
И из-за такого подхода, вот уже сколько лет во многих проектах, постоянно сталкиваешься с проблемами, вызванным таким наплевательским подходом.


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


А вообще использование RuntimeException напоминает такой подход:
image

Ну, «и так сойдёт» делают на любых языках и технологиях :)

А вот про иное мнение мне тоже интересно. И да, про трудности с расширением контракта слышал. Но что касается современных систем, можете показать/рассказать, как «правильно» обрабатывать exceptions в примере, который я описал выше?
Самый трезвый довод — в легаси системе нельзя расширить контракт. Это понятно, с легаси все работали. Но не надо переносить опыт с легаси на общий подход к разработке.

Это не основной довод. Я не пишу на Java, но обычно говорят про контракты любых интерфейсов, а не конкретно про легаси код. Я поэтому и привел пример с IStorage.


Вот есть у нас IStorage


interface IStorage
{
    public save(Array data);
}

Реализаций вообще никаких нет, мы их потом добавим. Какие там исключения задавать? Если никакие не задавать, тогда нельзя из реализации бросить проверяемый ConnectionException.
Вы предлагаете задавать какой-то абстрактный StorageException. И чем это тогда отличается от RuntimeException? Вы точно знаете, что и то и другое может вылететь, и что его можно поймать. При этом какой-нибудь NullStorage вообще не бросает исключений, а в сигнатуре они есть.


А если конкретные реализации используются, то получается так.


CacheService::set(key, value) throws CacheException
{
    ...
}

SomeDataService::getData(params) throws CacheException, ORMException, DBALException, ConnectionException
{
    key = this.getKey(params);
    data = this.cacheService.get(key);
    if (data == null) {
        data = this.orm.getData();
        this.cacheService.set(key, data);
    }
    return data;
}

SomeDataController::index() throws CacheException, ORMException, DBALException, ConnectionException, InvalidArgumentException, JSONException
{
    params = this.jsonDecoder.decode(request);
    data = this.someDataService.getData(params);
    ...
}

Все эти эксепшены расползаются по всему проекту с каждым вложенным вызовом. Они копируются в каждую новую функцию, которая использует те же классы. А потом в CacheService добавляется KeyNotFoundException, и надо по всему проекту это обновлять.

У вас что, фантазии не хватает? Почему вы всё время упираетесь в примитивное перечисление. Вы как будто специально говнокодите, чтобы доказать свою точку зрения.


Типичный представитель StorageException — SQLException. Используется много где. Куча драйверов и кэшей. Что-то я не видел, чтобы кто-то жаловался. И всё прекрасно работает. Никаких рантаймов обычно не кидает.


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

Почему вы всё время упираетесь в примитивное перечисление.

Потому что вы именно его и предлагаете: "NotFoundException, NotModifiedException, ResourseUnavailableException".


Я привел конкретный пример, почему checked exceptions считаются неудобным подходом. Перепишите его как считаете нужным и покажите, что конкретно вы предлагаете. Говорить с умным видом "вы делаете неправильно, но как надо делать я вам не скажу" это неконструктивно.


Типичный представитель StorageException — SQLException. Используется много где. Куча драйверов и кэшей. Что-то я не видел, чтобы кто-то жаловался.

В комментариях к этой статье много кто "жаловался". И ссылки приводили на другие ресурсы, где тоже на них жалуются.


И да, в сигнатуре абстрактного IStorage его объявлять нельзя.

Во-первых, это мой конкретный случай, где от результата операции зависит, что я далее буду делать. Во-вторых, это не сторадж, а запуск докер контейнеров, и там требований больше чем просто сохранить. В-третьих, у меня есть целый слой, работающий с ресурсами и их взаимосвязями, поэтому используется своя логика в т.ч. при инициализации ресурсов.
К слову, NotFoundException, NotModifiedException используются только в данном конкретном поставщике ресурсов. В других поставщиках я знать ничего не хочу про эти ошибки, они не относятся к верхнеуровневой логике. Но и просто падать тоже нельзя. Я вообще удивляюсь, почему такая базовая вещь вызывает вопросы. Возможно, стоит пройти курсы повышения квалификации? Или, например, пройти сертификационный экзамен?


Я сказал, как делать правильно в первом пункте в статье. Статья вообще не про правильные подходы, а как упростить синтаксис правильных подходов. Если будет возможность/желание написать именно про обработку ошибок, можно это организовать. Хотя я уверен, по этому поводу уже много написано. Правда то, что на Хабре представлено, я не считаю верными подходами.


Ваш IStorage это *.sql.Connection. И там прекрасно объявлены SQLException. Так что не нельзя, а нужно.

Я сказал, как делать правильно в первом пункте в статье.

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


Не хотите писать пример, как хотите. Непонятно только зачем вы вообще задаете вопросы типа "а какого собственно чёрта". Я попытался ответить, а вы не хотите слушать, да еще и на личности переходите. Вы не понимаете потому что не хотите понимать.


Я вообще удивляюсь, почему такая базовая вещь вызывает вопросы.

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


Ваш IStorage это *.sql.Connection.

Нет, мой IStorage это IStorage. Реализация может писать в базу, в файл, в AWS, или вообще никуда не писать для тестов. Нафига мне SqlException в коде, который пишет в Amazon S3?

Боже мой, да это просто аналогия. Зовите, как хотите. Я к тому, что никто не запрещает, а даже рекомендует вам писать StorageException в сигнатуре метода. И не важно Амазон он или нет.
Есть у вас StorageException — его и кидайте, и сделайте проверяемым. А всю остальную гирлянду уберите.

А я к тому, что абстрактный StorageException в сигнатуре метода не дает то, зачем вообще были придуманы checked exceptions, и то, что вы хотите получить, а потому бесполезен. Вот эти "Потому как перехватив это отклонение, можно курс выправить и продолжить работу" и всё такое. И проблему, которую я описал в примере он не убирает, просто список исключений будет меньше.

checked exceptions созданы, чтобы их обработать. Очевидно, что сервис и контроллер — независимые слои, прокидывать ошибки между ними напрямую нельзя, выкидывать из контроллера любую ошибку — тоже. Поэтому, ОБРАБАТЫВАЙТЕ. Я не буду пользоваться вашим сервисом, кидающим наружу ошибку коннекта к БД.

Я не буду пользоваться вашим сервисом, кидающим наружу ошибку коннекта к БД.

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


Поэтому, ОБРАБАТЫВАЙТЕ.

Все так и делают. Обработка заключается в записи в лог.


checked exceptions созданы, чтобы их обработать.

Я вам именно об этом и говорю. Там, где используется IStorage, checked exceptions конкретного Storage обработать нельзя. А сам он их обрабатывать не умеет, иначе бы не бросал. Это и есть ответ на ваш вопрос "какого черта все используют наследников RuntimeException".

Обработка заключается в записи в лог.

Нет.


Опять 25. Я устал.


checked exceptions конкретного Storage

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

Вот поэтому надо код показывать, а не отговариваться общими словами "вы ничего не понимаете, надо делать по-другому". Я вот тоже не понимаю, зачем надо вместо кода разводить болтовню на 20 комментов и уставать от нее.

Без null-value (например) и без исключений программы получаются гораздо надёжнее, но появляется куча синтаксического «мусора» для декларативного описания того, что «может произойти» в модуле на каждом из уровней иерархии. Большую часть «мусора» может взять на себя IDE, и тут вопрос, скорее, подбора инструментов, с которыми будет удобно, и иногда можно пожертвовать той или иной фичей языка/IDE в пользу другой.
Sign up to leave a comment.

Articles