Мой обработчик, если вы посмотрите код — не панацея. Это всего лишь небольшой метод, превносящий нечто вроде синтаксического сахара в код, не более. Он как раз заменяет предобработку ошибки в месте использования, уменьшая загроможение кода.
Использование RuntimeException в вашем случае — исключительно вынужденная мера, применяемая только для совместимости. Разработка нового контракта, и тем более новой системы — не требует использования костылей, и возводить такой подход в правило нельзя. Костыли вам требуются, потому что изначальный сервис был криво спроектирован.
И ещё раз: при разработке нового функционала объявлять RuntimeException, который означает баг, нет смысла. И выкидывать несуществующий баг, даже необъявленый — грубейшая ошибка дизайна.
Слушайте, я нигде не говорил, что системные ошибки надо объявлять. Напротив, их как раз трогать не надо. Если у вас деление на ноль, то это код некорректный, и его нужно срочно остановить и исправить. При этом Список нигде не кидает проверяемые исключения, т.к. у него нет бизнес-логики, независимой от вызова. Если вызов некорректный — ошибка в коде.
Я настаиваю на объявлении именно бизнес ошибок, если они могут быть, конечно. При этом предпочтительным вариантом являются именно проверяемые исключения, т.к. непровереямые означают баги.
Ну во-первых, OutOfMemoryError а не эксепшн. Это ошибка ДЖВМ, я уже несколько раз спорил о том, можно ли вообще что-то делать в ДЖВМ, когда происходят именно Эрроры. На мой взгляд, в случае с OutOfMemoryError продолжать категорически нельзя.
Впрочем, его-то как раз ловить не надо, пусть падает сам.
Но если вдруг случайно поймали — в томкате например, надо звать java.lang.System#exit прям сразу после лога.
Не сказать, чтобы я прям топил, но в своих проектах применяю, обычно это не так страшно смотрится почему-то, один-два типа хватает слихвой. Если нет — то наибольший общий знаменатель вполне подходит.
Ваш пример гипертрофирован. Вполне достаточно объявить что-то типа CalculateAmountException — общий предок CannotCalculateAmount и CouldNotCollectAmountPct. Хотя даже эти случаи у вас надуманны.
Если происходит IndexOutOfBoundException, DivisionByZeroException, то их очевидно здесь ловить не надо. Это ошибка программирования, если они происходят, а значит продолжать в принципе небезопасно. mybean.getId() должен быть проверен на нуль ещё до какого-либо использования: mybean.getId().getClass()
Из UnsupportedOperationException | ClassCastException | NullPointerException |
IllegalArgumentException может произойти только NullPointerException, но его проверили выше.
В общем здесь максимум одно проверяемое исключение кидается. Потенциально — вообще без.
Насчёт JavaEE — спорно, они как раз сначала долго всё согласуют, но надо сказать, в итоге у них получается хорошо в последнее время.
А в Spring я вот прям сейчас борюсь с последствиями излишней гибкости.
Кстати, по поводу сервиса и его переезда на RPC: сделайте корректную обработку ошибок, если коннект валится — то всё, это как нуллпойнтер в локальной версии. То есть сервис неработоспособен. В целом конечно всё от ситуации зависит, но в изначальной логике всегда должен быть какой-то канал сообщения ошибок. Иначе она не расширяема по дизайну.
Ага, классический пример. Если надо расширить контракт — расширяйте, в смысле берите новый расширенный контракт и реализуйте. Потому что от вашей новой реализации на старом контракте никто не хочет неожиданностей. Они не заявлены явно, не надо их создавать неявно. Выйдут новые версии клиентов нового контракта — тогда всё будет ок. А ломать логику через коленку не надо.
Обычно приводят список, который вдруг стал работать по сети. Но, во-первых, это как раз нарушение S в SOLID — вообще то грубейшая ошибка. Да ещё строить на этом нарушении какие-то доказательства, хуже не придумаешь.
Если же прям сильно хочется именно старый контракт — то будьте добры соблюдайте его. Он никаких райнтаймов не кидал. А если будет кидать, то это сугубо ваша вина, потому что вы корявый программист, это вы нарушаете SOLID. Поэтому в наказание вам — закопать вашу реализацию, чтоб не было дурного примера.
В самой Java API использование checked/unchecked много где неконсистентно.
Ява с точки зрения API хороша, но далеко не идеальна. Меня, например, удивляет, почему разработав сносную библиотеку коллекций, сановцы не продумали создание отдельных интерфейсов для немутабельных версий. Зачем делать эти грязные хаки в виде выброса UnsupportedOperationException в модифицирующих методах списков, множеств и т.п? Нельзя было для такого случая просто не применять эти методы, т.е. создать отдельные ридонли-интерфейсы?
Да, насчёт клиентского кода или вызывающего, надо прояснить.
Здесь я не имею в виду непосредственно предшествующий метод. Это может быть и весь колл-стек, и даже другой стек в случае, когда таска запущена асинхронно и запулена в текущий поток. Обработку ошибок в итоге должен сделать кто-то из всей вызывающей структуры.
Да, ещё момент: игнорирование ошибок в методе — это пренебрежение к пользователю вашего метода. Вы экономите своё время за счёт времени вашего контрагента. Он точно рад не будет.
на основе этих сведений можно только упать с записью в лог
Зачем же падать? Может у вас это сохранение было опционально, или имеется другой резервный склад, чуть поодаль.
К тому же если просто упасть, то вы не освободите коннект, ваш поток завершится, сокет будет висеть. Если поток был из пула и произойдёт ещё ~64к запусков потока и попыток подключиться — ваше приложение вообще перестанет отвечать, если к тому времени оно ещё будет живо. Так что падать с лапками кверху — это самое глупое, что можно сделать.
Для сети можно то же самое действие повторить
По идее это ваш склад должен делать сам.
Исключения в сигнатуре — чтобы явно видеть, на что рассчитывать, когда зовёшь метод. Из моей практики, не называя имён: сервис не объявил вообще никаких исключений, ни проверяемых, ни не-. В документации по этому поводу тишина. Сервис корный, очень системный. В итоге вызвав этот метод, я получаю ошибки только после тяжёлого развёртывания на стенде. Какого х* спрашивается?
Причем из логики (кода) сервиса становится понятно, что кинуть рантайм-эксепшн, если в системе не настроены кое-какие параметры — в порядке вещей. И эти параметры не в проперти-файлах, а нетривиально задаются отдельно. У меня вопрос, я методом тыка должен об этом узнавать?
writeInt(int v) не очень удачный пример, потому что здесь однотипные операции с одной и той же ошибкой могут завершиться. И прокидывает наверх потому что это чистый агрегат/делегатор. Здесь нет никакой особой логики внутри.
Это ошибочное мнение.
Некоторые так не считают: например NoRegrets, mwizard
RuntimeException потому так и называется, что он не предназначен для обработки бизнес-логикой. Это теперь уже мы имеем корявое наследие говно-кода в виде разных его обёрток, которые приходится учитывать при обработке ошибок. Исходно для бизнес-ошибок RuntimeException не предназначен. RuntimeException — это ошибка в программе (например NPE), когда надо просто падать.
Если в сигнатуре метода прописаны основные ошибки, которые он генерит, то задача перебора становится реальной. Вы можете посмотреть на аналогию, бывшую в C++: если вы заявляете, что метод генерит только предопределённые исключения, то при попытке выкинуть что-то иное происходит системный сбой, обычно с немедленным завершением работы. И это логично, потому что все нижележащие ошибки метод должен был обработать сам. А то, что не может — объявляет явным образом в своей сигнатуре, то бишь контракте. То же самое происходит по логике вещей в Яве: вы явно выкидываете проверяемые исключения, а непроверяемые — это всегда должен быть только результат системного сбоя / ошибки программирования. Использование RuntimeException, означающего сбой, для целей бизнес-логики — это дизайнерский баг (или баг в мозгах).
Итак, ошибки, ожидаемые от сервиса, известны, они объявлены. Тогда подключая соответсвующую либу/фреймворк, вы обязаны эти ошибки обработать. В свою очередь, наружу вы выставляете только то, что надо знать и уметь вашему клиенту, в том числе типы ошибок. И они не должны вообще никак повторять внутренние ошибки внешних либ. Иногда он даже не сможет определить тип этих ошибок, потому что ему соответствующие классы недоступны. И они не должны быть доступны. Потому что это не его дело знать, что произошло внутри, об этом должен был позаботиться непосредственный вызывающий — вы.
Подключая провайдера, вы должны учитывать его специфику, в т.ч. контракт по ошибкам и если он не совпадает с вашим (для общего случая именно так) — то вы делаете маппинг его ошибок на свои. И тут, конечно уже надо включать голову, потому что вашему клиенту не интересны ваши взаимоотношения с вашим субподрядчиком.
Если у вас склад недоступен — то вам в общем-то не важно, что послужило этому причиной: уборщица кабель отключила или диск сбоит. Вы поставлены перед фактом: сейчас сохранить не могу, нет доступа к хранилищу. И дальше уже ваша ответственность на основе этих сведений принимать решение.
Как выше было замечено, в яве обычно сохраняют исходную причину сбоя, так что при большом желании можно посмотреть детали. Я к примеру, всегда разворачиваю InvocationTargetException, потому что само исключение не имеет никакого смысла.
ConnectionError обрабатывается в зависимости от контекста. Для абстракции склада нас не интересует какого типа ошибка произошла в реализации (уборщицу будут наказывать люди после просмотра логов, а не ваш сервис), нас интересует, что определённого типа операции нам временно/постоянно недоступны.
Склад недоступен — это когда даже начать операцию нельзя, например сервис вообще не отвечает. Операция прервана — значит взаимодействие есть, но состояние склада вызывает вопросы: можно ли к нему обращаться в следующий раз.
Ошибку соединения по сети и ошибку прожига CD обрабатывать надо по-разному
Как по разному? У вас склад не работает и так, и эдак. То, что потом, посмотрев логи, вы позвоните админу/уборщице с приказом восстановить сеть или тому же админу поручите заменить/прочистить диски в устройстве — это дело дестятое, организационное и к вашему сервису уже не имеет отношения, никак в данную секунду ему не поможет.
Если ваш сторадж умеет воспринимать ошибки реализации, а он обязан, то можно рассчитывать на некие обобщенные ошибки сохранения.
Если вы на это не можете полагаться, выкиньте этот сторадж и возьмите другой более правильный тулкит.
Что-то классдоадеры не падают, когда по конкретному пути не могут найти конкретный класс. И исключение кидают. Но их предусмотрительные дети пробуют поискать в другом месте.
Вблизи — на значит непосредственно после. Это должно быть хотя бы не на вершине стека, в идеале — в той же либе, где была ошибка, потому что там ещё понятен контекст.
Конечно не надо обрабатывать исключение в месте выброса, иначе зачем вообще выбрасывать.
Мой обработчик, если вы посмотрите код — не панацея. Это всего лишь небольшой метод, превносящий нечто вроде синтаксического сахара в код, не более. Он как раз заменяет предобработку ошибки в месте использования, уменьшая загроможение кода.
Использование RuntimeException в вашем случае — исключительно вынужденная мера, применяемая только для совместимости. Разработка нового контракта, и тем более новой системы — не требует использования костылей, и возводить такой подход в правило нельзя. Костыли вам требуются, потому что изначальный сервис был криво спроектирован.
И ещё раз: при разработке нового функционала объявлять RuntimeException, который означает баг, нет смысла. И выкидывать несуществующий баг, даже необъявленый — грубейшая ошибка дизайна.
Слушайте, я нигде не говорил, что системные ошибки надо объявлять. Напротив, их как раз трогать не надо. Если у вас деление на ноль, то это код некорректный, и его нужно срочно остановить и исправить. При этом Список нигде не кидает проверяемые исключения, т.к. у него нет бизнес-логики, независимой от вызова. Если вызов некорректный — ошибка в коде.
Я настаиваю на объявлении именно бизнес ошибок, если они могут быть, конечно. При этом предпочтительным вариантом являются именно проверяемые исключения, т.к. непровереямые означают баги.
Возможно, вы сможете предложить лучшие практики
Ну во-первых, OutOfMemoryError а не эксепшн. Это ошибка ДЖВМ, я уже несколько раз спорил о том, можно ли вообще что-то делать в ДЖВМ, когда происходят именно Эрроры. На мой взгляд, в случае с OutOfMemoryError продолжать категорически нельзя.
Впрочем, его-то как раз ловить не надо, пусть падает сам.
Но если вдруг случайно поймали — в томкате например, надо звать java.lang.System#exit прям сразу после лога.
Не сказать, чтобы я прям топил, но в своих проектах применяю, обычно это не так страшно смотрится почему-то, один-два типа хватает слихвой. Если нет — то наибольший общий знаменатель вполне подходит.
Ваш пример гипертрофирован. Вполне достаточно объявить что-то типа CalculateAmountException — общий предок CannotCalculateAmount и CouldNotCollectAmountPct. Хотя даже эти случаи у вас надуманны.
Если происходит IndexOutOfBoundException, DivisionByZeroException, то их очевидно здесь ловить не надо. Это ошибка программирования, если они происходят, а значит продолжать в принципе небезопасно. mybean.getId() должен быть проверен на нуль ещё до какого-либо использования: mybean.getId().getClass()
Из UnsupportedOperationException | ClassCastException | NullPointerException |
IllegalArgumentException может произойти только NullPointerException, но его проверили выше.
В общем здесь максимум одно проверяемое исключение кидается. Потенциально — вообще без.
Насчёт JavaEE — спорно, они как раз сначала долго всё согласуют, но надо сказать, в итоге у них получается хорошо в последнее время.
А в Spring я вот прям сейчас борюсь с последствиями излишней гибкости.
Кстати, по поводу сервиса и его переезда на RPC: сделайте корректную обработку ошибок, если коннект валится — то всё, это как нуллпойнтер в локальной версии. То есть сервис неработоспособен. В целом конечно всё от ситуации зависит, но в изначальной логике всегда должен быть какой-то канал сообщения ошибок. Иначе она не расширяема по дизайну.
Имелось в виду игнорирование объявления ошибок в сигнатуре.
Ага, классический пример. Если надо расширить контракт — расширяйте, в смысле берите новый расширенный контракт и реализуйте. Потому что от вашей новой реализации на старом контракте никто не хочет неожиданностей. Они не заявлены явно, не надо их создавать неявно. Выйдут новые версии клиентов нового контракта — тогда всё будет ок. А ломать логику через коленку не надо.
Обычно приводят список, который вдруг стал работать по сети. Но, во-первых, это как раз нарушение S в SOLID — вообще то грубейшая ошибка. Да ещё строить на этом нарушении какие-то доказательства, хуже не придумаешь.
Если же прям сильно хочется именно старый контракт — то будьте добры соблюдайте его. Он никаких райнтаймов не кидал. А если будет кидать, то это сугубо ваша вина, потому что вы корявый программист, это вы нарушаете SOLID. Поэтому в наказание вам — закопать вашу реализацию, чтоб не было дурного примера.
Ява с точки зрения API хороша, но далеко не идеальна. Меня, например, удивляет, почему разработав сносную библиотеку коллекций, сановцы не продумали создание отдельных интерфейсов для немутабельных версий. Зачем делать эти грязные хаки в виде выброса UnsupportedOperationException в модифицирующих методах списков, множеств и т.п? Нельзя было для такого случая просто не применять эти методы, т.е. создать отдельные ридонли-интерфейсы?
Да, насчёт клиентского кода или вызывающего, надо прояснить.
Здесь я не имею в виду непосредственно предшествующий метод. Это может быть и весь колл-стек, и даже другой стек в случае, когда таска запущена асинхронно и запулена в текущий поток. Обработку ошибок в итоге должен сделать кто-то из всей вызывающей структуры.
Да, ещё момент: игнорирование ошибок в методе — это пренебрежение к пользователю вашего метода. Вы экономите своё время за счёт времени вашего контрагента. Он точно рад не будет.
Как один из наиболее близких по принципам язык
В статье есть два момента.
Всё остальное вы придумали.
Зачем же падать? Может у вас это сохранение было опционально, или имеется другой резервный склад, чуть поодаль.
К тому же если просто упасть, то вы не освободите коннект, ваш поток завершится, сокет будет висеть. Если поток был из пула и произойдёт ещё ~64к запусков потока и попыток подключиться — ваше приложение вообще перестанет отвечать, если к тому времени оно ещё будет живо. Так что падать с лапками кверху — это самое глупое, что можно сделать.
По идее это ваш склад должен делать сам.
Исключения в сигнатуре — чтобы явно видеть, на что рассчитывать, когда зовёшь метод. Из моей практики, не называя имён: сервис не объявил вообще никаких исключений, ни проверяемых, ни не-. В документации по этому поводу тишина. Сервис корный, очень системный. В итоге вызвав этот метод, я получаю ошибки только после тяжёлого развёртывания на стенде. Какого х* спрашивается?
Причем из логики (кода) сервиса становится понятно, что кинуть рантайм-эксепшн, если в системе не настроены кое-какие параметры — в порядке вещей. И эти параметры не в проперти-файлах, а нетривиально задаются отдельно. У меня вопрос, я методом тыка должен об этом узнавать?
writeInt(int v)
не очень удачный пример, потому что здесь однотипные операции с одной и той же ошибкой могут завершиться. И прокидывает наверх потому что это чистый агрегат/делегатор. Здесь нет никакой особой логики внутри.RuntimeException потому так и называется, что он не предназначен для обработки бизнес-логикой. Это теперь уже мы имеем корявое наследие говно-кода в виде разных его обёрток, которые приходится учитывать при обработке ошибок. Исходно для бизнес-ошибок RuntimeException не предназначен. RuntimeException — это ошибка в программе (например NPE), когда надо просто падать.
Если в сигнатуре метода прописаны основные ошибки, которые он генерит, то задача перебора становится реальной. Вы можете посмотреть на аналогию, бывшую в C++: если вы заявляете, что метод генерит только предопределённые исключения, то при попытке выкинуть что-то иное происходит системный сбой, обычно с немедленным завершением работы. И это логично, потому что все нижележащие ошибки метод должен был обработать сам. А то, что не может — объявляет явным образом в своей сигнатуре, то бишь контракте. То же самое происходит по логике вещей в Яве: вы явно выкидываете проверяемые исключения, а непроверяемые — это всегда должен быть только результат системного сбоя / ошибки программирования. Использование RuntimeException, означающего сбой, для целей бизнес-логики — это дизайнерский баг (или баг в мозгах).
Итак, ошибки, ожидаемые от сервиса, известны, они объявлены. Тогда подключая соответсвующую либу/фреймворк, вы обязаны эти ошибки обработать. В свою очередь, наружу вы выставляете только то, что надо знать и уметь вашему клиенту, в том числе типы ошибок. И они не должны вообще никак повторять внутренние ошибки внешних либ. Иногда он даже не сможет определить тип этих ошибок, потому что ему соответствующие классы недоступны. И они не должны быть доступны. Потому что это не его дело знать, что произошло внутри, об этом должен был позаботиться непосредственный вызывающий — вы.
Подключая провайдера, вы должны учитывать его специфику, в т.ч. контракт по ошибкам и если он не совпадает с вашим (для общего случая именно так) — то вы делаете маппинг его ошибок на свои. И тут, конечно уже надо включать голову, потому что вашему клиенту не интересны ваши взаимоотношения с вашим субподрядчиком.
Если у вас склад недоступен — то вам в общем-то не важно, что послужило этому причиной: уборщица кабель отключила или диск сбоит. Вы поставлены перед фактом: сейчас сохранить не могу, нет доступа к хранилищу. И дальше уже ваша ответственность на основе этих сведений принимать решение.
Как выше было замечено, в яве обычно сохраняют исходную причину сбоя, так что при большом желании можно посмотреть детали. Я к примеру, всегда разворачиваю InvocationTargetException, потому что само исключение не имеет никакого смысла.
ConnectionError обрабатывается в зависимости от контекста. Для абстракции склада нас не интересует какого типа ошибка произошла в реализации (уборщицу будут наказывать люди после просмотра логов, а не ваш сервис), нас интересует, что определённого типа операции нам временно/постоянно недоступны.
Склад недоступен — это когда даже начать операцию нельзя, например сервис вообще не отвечает. Операция прервана — значит взаимодействие есть, но состояние склада вызывает вопросы: можно ли к нему обращаться в следующий раз.
Как по разному? У вас склад не работает и так, и эдак. То, что потом, посмотрев логи, вы позвоните админу/уборщице с приказом восстановить сеть или тому же админу поручите заменить/прочистить диски в устройстве — это дело дестятое, организационное и к вашему сервису уже не имеет отношения, никак в данную секунду ему не поможет.
Для приведенных вами случаев:
НашСкладНедоступенОшибка
СохранениеПрервалосьОшибка
Более общее — ОшибкаСохраненияНаСкладе.
Если ваш сторадж умеет воспринимать ошибки реализации, а он обязан, то можно рассчитывать на некие обобщенные ошибки сохранения.
Если вы на это не можете полагаться, выкиньте этот сторадж и возьмите другой более правильный тулкит.
Через несколько не стоит — можно через один, можно не протаскивать, а выправить ситуацию, если это возможно.
Что-то классдоадеры не падают, когда по конкретному пути не могут найти конкретный класс. И исключение кидают. Но их предусмотрительные дети пробуют поискать в другом месте.
Вблизи — на значит непосредственно после. Это должно быть хотя бы не на вершине стека, в идеале — в той же либе, где была ошибка, потому что там ещё понятен контекст.
Конечно не надо обрабатывать исключение в месте выброса, иначе зачем вообще выбрасывать.