Дизайн за контрактом з використанням тверджень чи винятків? [зачинено]


123

При програмуванні за контрактом функція або метод спочатку перевіряє, чи виконуються її передумови, перш ніж розпочати роботу над своїми обов'язками, правда? Два найбільш відомих способів зробити ці перевірки на assertі exception.

  1. стверджується не вдається лише в режимі налагодження. Щоб переконатися в тому, що важливо (перевірити) всі випробувані окремі передумови, щоб переконатися, що вони насправді не виконують
  2. виняток не вдається в режимі налагодження та випуску. Це має перевагу в тому, що тестована поведінка налагодження ідентична поведінці випуску, але за неї виконується штраф за виконання.

Який ви вважаєте кращим?

Дивіться звільнене запитання тут


3
Вся суть, що стоїть за дизайном за контрактом, полягає в тому, що вам не потрібно (і, мабуть, не слід) перевіряти передумови під час виконання. Ви перевіряєте вхід до того, як передати його в метод з попередніми умовами , саме так ви дотримуєтесь свого закінчення договору. Якщо введення даних недійсне або порушує ваше завершення договору, програма, як правило, все-таки вийде з ладу через звичайний хід дій (який ви хочете).
void.pointer

Приємне запитання, але я думаю, ви повинні дійсно змінити прийняту відповідь (як показують голоси)!
DaveFar

Назавжди пізніше я знаю, але чи має насправді це питання мати тег c ++? Я шукав цю відповідь, щоб використати іншою мовою (Delpih), і не можу уявити жодної мови, яка містить винятки та твердження, які б не відповідали тим же правилам. (Ще вивчаю вказівки щодо переповнення стека.)
Ерік Г

У цій відповіді дуже коротка відповідь : "Іншими словами, винятки стосуються надійності вашої програми, тоді як твердження стосуються її коректності".
Шмуель Левін

Відповіді:


39

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

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


4
Твердження корисні принаймні в тих випадках, коли перевірка правильності була б неефективною або неефективною для належного впровадження.
Casebash

89
Сенс у твердженнях полягає не в виправленні помилок, а в оповіщенні програміста. Зберігати їх увімкнутими у версіях версій з цієї причини марно : Що б ви могли отримати, стверджуючи, що стверджується? Розробник не зможе заскочити і налагодити його. Твердження є засобом налагодження, вони не є заміною винятків (і винятки не є заміною для тверджень). Винятки попереджають програму про стан помилки. Assert попереджує розробника.
jalf

12
Але твердження слід використовувати, коли внутрішні дані були пошкоджені минулим виправленням - якщо твердження спрацьовує, ви не можете робити жодних припущень щодо стану програми, оскільки це означає, що щось є / неправильним /. Якщо твердження вимкнулося, ви не можете вважати, що будь-які дані є дійсними. Ось чому збірка версії повинна стверджувати - не говорити програмісту, де проблема, а щоб програма могла вимкнутись і не ризикувати більшими проблемами. Програма повинна просто зробити все можливе, щоб полегшити відновлення пізніше, коли дані можна довіряти.
coppro

5
@jalf, Хоча ви не можете помістити гачок у свою налагоджувальну версію у версіях версій, ви можете використовувати протокол ведення журналу, щоб розробники бачили інформацію, що стосується вашої заяви. У цьому документі ( martinfowler.com/ieeeSoftware/failFast.pdf ) Джим Шор вказує: "Пам'ятайте, що помилка, яка виникає на сайті замовника, виникла через процес тестування. Ви, ймовірно, матимете проблеми з їх відтворенням. Ці помилки: найскладніше знайти, і добре зроблене твердження, що пояснює проблему, може заощадити дні зусиль ".
Стриптинг-воїн

5
Особисто я віддаю перевагу дизайну за контрактними підходами. Винятки є захисними і проводять перевірку аргументів всередині функції. Крім того, в передумовах dbc не вказано "Я не буду працювати, якщо ви використовуєте значення поза робочим діапазоном", але "я не гарантую надати правильну відповідь, але все одно я можу це зробити". Затвердження надають розробнику зворотній зв'язок, що він викликає функцію з порушенням стану, але не заважайте їм використовувати її, якщо вони вважають, що знають краще. Порушення може спричинити винятки, але я бачу це як інша річ.
Matt_JD

194

Основне правило полягає в тому, що ви повинні використовувати твердження, коли ви намагаєтеся вловити власні помилки, та винятки, коли намагаєтеся впіймати помилки інших людей. Іншими словами, ви повинні використовувати винятки, щоб перевірити передумови для функцій загальнодоступного API та кожного разу, коли ви отримаєте будь-які дані, які є зовнішніми для вашої системи. Ви повинні використовувати твердження для функцій або даних, які є внутрішніми у вашій системі.


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

Якщо дані зовнішні, то слід використовувати винятки. У цьому конкретному випадку ви, ймовірно, також повинні вловлювати ці винятки та поводитися з ними якось розумно, а не просто відпускати програму. Також моя відповідь - це правило, а не закон природи. :) Отже, ви повинні розглядати кожен випадок окремо.
Діма

Якщо ваша функція f (int * x) містить в ній рядок x-> len, тоді f (v), де v доведено, що нуль, гарантовано руйнується. Крім того, якщо навіть раніше на v виявилося нульовим, все ж доведено називати f (v), у вас є логічне протиріччя. Це те саме, що мати a / b, де b в кінцевому підсумку доведено, що дорівнює 0. В ідеалі такий код не може скластися. Відключення чеків припущення є абсолютно нерозумним, якщо питання не є вартістю перевірок, оскільки це затьмарює місце, де припущення було порушено. Він повинен бути принаймні занесений у систему. Ви все одно повинні мати дизайн перезавантаження при аварії.
Роб

22

Принцип, якого я слідую, такий: Якщо ситуацію реально уникнути кодуванням, тоді використовуйте твердження. В іншому випадку використовуйте виняток.

Ствердження - це забезпечення дотримання Договору. Договір повинен бути справедливим, так що клієнт повинен бути в змозі забезпечити його виконання. Наприклад, ви можете вказати в договорі, що URL-адреса повинна бути дійсною, оскільки правила про те, що є, а не є дійсною, відомі та послідовні.

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

Я хотів би додати, що відмінність твердження / винятку насправді не найкращий спосіб подумати про це. Те, що ви насправді хочете думати, - це договір і як його можна виконати. У моєму прикладі URL-адреси, що найкраще робити, - це клас, який інкапсулює URL-адресу, або є нульовою чи дійсною URL-адресою. Це перетворення рядка в URL-адресу, яка виконує контракт, і виняток видається, якщо він недійсний. Метод з параметром URL набагато зрозуміліше, ніж метод з параметром String і твердженням, що вказує URL.


6

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

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

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


5

Не зовсім вірно, що "стверджувати не вдається лише в режимі налагодження".

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

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

Чи слід завжди залишати перевірені передумови включеними? Це залежить. Тобі вирішувати. Універсальної відповіді немає. Якщо ви створюєте програмне забезпечення для банку, може бути краще перервати виконання тривожним повідомленням, ніж переказувати 1 000 000 доларів замість 1000 доларів. Але що робити, якщо ви програмуєте гру? Можливо, вам потрібна вся швидкість, яку ви можете отримати, і якщо хтось отримає 1000 балів замість 10 через помилку, яку попередні умови не вловили (бо вони не ввімкнуті), велика удача.

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

Підводячи підсумок, ви можете мати твердження і все-таки отримувати винятки автоматично , якщо залишити їх увімкнутими - принаймні в Ейфелі. Я думаю, щоб зробити те ж саме в C ++, потрібно ввести його самостійно.

Дивіться також: Коли твердження повинні залишатися у виробничому коді?


1
Ваша думка, безумовно, справедлива. SO не вказав конкретну мову - у випадку C # стандартним затвердженням є System.Diagnostics.Debug.Assert, який не дає змоги лише у налагодженні налагодження і буде видалений під час компіляції у збірці Release.
yoyo

2

Існувала величезна нитка щодо включення / відключення тверджень у версіях версій на comp.lang.c ++., Які модерувались, і якщо у вас є кілька тижнів, ви можете побачити, наскільки різні думки щодо цього. :)

На відміну від coppro , я вважаю, що якщо ви не впевнені, що твердження може бути вимкнено у версії версії, то воно не повинно було стверджувати. Твердження - захист від порушення програмних інваріантів. У такому випадку, що стосується клієнта вашого коду, буде один з двох можливих результатів:

  1. Помирають з якихось збоїв типу ОС, внаслідок чого заклик перервати аборт. (Без твердження)
  2. Помер через прямий дзвінок на аборт. (З твердженням)

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

Відповідь на питання насправді набагато більше залежить від того, ким будуть клієнти API. Якщо ви пишете бібліотеку, що надає API, то вам потрібна певна форма механізму, щоб повідомити своїх клієнтів про те, що вони неправильно використовували API. Якщо ви не надаєте дві версії бібліотеки (одну з твердженнями, одну без), то стверджувати, що це малоймовірний вибір.

Однак я особисто не впевнений, що і в цьому випадку я би пішов з винятками. Винятки краще підходять для того, де може відбутися відповідна форма відновлення. Наприклад, можливо, ви намагаєтеся виділити пам'ять. Коли ви виберете виняток 'std :: bad_alloc', можливо, можна звільнити пам'ять і спробувати ще раз.


2

Я окреслив свій погляд на стан справи тут: Як ви підтверджуєте внутрішній стан об'єкта? . Як правило, заявляйте свої претензії та киньте на порушення з боку інших. Для відключення тверджень у версії версій ви можете:

  • Вимкнути твердження про дорогі чеки (наприклад, перевірити, чи замовлений діапазон)
  • Тримайте тривіальні чеки (наприклад, перевірка нульового вказівника чи булевого значення)

Звичайно, у складах випусків, невдалих тверджень та невиконаних винятків слід обробляти іншим способом, ніж у складах налагодження (де це може просто викликати std :: abort). Кудись запишіть журнал помилки (можливо, у файл), повідомте замовнику, що сталася внутрішня помилка. Замовник зможе надіслати вам файл журналу.


1

Ви запитуєте про різницю між помилками проектування та часом виконання.

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

винятки - сповіщення "ей користувач, щось пішло не так" (очевидно, ви можете зашифрувати їх, щоб користувач ніколи не повідомляв їх), але вони розроблені так, щоб вони виникали під час запуску, коли користувач Joe використовує додаток.

Тож, якщо ви думаєте, що зможете виправити всі ваші помилки, використовуйте лише винятки. Якщо ви думаєте, що не можете ..... використовуйте винятки. Ви все ще можете використовувати налагодження налагодження, щоб зменшити кількість винятків.

Не забувайте, що багато передумов будуть надані користувачем даними, тому вам знадобиться хороший спосіб інформування користувача, що його дані не були б корисними. Для цього вам часто потрібно буде повертати дані про помилки в стек викликів до бітів, з якими він взаємодіє. Затвердження не будуть корисними тоді - вдвічі, якщо ваша програма n-ярусна.

Нарешті, я б не використовував жодного - коди помилок набагато кращі для помилок, на які, на вашу думку, трапляються регулярно. :)


0

Я віддаю перевагу другому. Хоча ваші тести, можливо, пройшли нормально, Мерфі каже, що щось несподіване піде не так. Отже, замість того, щоб отримувати виняток під час фактичного помилкового виклику методу, ви в кінцевому підсумку відстежуєте NullPointerException (або еквівалент) 10 стекових кадрів глибше.


0

Попередні відповіді правильні: використовуйте винятки для функцій публічного API. Єдиний раз, коли ви можете скористатись цим правилом, - це перевірка обчислювально дорого. У такому випадку ви можете поставити це як твердження.

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


0

Ви повинні використовувати обидва. Твердження для вашої зручності як розробника. Винятки охоплюють речі, які ви пропустили або не очікували під час виконання.

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

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

#ifdef DEBUG
#define RETURN_IF_FAIL(expr)      do {                      \
 if (!(expr))                                           \
 {                                                      \
     fprintf(stderr,                                        \
        "file %s: line %d (%s): precondition `%s' failed.", \
        __FILE__,                                           \
        __LINE__,                                           \
        __PRETTY_FUNCTION__,                                \
        #expr);                                             \
     ::print_stack_trace(2);                                \
     return;                                                \
 };               } while(0)
#define RETURN_VAL_IF_FAIL(expr, val)  do {                         \
 if (!(expr))                                                   \
 {                                                              \
    fprintf(stderr,                                             \
        "file %s: line %d (%s): precondition `%s' failed.",     \
        __FILE__,                                               \
        __LINE__,                                               \
        __PRETTY_FUNCTION__,                                    \
        #expr);                                                 \
     ::print_stack_trace(2);                                    \
     return val;                                                \
 };               } while(0)
#else
#define RETURN_IF_FAIL(expr)
#define RETURN_VAL_IF_FAIL(expr, val)
#endif

Якщо мені потрібна перевірка аргументів часу виконання, я б це зробив:

char *doSomething(char *ptr)
{
    RETURN_VAL_IF_FAIL(ptr != NULL, NULL);  // same as assert(ptr != NULL), but returns NULL if it fails.
                                            // Goes away when debug off.

    if( ptr != NULL )
    {
       ...
    }

    return ptr;
}

Я не думаю, що я бачив у питанні ОП щось, що стосується C ++. Я вважаю, що це не повинно бути включено у вашу відповідь.
ForceMagic

@ForceMagic: Питання було тегом C ++ у 2008 році, коли я опублікував цю відповідь, і фактично тег C ++ був видалений лише 5 годин тому. Незважаючи на те, код ілюструє поняття, незалежне від мови.
indiv

0

Я спробував синтезувати декілька інших відповідей тут із власними поглядами.

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

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

Знову аргумент, що простіше відлагоджувати твердження: Слід стека від правильно названого винятку так само легко читати, як і твердження. Хороший код повинен охоплювати лише певні типи винятків, тому винятки не повинні залишатися непоміченими через те, що вони потрапляють. Однак я думаю, що Java іноді змушує вас зловити всі винятки.


0

Для мене, як правило, застосовують вирази ствердження, щоб знайти внутрішні помилки та винятки для зовнішніх помилок. Ви можете отримати багато користі з наступної дискусії Грега звідси .

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

Щоб зрозуміти, як твердження вписуються в програму, розгляньте рутину в програмі C ++, яка збирається відкинути покажчик. Тепер слід перевіряти, чи вказівник є NULL перед перенаправленням, чи він повинен стверджувати, що вказівник не NULL, а потім продовжувати та відкидати його незалежно?

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

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

Мета поводження з помилками (винятки) - дворазова. Перший - повідомити користувачеві (або іншому клієнту) про виявлення помилки у введенні програми та що це означає. Друга мета - відновити додаток після виявлення помилки до чітко визначеного стану. Зауважте, що сама програма не помиляється в цій ситуації. Зрозуміло, програма може перебувати в неідеальному стані або навіть у стані, в якому не можна нічого корисного зробити, але немає помилки програмування. Навпаки, оскільки стан відновлення помилок передбачено дизайном програми, саме програма може впоратися.

PS: Ви можете перевірити подібне питання: Виняток проти твердження .


-1

Дивіться також це питання :

У деяких випадках, твердження вимкнено при будівництві для звільнення. Ви можете не мати контролю над цим (інакше ви могли б будувати з твердженнями), тому може бути хорошою ідеєю зробити це так.

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

Зазвичай я викидаю виняток у if-заяві, щоб взяти на себе роль затвердження у випадку їх відключення

assert(value>0);
if(value<=0) throw new ArgumentOutOfRangeException("value");
//do stuff
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.