Винятки - "що трапилось" проти "що робити"


19

Ми використовуємо винятки, щоб дозволити споживачеві коду корисно керувати несподіваною поведінкою. Зазвичай винятки складаються навколо сценарію "що трапилося" - наприклад FileNotFound(нам не вдалося знайти вказаний вами файл) або ZeroDivisionError(нам не вдалося виконати 1/0операцію).

Що робити, якщо є можливість уточнити очікувану поведінку споживача?

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

Ми робимо це не часто, оскільки не знаємо всіх випадків використання споживачів. Але уявіть, що це якийсь конкретний компонент, який ми знаємо найкращим способом дій для абонента - тож ми повинні потім використовувати підхід "що робити"?


3
Хіба HTTP вже не робить цього? 503 - це тимчасова невідповідь, тому запитувач повинен повторити спробу, 404 - це принципова відсутність, тому не має сенсу повторювати спробу, 301 означає "переїхати назавжди", тому вам слід повторити спробу, але з іншою адресою тощо.
Кіліан Foth

7
У багатьох випадках, якщо ми дійсно знаємо, «що робити», ми можемо просто змусити комп’ютер робити це автоматично і користувачеві навіть не потрібно знати, що щось пішло не так. Я вважаю, що кожен раз, коли мій браузер отримує номер 301, він просто переходить на нову адресу, не запитуючи мене.
Іксрек

@Ixrec - теж мав таку ж ідею. однак споживач може не захотіти чекати чергового запиту та ігнорувати товар або повністю відмовитись.
Роман Боднарчук

1
@ РоманБоднарчук: Я не згоден. Це як сказати, що людині не потрібно знати китайську мову, щоб говорити по-китайськи. HTTP - це протокол, і клієнт, і сервер, як очікується, знають його та слідкують за ним. Ось так працює протокол. Якщо лише одна сторона знає і дотримується її, то ви не можете спілкуватися.
Кріс Пратт

1
Це чесно звучить так, ніби ви намагаєтесь замінити свої винятки блоком вилову. Ось чому ми перейшли до винятків - не більше if( ! function() ) handle_something();, але будучи в змозі впоратися з помилкою де-небудь, де ви насправді знаєте контекст виклику - тобто, скажіть клієнту, щоб зателефонував адміністратору sys, якщо ваш сервер вийшов з ладу або перезавантажився автоматично, якщо з'єднання перестало, але попередити вас у у випадку, якщо абонент - це ще одна мікросервіс. Нехай блоки захоплення обробляють лову.
Себб

Відповіді:


47

Але уявіть, що це якийсь конкретний компонент, який ми знаємо найкращим способом дій для абонента.

Це майже завжди не вдається хоча б одному з ваших абонентів, для яких така поведінка неймовірно дратує. Не вважайте, що ви знаєте найкраще. Розкажіть своїм користувачам, що відбувається, а не те, що ви вважаєте, що вони повинні зробити з цим. У багатьох випадках вже зрозуміло, яким повинен бути розумний хід дій (і, якщо це не так, внесіть пропозицію у керівництво користувача).

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

Дайте їм вибір .


4
Я згоден. Сервер повинен лише належним чином передавати інформацію. Документація з іншого боку повинна чітко окреслювати належний хід дій у таких випадках.
Ніл

1
Невеликий недолік у цьому полягає в тому, що для деяких хакерів певні винятки можуть сказати їм досить багато про те, що робить ваш код, і вони можуть використовувати його для з'ясування способів його використання.
Фарап

3
@Pharap Якщо ваші хакери мають доступ до самого винятку замість повідомлення про помилку, ви вже втратили.
corsiKa

2
Мені подобається ця відповідь, але пропущено те, що я вважаю вимогою винятків ... якби ви знали, як відновити, це не було б винятком! Виняток має бути лише тоді, коли ви не можете щось з цим зробити: недійсний ввід, недійсний стан, недійсна безпека - ви не можете їх виправити програмно.
corsiKa

1
Домовились. Якщо ви наполягаєте на тому, щоб вказати, чи можлива повторна спроба, ви завжди можете просто зробити успадковані відповідні конкретні винятки RetryableError.
сапі

18

Увага! С ++ програміст, що приходить сюди з можливо різними уявленнями про те, як слід робити обробку винятків, намагаючись відповісти на питання, що, безумовно, стосується іншої мови!

Враховуючи цю ідею:

Наприклад, уявіть, що у нас є ресурс отримання, який виконує HTTP-запит і повертає отримані дані. І замість помилок, таких як ServiceTemporaryUnavailable або 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контексті, коли це зазвичай рекомендується. Найкраще, що ми можемо утримати сайти, які кидають, не стосуватися себе "що робити" (збереження поняття "сліпий метальник").

Якщо ви не знайдете жодної з цих пропозицій тут корисною, і все-таки є сильна спокуса кинути винятки "що робити", головним чином пам’ятайте, що це принаймні дуже антиідіоматично, а також потенційно відштовхує узагальнене мислення.


2
+1. Я ніколи не чув про ідею "Сліпого метателя" як таку, але вона вписується в те, як я вважаю поводження з винятками: констатуйте помилку, не думайте, як з нею поводитися. Коли ви несете відповідальність за повний стек, важко (але важливо!) Чітко розділяти відповідальність, і виклик відповідає за повідомлення про проблеми, а абонент - за вирішення проблем. Слухач знає лише, що просили зробити, а не чому. Поводження з помилками повинно бути зроблено в контексті "чому", отже: у абонента.
Jojo Postb

1
(Також: Я не думаю, що ваша відповідь специфічна для C ++, але застосовна для обробки винятків взагалі)
Sjoerd Job Postmus

1
@SjoerdJobPostmus Так, спасибі! "Сліпий метальник" - це просто неприємна аналогія, яку я придумав тут - я не дуже розумний або швидкий у перетравленні складних технічних концепцій, тому мені часто хочеться знайти мало зображень та аналогій, щоб спробувати пояснити та покращити власне розуміння речей. Можливо, якихось день я можу спробувати попрацювати над тим, щоб написати маленьку книгу програмування, наповнену безліччю мультиплікаційних малюнків. :-D

1
Хе, це приємне маленьке зображення. Маленький персонаж з мультфільмів, одягнений із зав'язаними очима та викидаючи винятки у формі бейсболу, не впевнений, хто їх зловить (або навіть, чи їх взагалі зловить), але виконує свій обов'язок сліпого метателя.
Blacklight Shining

1
@DrunkCoder: Будь ласка, не скасовуйте своїх публікацій. У нас в Інтернеті вже є достатня кількість матеріалів для закуски. Якщо у вас є вагомі причини для видалення, позначте свою публікацію для уваги модератора і зробіть свою справу.
Роберт Харві

2

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

Наприклад, розглянемо функцію:

Response fetchUrl(URL url, RetryPolicy retryPolicy);

Я можу передавати RetryPolicy.noRetries () або RetryPolicy.retries (3) чи будь-що інше. У випадку помилки, яку можна повторно перевірити, вона проконсультується з політикою, щоб вирішити, чи слід її повторити.


Але це насправді не має великого відношення до повернення винятків на сайт для викликів. Ви говорите про щось дещо інше, що добре, але насправді це не питання.
Гонки легкості з Монікою

@LightnessRacesinOrbit, навпаки. Я представляю це як альтернативу ідеї повернення винятків на сайт для викликів. У прикладі ОП, fetchUrl буде кидати RetryableException, і я кажу, замість цього, ви повинні сказати fetchUrl, коли він повинен повторити спробу.
Вінстон Еверт

2
@WinstonEwert: Хоча я згоден із LightnessRacesinOrbit, я також бачу вашу думку, але думаю про це як про інший спосіб представлення одного і того ж елемента управління. Але врахуйте, що ви, мабуть, хочете пройти new RetryPolicy().onRateLimitExceeded(STOP).onServiceTemporaryUnavailable(RETRY, 3)або щось подібне, тому що, RateLimitExceededможливо, потрібно поводитися інакше ServiceTemporaryUnavailable. Після того, як виписав це, моя думка: краще кинути виняток, оскільки це дає більш гнучкий контроль.
Jojo Postbut

@SjoerdJobPostmus, я думаю, це залежить. Цілком можливо, що обмеження швидкості та повторна логіка мають значення у вашій бібліотеці, і в такому випадку, я думаю, мій підхід має сенс. Якщо має сенс просто залишити це своєму абоненту, будь-яким чином киньте його.
Вінстон Еверт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.