Чи слід перевірити зворотне значення виклику методу, навіть якщо я знаю, що метод не може повернути неправильний вхід?


30

Мені цікаво, чи варто захищати від зворотного значення виклику методу, підтверджуючи, що вони відповідають моїм очікуванням, навіть якщо я знаю, що метод, який я викликаю, відповідатиме таким очікуванням.

ПОДАРУТЬ

User getUser(Int id)
{
    User temp = new User(id);
    temp.setName("John");

    return temp;
}

ПОТРІБНО ЗРОБИТИ

void myMethod()
{
    User user = getUser(1234);
    System.out.println(user.getName());
}

АБО

void myMethod()
{
    User user = getUser(1234);

    // Validating
    Preconditions.checkNotNull(user, "User can not be null.");
    Preconditions.checkNotNull(user.getName(), "User's name can not be null.");

    System.out.println(user.getName());
}

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


Мій висновок з усіх відповідей (сміливо прийдіть до своїх):

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

1
Прагніть до цього покриття коду% 100!
Джаред Берроуз

9
Чому ви зосереджуєтесь лише на одній проблемі ( Null)? Це, мабуть, однаково проблематично, якщо ім'я ""(не нульове, а порожній рядок) або "N/A". Або ви можете довіряти результату, або ви повинні бути належним чином параноїком.
MSalters

3
У нашій кодовій базі у нас є схема імен: getFoo () обіцяє не повертати null, а getFooIfPresent () може повернути null.
pjc50

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

Відповіді:


42

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

Якщо ви якось точно знаєте, що getUser ніколи, ніколи, ніколи не зміниться у майбутньому, то так, це марно витрачати час на його перевірку, стільки ж, як витрачати час на перевірку часу, який iмає значення 3 одразу після i = 3;заяви. Насправді ви цього не знаєте. Але ви знаєте деякі речі:

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

  • Чи є функція getUser частиною документально підтвердженого API з конкретним контрактом, а myMethod - лише клієнт вказаного API в іншій кодовій базі? Якщо це так, ви можете прочитати цю документацію, щоб дізнатися, чи слід перевіряти повернені значення (або попередньо перевірити вхідні параметри!) Чи дійсно безпечно сліпо слідувати щасливому шляху. Якщо документація цього не дає зрозуміти, попросіть технічне обслуговування виправити це.

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

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


2
Також: Якщо ви змінили getUserпізніше, щоб він міг повернути нуль, чи явний чек дав би вам інформацію, яку ви не могли отримати з "NullPointerException" та номер рядка?
користувач253751

Здається, ви можете перевірити дві речі: (1) що метод працює належним чином, як і в ньому, немає помилок. (2) Що метод поважає ваш договір. Я можу бачити, як перевірити, що №1 є надмірним. Ви повинні замість цього зробити тести №1. Ось чому я б не підтвердив i = 3 ;. Тож моє запитання стосується №2. Мені подобається ваша відповідь на це, але в той же час це відповідь на ваше судження. Чи бачите ви будь-яку іншу проблему, яка виникає з явним підтвердженням ваших контрактів, крім того, щоб витратити трохи часу на написання заяви?
Дідьє А.

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

1
Не можу йти проти натовпу. Ця відповідь, безумовно, є найбільш правильною у висвітленні теми мого питання. Я хотів би додати, що, читаючи всі відповіді, особливо @MikeNakis, я думаю, що вам слід стверджувати свої попередні умови, принаймні в режимі налагодження, коли у вас є будь-яка з двох останніх точок кулі Ixrec. Якщо ви зіткнулися з першою точкою кулі, то, ймовірно, зайве, щоб стверджувати свої передумови. Нарешті, з огляду на чіткий контракт, як-от належний документ, безпеку типу, тест на одиницю чи перевірку після умови, ви не повинні стверджувати свої передумови, що було б надмірним.
Дідьє А.

22

Відповідь Іксрека хороша, але я прийму інший підхід, оскільки вважаю, що варто задуматися. Для цілей цієї дискусії я розповім про твердження, оскільки це назва, за якою ваша Preconditions.checkNotNull()традиційно відома.

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

Мій девіз:

Питання, яке ви завжди повинні задавати собі, це не "чи варто це стверджувати?" але "чи я щось забув стверджувати?"

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

Але є навіть винятки з цього правила. Що цікаво, якщо взяти приклад Іксрека, існує така ситуація, коли цілком бажано дотримуватися int i = 3;твердження, яке iдійсно знаходиться в певному діапазоні. Це ситуація, коли iвін може бути змінений тим, хто намагається виконати різні сценарії "що-що", а наступний код покладається на iте, щоб мати значення в певному діапазоні, і це не очевидно, швидко переглянувши наступний код, що допустимий діапазон -. Таким чином, він застосовується машиною , тому він є ідеальною гарантією, і він переробляється разом з кодом , тому він залишається завжди доречним.int i = 3; assert i >= 0 && i < 5; може спочатку здатися безглуздим, але якщо ви подумаєте про це, це говорить про те, що ви можете грати i, а також говорить про те, в якому діапазоні ви повинні залишитися. Твердження - найкращий тип документації, оскільки

Ваш getUser(int id)приклад не дуже віддалений концептуальний родич цього assign-constant-and-assert-in-rangeприкладу. Ви бачите, за визначенням, помилки трапляються, коли речі виявляють поведінку, яка відрізняється від тієї поведінки, яку ми очікували. Щоправда, ми не можемо стверджувати все, тому іноді буде налагодження. Але це добре встановлений факт у галузі, що чим швидше ми виявляємо помилку, тим менше це коштує. Питання, які вам слід задати, полягають не лише в тому, наскільки ви впевнені, що getUser()ніколи не повернете нулеве значення (і ніколи не повернете користувача з нульовим іменем), але також і те, які погані речі відбудуться з рештою коду, якщо він буде Факт, одного дня повернеш щось несподіване, і як легко, швидко переглянувши решту коду, щоб точно знати, що від нього чекали getUser().

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

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


2
Так. Стверджуйте це. Я прийшов сюди, щоб дати більш-менш цю відповідь. ++
RubberDuck

2
@SteveJessop Я б це виправив ... && sureness < 1).
Майк Накіс

2
@MikeNakis: це завжди кошмар, коли пишуть одиничні тести, повернене значення, дозволене інтерфейсом, але неможливо насправді генерувати з будь-яких входів ;-) У будь-якому випадку, коли ти на 101% впевнений, що ти маєш рацію, то ти точно не помилишся !
Стів Джессоп

2
+1 для зазначення речей, які я зовсім забув згадати у своїй відповіді.
Іксрек

1
@DavidZ Це правильно, моє запитання передбачає перевірку часу виконання. Але сказати, що ви не довіряєте методу налагодження і довіряєте йому в prod - це, можливо, хороший погляд на цю тему. Тож твердження у стилі C може бути хорошим способом вирішити цю ситуацію.
Дідьє А.

8

Певного немає.

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

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

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


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

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

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

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

5

Якщо null є дійсним зворотним значенням методу getUser, то ваш код повинен обробляти це за допомогою If, а не винятку - це не винятковий випадок.

Якщо null не є дійсним значенням повернення методу getUser, то в getUser є помилка - метод getUser повинен викидати виняток або іншим чином виправлятись.

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


Null не є дійсним зворотним значенням getUser. Оглядаючи getUser, я побачив, що реалізація ніколи не повернеться до нуля. Моє запитання: чи слід довіряти моїй інспекції та чи не мати перевірку у своєму методі, чи слід сумніватися в моїй інспекції, і все-таки перевірити. Можливо також, що метод зміниться в майбутньому, порушивши мої очікування щодо не повернення нуля.
Дідьє А.

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

@didibus Перефразовуючи свою відповідь ... Смайлик смайлик не є дійсним зворотним значенням getUser. Оглядаючи getUser, я побачив, що реалізація ніколи не поверне смайлик . Моє запитання: чи слід довіряти моїй інспекції та чи не мати перевірку у своєму методі, чи слід сумніватися в моїй інспекції, і все-таки перевірити. Можливо також, що метод зміниться в майбутньому, порушивши мої очікування не повернути смайлик смайлик . Не обов'язок абонента підтвердити, що викликана функція виконує свій договір.
Вінс О'Салліван

@ VinceO'Sullivan Схоже, що "контракт" знаходиться в центрі питання. І що моє питання стосується неявних контрактів, а не явних. Метод, який повертає int, я не хочу стверджувати, що я не отримую рядок. Але, якщо я хочу лише позитивні дані, чи варто? Мається на увазі лише те, що метод не повертає негативний int, тому що я його перевірив. З підпису чи документації незрозуміло, що цього немає.
Дідьє А.

1
Не має значення, чи договір неявний або явний. Не є завданням коду виклику перевірити, що викликаний метод повертає дійсні дані при наданні дійсних даних. У прикладі ви знаєте, що негативні inits - це невірні відповіді, але чи знаєте ви, що позитивний int є правильним? Скільки тестів вам дійсно потрібно, щоб довести, що метод працює? Єдиний правильний курс - припустити, що метод правильний.
Вінс О'Салліван

5

Я б заперечував, що передумова вашого запитання відключена: навіщо для початку використовувати нульове посилання?

Модель об'єкта нульового спосіб повернути об'єкти , які по суті нічого не робити: замість повернення NULL для вашого користувача, повертає об'єкт , який не представляє «немає користувача.»

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

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

Ось приклад перевірки коду на null:

User u = getUser();
String displayName = "";
if (u != null) {
  displayName = u.getName();
}
return displayName;

Ось приклад коду з використанням нульового об’єкта:

return getUser().getName();

Що зрозуміліше? Я вважаю, що другий.


Хоча питання позначено , варто зазначити, що нульовий шаблон об'єкта дійсно корисний для C ++ при використанні посилань. Посилання на Java більше схожі на покажчики C ++, і дозволяють робити null. Посилання на C ++ не можуть містити nullptr. Якщо хотілося б мати щось аналогічне нульовій посиланню в C ++, єдиний спосіб я знаю, щоб це досягти.


Null - це лише приклад того, що я стверджую. Це могло б бути мені потрібне не порожнє ім’я. Моє запитання все-таки стосуватиметься, чи підтверджуєте ви це? Або це може бути щось, що повертає Int, і я не можу мати негативний int, або воно повинно бути в заданому діапазоні. Не думайте про це як про нульову проблему.
Дідьє А.

1
Чому метод повертає неправильне значення? Єдине місце, на яке слід це перевірити, - це межа вашої програми: інтерфейси з користувачем та з іншими системами. Інакше, тому ми маємо винятки.

Уявіть, що у мене було: int i = getCount; float x = 100 / i; Якщо я знаю, getCount не повертає 0, тому що я написав метод або перевірив його, чи слід пропустити чек на 0?
Дідьє А.

@didibus: ви можете пропустити чек, якщо getCount() документи гарантують, що він тепер не поверне 0, і не зробить цього в майбутньому, за винятком випадків, коли хтось вирішить внести переломну зміну і піде шукати оновлення всього, що вимагає його впоратися новий інтерфейс. Побачити, що зараз робить код, це не допоможе, але якщо ви збираєтесь перевірити код, то код для перевірки - це одиничний тест getCount(). Якщо вони перевіряють його не повертає 0, то ви знаєте, що будь-яка майбутня зміна для повернення 0 вважатиметься переломною зміною, оскільки тести не зможуть.
Стів Джессоп

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

1

Як правило, я б сказав, що це залежить в основному від наступних трьох аспектів:

  1. надійність: чи може метод виклику впоратися зі nullзначенням? Якщо nullзначення може призвести до RuntimeExceptionя, рекомендую перевірити - якщо складність низька, а методи виклику / виклику створюються тим самим автором, і nullне очікується (наприклад, обидва privateв одному пакеті).

  2. Відповідальність за код: якщо розробник A відповідає за getUser()метод, а розробник B використовує його (наприклад, як частину бібліотеки), я настійно рекомендую перевірити його значення. Просто тому, що розробник B може не знати про зміну, яка призводить до потенційного nullповернення.

  3. складність: чим вище загальна складність програми чи середовища, тим більше я рекомендую перевірити повернене значення. Навіть якщо ви впевнені, що це не може бути nullсьогодні в контексті методу виклику, можливо, вам доведеться змінити getUser()інший випадок використання. Після того, як минули кілька місяців або років, і було додано кілька тисяч рядків кодів, це може бути досить пасткою.

Крім того, я рекомендую задокументувати потенційні nullзначення повернення у коментарі JavaDoc. Завдяки виділенню описів JavaDoc у більшості IDE, це може бути корисним попередженням для тих, хто використовує getUser().

/** Returns ....
  * @param: id id of the user
  * @return: a User instance related to the id;
  *          null, if no user with identifier id exists
 **/
User getUser(Int id)
{
    ...
}

-1

Ви точно не повинні.

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


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