Исключения - «что случилось» против «что делать»

19

Мы используем исключения, чтобы позволить потребителю кода обработать неожиданное поведение полезным способом. Обычно исключения строятся вокруг сценария «что случилось», например FileNotFound(мы не смогли найти указанный вами файл) или ZeroDivisionError(мы не смогли выполнить 1/0операцию).

Что, если есть возможность указать ожидаемое поведение потребителя?

Например, представьте, что у нас есть fetchресурс, который выполняет HTTP-запрос и возвращает полученные данные. И вместо ошибок типа ServiceTemporaryUnavailableили RateLimitExceededмы просто RetryableErrorпредложили бы потребителю, чтобы он просто повторил запрос и не заботился о конкретной ошибке. Таким образом, мы в основном предлагаем действия вызывающей стороне - «что делать».

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

Роман Боднарчук
источник
3
Разве HTTP уже не делает это? 503 - временный сбой при ответе, поэтому запрашивающая сторона должна повторить попытку, 404 - принципиальное отсутствие, поэтому повторять нет смысла, 301 означает «перемещен навсегда», поэтому следует повторить попытку, но с другим адресом и т. Д.
Килиан Фот
7
Во многих случаях, если мы действительно знаем «что делать», мы можем просто заставить компьютер делать это автоматически, и пользователю даже не нужно знать, что что-то пошло не так. Я предполагаю, что всякий раз, когда мой браузер получает 301, он просто переходит на новый адрес, не спрашивая меня.
Ixrec
@Ixrec - тоже думал об этом. однако потребитель может не захотеть ждать другого запроса и игнорировать элемент или полностью потерпеть неудачу.
Роман Боднарчук
1
@RomanBodnarchuk: я не согласен. Это все равно что сказать, что человеку не нужно знать китайский, чтобы говорить по-китайски. HTTP - это протокол, и ожидается, что и клиент, и сервер его знают и следуют ему. Вот как работает протокол. Если только одна сторона знает и соблюдает это, то вы не можете общаться.
Крис Пратт
1
По правде говоря, вы пытаетесь заменить свои исключения блоком catch. Вот почему мы перешли к исключениям - не более того if( ! function() ) handle_something();, но имея возможность обработать ошибку там, где вы на самом деле знаете контекст вызова - то есть сообщить клиенту, чтобы он вызывал системного администратора, если ваш сервер вышел из строя, или перезагрузите автоматически, если соединение разорвано, но предупредите вас в В случае звонящего это еще один микросервис. Пусть блоки захвата справятся с ловлей.
Себб

Ответы:

47

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

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

Например, даже исключения, приведенные в вашем вопросе, демонстрируют ваше ошибочное предположение: a ServiceTemporaryUnavailableприравнивается к «попробуйте еще раз позже» и RateLimitExceededприравнивается к «вау, успокойтесь, возможно, отрегулируйте параметры таймера и повторите попытку через несколько минут». Но пользователь может также захотеть поднять какую-то тревогу ServiceTemporaryUnavailable(которая указывает на проблему с сервером), а не для RateLimitExceeded(которая не делает).

Дайте им выбор .

Гонки легкости с Моникой
источник
4
Я согласен. Сервер должен только правильно передавать информацию. С другой стороны, документация должна четко указывать надлежащий порядок действий в таких случаях.
Нил
1
Один крошечный недостаток в том, что некоторые хакеры, определенные исключения, могут многое рассказать им о том, что делает ваш код, и они могут использовать его, чтобы выяснить способы его использования.
Pharap
3
@Pharap Если ваши хакеры имеют доступ к самому исключению вместо сообщения об ошибке, вы уже потеряны.
CorsiKa
2
Мне нравится этот ответ, но в нем отсутствует то, что я считаю требованием исключений ... если бы вы знали, как восстановить, это не было бы исключением! Исключением может быть только случай, когда вы не можете что-то с этим сделать: неверный ввод, недопустимое состояние, недопустимая защита - вы не можете исправить это программно.
CorsiKa
1
Согласовано. Если вы настаиваете на указании возможности повторной попытки, вы всегда можете просто унаследовать соответствующие конкретные исключения RetryableError.
Сапи
18

Предупреждение! Программист C ++ приходит сюда с возможно разными идеями о том, как следует обрабатывать исключения, пытаясь ответить на вопрос, который, безусловно, касается другого языка!

Учитывая эту идею:

Например, представьте, что у нас есть ресурс fetch, который выполняет HTTP-запрос и возвращает полученные данные. И вместо ошибок, таких как ServiceTeilitaryUnavailable или RateLimitExceeded, мы просто вызовем RetryableError, предлагая потребителю просто повторить запрос и не заботиться о конкретной ошибке.

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

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

Независимо от того, что произойдет, скажем, что действие состоит в том, чтобы сообщить о том, что случилось с пользователем, и подсказать ему, что он хочет с этим сделать («повторите попытку, загрузите другой файл, отмените»).

Метатель против Ловца

Такой порядок действий применяется независимо от того, с какой ошибкой мы столкнулись в этом случае. Он не встроен в общую идею ошибки синтаксического анализа, он не встроен в общую идею неудачной загрузки плагина. Он встроен в идею возникновения таких ошибок в точном контексте загрузки файла (сочетание загрузки файла и сбоя). Поэтому, как правило, я рассматриваю это, грубо говоря, как catcher'sответственность за определение направления действий в ответ на выброшенное исключение (например: подсказка пользователю параметров), а не за thrower's.

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

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

Метатель слепых

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

Перевернутые обязанности и обобщение ловца

Подумав об этом более тщательно, я пытался представить себе тот тип кодовой базы, где это может стать соблазном. Мое воображение (возможно, ошибочное) состоит в том, что ваша команда по-прежнему играет роль «потребителя» и реализует большую часть вызывающего кода. Возможно, у вас есть много разрозненных транзакций (много tryблоков), которые могут столкнуться с одинаковыми наборами ошибок, и все они, с точки зрения проектирования, должны привести к единому курсу действий по восстановлению.

Принимая во внимание мудрый совет из Lightness Races in Orbit'sтонкого ответа (который, я думаю, на самом деле исходит из продвинутого библиотечно-ориентированного мышления), у вас все еще может возникнуть искушение создавать исключения «что делать», только ближе к сайту восстановления транзакций.

Из этого здесь можно найти промежуточный, общий сайт обработки транзакций, который фактически централизует проблемы «что делать», но все же в контексте отлова.

введите описание изображения здесь

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

Тем не менее, он может быть ответственным за централизацию действий пользователя в ответ на множество возможных ошибок, и все же в контексте ловли, а не выбрасывания. Простой пример (псевдокод Python-ish, и я совсем не опытный разработчик Python, так что может быть более идиоматический способ сделать это):

def general_catcher(task):
    try:
       task()
    except SomeError1:
       # do some uniformly-designed recovery stuff here
    except SomeError2:
       # do some other uniformly-designed recovery stuff here
    ...

[Надеюсь, с лучшим именем, чем general_catcher]. В этом примере вы можете передать функцию, содержащую задачу, которую нужно выполнить, но при этом воспользоваться преимуществами обобщенного / унифицированного поведения перехвата для всех типов исключений, которые вас интересуют, и продолжать расширять или изменять часть «что делать». Вам нравится из этого центрального местоположения и все еще в catchконтексте, где это обычно поощряется. Лучше всего то, что мы можем помешать участкам метания относиться к себе с тем, «что делать» (сохраняя понятие «метатель слепых»).

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

ChrisF
источник
2
+1. Я никогда не слышал об идее «Blind Thrower» как таковой, но она вписывается в то, как я отношусь к обработке исключений: констатируйте возникшую ошибку, не думайте о том, как ее следует обрабатывать. Когда вы отвечаете за полный стек, трудно (но важно!) Четко разделить обязанности, и вызываемый абонент отвечает за уведомление о проблемах, а вызывающий - за решение проблем. Вызываемый знает только то, что его попросили сделать, а не почему. Обработка ошибок должна быть сделана в контексте «почему», следовательно: в вызывающей стороне.
Sjoerd Job Postmus
1
(Также: я не думаю, что ваш ответ специфичен для C ++, но в целом применим к обработке исключений)
Sjoerd Job Postmus
1
@SjoerdJobPostmus Yay спасибо! «Слепой метатель» - это просто глупая аналогия, которую я привел здесь - я не очень умен и не быстр в усвоении сложных технических концепций, поэтому я часто хочу найти небольшие образы и аналогии, чтобы попытаться объяснить и улучшить свое собственное понимание вещей. Может быть, когда-нибудь я попытаюсь найти способ написать небольшую книгу по программированию, наполненную множеством рисунков мультфильмов. :-D
1
Хех, это миленькое изображение. Маленький мультипликационный персонаж, носящий повязку на глаза и выбрасывающий исключения в форме бейсбола, не уверенный, кто их поймает (или даже будет ли их вообще пойман), но исполняющий свой долг слепого метателя.
Blacklight Shining
1
@DrunkCoder: Пожалуйста, не вандализируйте свои сообщения. У нас в Интернете уже достаточно материалов. Если у вас есть веская причина для удаления, пометьте свое сообщение модератором и представьте свое мнение.
Роберт Харви
2

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

Например, рассмотрим функцию:

Response fetchUrl(URL url, RetryPolicy retryPolicy);

Я могу передать RetryPolicy.noRetries () или RetryPolicy.retries (3) или что угодно. В случае повторяющейся ошибки, он будет консультироваться с политикой, чтобы решить, следует ли повторить попытку.

Уинстон Эверт
источник
Это, правда, не имеет большого отношения к выдаче исключений обратно на сайт вызовов. Вы говорите о чем-то немного ином, что хорошо, но на самом деле это не часть вопроса ..
Легкость в гонках с Моникой
@LightnessRacesinOrbit, наоборот. Я представляю это как альтернативу идее отбрасывать исключения на сайт вызова. В примере OP fetchUrl выдаст исключение RetryableException, и вместо этого я говорю, что вы должны сообщить fetchUrl, когда следует повторить попытку.
Уинстон Эверт
2
@WinstonEwert: Хотя я согласен с LightnessRacesinOrbit, я также понимаю вашу точку зрения, но думаю о ней как об ином способе представления того же элемента управления. Но учтите, что вы, вероятно, захотите пройти new RetryPolicy().onRateLimitExceeded(STOP).onServiceTemporaryUnavailable(RETRY, 3)или что-то в этом роде, потому что это RateLimitExceededможет потребовать обработки иначе ServiceTemporaryUnavailable. После этого я подумал: лучше бросить исключение, потому что оно дает более гибкий контроль.
Sjoerd Job Postmus
@SjoerdJobPostmus, я думаю, что это зависит. Вполне возможно, что логика ограничения скорости и повторных попыток делают в вашей библиотеке, и в этом случае я думаю, что мой подход имеет смысл. Если имеет смысл просто оставить это своему звонящему, непременно бросьте.
Уинстон Эверт