Параметр для контролю, чи потрібно викидати виняток чи повертати null - хороша практика?


24

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

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

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

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

Отже, що краще?

A: Один метод з $exception_on_failureпараметром.

/**
 * @param int $id
 * @param bool $exception_on_failure
 *
 * @return Item|null
 *   The item, or null if not found and $exception_on_failure is false.
 * @throws NoSuchItemException
 *   Thrown if item not found, and $exception_on_failure is true.
 */
function loadItem(int $id, bool $exception_on_failure): ?Item;

Б: Два різних методи.

/**
 * @param int $id
 *
 * @return Item|null
 *   The item, or null if not found.
 */
function loadItemOrNull(int $id): ?Item;

/**
 * @param int $id
 *
 * @return Item
 *   The item, if found (exception otherwise).
 *
 * @throws NoSuchItemException
 *   Thrown if item not found.
 */
function loadItem(int $id): Item;

EDIT: C: Ще щось?

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

Примітки

Якщо хтось задається питанням: приклади є в PHP. Але я думаю, що питання стосується мов, якщо вони дещо схожі на PHP або Java.


5
Хоча мені доводиться працювати в PHP і я досить досвідчений, це просто погана мова, і його стандартна бібліотека є повсюдно. Прочитайте eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design для довідки. Тож насправді не дивно, що деякі розробники PHP сприймають шкідливі звички. Але я ще не бачив цього особливого дизайнерського запаху.
Полігном

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


1
@ Polygnome Ви маєте рацію, стандартні бібліотечні функції є скрізь, і існує багато бідного коду, і існує проблема "один процес на запит". Але з кожною версією стає все краще. Типи та об'єктна модель робить усі або більшість речей, які можна від неї очікувати. Я хочу побачити дженерики, спів- та протиріччя, типи зворотного виклику, які визначають підпис тощо. Багато цього обговорюється чи йдеться про спосіб їх реалізації. Я думаю, що загальний напрямок хороший, навіть незважаючи на те, що стандартні бібліотечні вигадки не пройдуть легко
donquixote

4
Одна пропозиція: замість того loadItemOrNull(id), щоб розглянути, чи loadItemOr(id, defaultItem)має для вас сенс. Якщо елемент - це рядок або число, це часто робиться.
user949300

Відповіді:


35

Ви правильно: два методи набагато кращі для цього з кількох причин:

  1. У Java підпис методу, який потенційно може винести виняток, буде включати цей виняток; інший метод не буде. Це особливо чітко дає зрозуміти, чого очікувати від одних та інших.

  2. У таких мовах, як C #, де підпис методу нічого не говорить про винятки, публічні методи все одно повинні бути задокументовані, і така документація включає винятки. Документування єдиного методу було б непростим.

    Ваш приклад ідеальний: коментарі у другому фрагменті коду виглядають набагато чіткіше, і я б навіть короткий "Елемент, якщо його знайдено (виняток є іншим)". Вниз до "Елемент". опис, який ви йому дали, є пояснювальним.

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

  4. Деякі рамки, такі як .NET Framework, встановили умови для цієї ситуації, і вони вирішують її двома методами, як ви запропонували. Єдина відмінність полягає в тому, що вони використовують відповідь, пояснену Еваном у своїй відповіді , тому int.Parse(string): intвикидає виняток, а int.TryParse(string, out int): boolні. Ця умова іменування дуже сильна у спільноті .NET, і її слід дотримуватися, коли код відповідає ситуації, яку ви описуєте.


1
Дякую, це відповідь, яку я шукав! Метою було те, що я розпочав дискусію з цього приводу для Drupal CMS, drupal.org/project/drupal/isissue/2932772 , і я не хочу, щоб це стосувалося моїх особистих уподобань, але підкріплювати їх зворотним зв'язком інших.
donquixote

4
Насправді давня традиція принаймні .NET - НЕ використовувати два методи, а замість цього вибрати правильний вибір для правильного завдання. Винятки становлять виняткові обставини, щоб не впоратися з очікуваним контрольним потоком. Не вдалося розібрати рядок у ціле число? Не дуже несподівана чи виняткова обставина. int.Parse()вважається дійсно поганим дизайнерським рішенням, саме тому команда .NET пішла вперед і впровадила TryParse.
Во

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

@Voo Дійсна точка. Насправді надання двох методів ставить вибір на абонента. Можливо, для деяких значень параметрів очікується існування елемента, тоді як для інших значень параметрів неіснуючий елемент вважається звичайним випадком. Можливо, компонент використовується в одній програмі, де очікується, що елементи існуватимуть, і в іншій програмі, де неіснуючі елементи вважаються нормальними. Можливо, той, хто телефонує, дзвонить ->itemExists($id)перед тим, як телефонувати ->loadItem($id). Або, можливо, це лише вибір, який зробили інші люди в минулому, і ми повинні сприймати це як належне.
donquixote

1
Причина 1b: Деякі мови (Haskell, Swift) вимагають, щоб в підписі була присутня нікчемність. Зокрема, Haskell має абсолютно інший тип для змінних значень ( data Maybe a = Just a | Nothing).
Джон Дворак

9

Цікавою варіацією є мова Swift. У Swift ви оголошуєте функцію "кидкою", тобто дозволено викидати помилки (схожі, але не зовсім такі, як, наприклад, винятки Java). Абонент може викликати цю функцію чотирма способами:

а. У заяві спробу / лову вилучення та обробка виключення.

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

c. Позначення виклику функції, try!що означає "Я впевнений, що цей виклик не збирається кидати". Якщо викликається, робить кидок, додаток гарантовано аварії.

г. Позначення виклику функції, try?що означає "я знаю, що функція може кинути, але мені все одно, яка саме помилка кидається". Це змінює повернене значення функції з типу " T" в тип " optional<T>", і викинутий виняток перетворюється на повертається нуль.

(а) та (г) - це справи, про які ви питаєте. Що робити, якщо функція може повернути нуль і може кинути винятки? У такому випадку тип поверненого значення був би " optional<R>" для деякого типу R, і (d) змінив би це значення на " optional<optional<R>>", що є трохи загадковим і важко зрозумілим, але зовсім чудовим. І функція Swift може повернутися optional<Void>, тому (d) можна використовувати для перекидання функцій, що повертають Void.


2

Мені подобається bool TryMethodName(out returnValue)підхід.

Це дає вам переконливу вказівку на те, чи вдався метод чи ні, і ім'я відповідає збігу методу метання винятків у блок спробу лову.

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

наприклад:

//throws exceptions when error occurs
Item LoadItem(int id);

//returns false when error occurs
bool TryLoadItem(int id, out Item item)

Приклад використання (вихід означає передачу неініціалізованого посилання)

Item i;
if(TryLoadItem(3, i))
{
    Print(i.Name);
}
else
{
    Print("unable to load item :(");
}

Вибачте, ти тут мене загубив. Як би bool TryMethodName(out returnValue)виглядав ваш " підхід" в оригінальному прикладі?
donquixote

відредаговано, щоб додати приклад
Еван

1
Отже, ви використовуєте параметр посилання за допомогою замість зворотного значення, тому у вас є повернене значення безкоштовно для успіху bool? Чи існує мова програмування, яка підтримує це ключове слово "out"?
donquixote

2
так, вибачте, що не зрозуміли, що "out" характерний для c #
Ewan

1
не завжди ні. Але що , якщо пункт 3 є нульовим? ви б не знали, чи сталася помилка чи ні
Еван

2

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


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

@Gerard, чи можете ви мені надати приклад?
Макс

@Max Якщо у вас є булева змінна b: Замість виклику foo(…, b);вам потрібно написати if (b) then foo_x(…) else foo_y(…);та повторити інші аргументи, або щось на зразок, ((b) ? foo_x : foo_y)(…)якщо мова має потрійний оператор та функції / методи першого класу. І це, можливо, навіть повторюється з кількох різних місць програми.
BlackJack

@BlackJack добре, так, я можу погодитися з вами. Я подумав про приклад, if (myGrandmaMadeCookies) eatThem() else makeFood()який так, я міг би вписати в іншу функцію, як це checkIfShouldCook(myGrandmaMadeCookies). Цікаво, чи має той нюанс, який ви згадали, пов’язаний з тим, чи передаємо ми булевим чином про те, як ми хочемо, щоб функція поводилась, як у первісному запитанні? бабусиний приклад я щойно наводив.
Макс

1
Нюанс, про який йдеться у моєму коментарі, стосується "завжди слід". Можливо перефразувати це як "якщо a, b і c застосовні, тоді [...]". Згадане вами «правило» - це чудова парадигма, але дуже варіант у випадку використання.
Джерард

1

Clean Code радить уникати булевих аргументів прапора, оскільки їх значення непрозоре на сайті виклику:

loadItem(id, true) // does this throw or not? We need to check the docs ...

тоді як окремі методи можуть використовувати виразні імена:

loadItemOrNull(id); // meaning is obvious; no need to refer to docs

1

Якби мені довелося використовувати або A, або B, тоді я використав би два окремі методи B з причин, зазначених у відповіді Арсена Мурценкоса .

Але є ще один метод 1 : У Java він називається Optional, але ви можете застосувати те саме поняття до інших мов, де ви можете визначити власні типи, якщо мова вже не забезпечує подібний клас.

Ви визначаєте свій метод так:

Optional<Item> loadItem(int id);

У своєму методі ви або повертаєтесь, Optional.of(item)якщо знайшли товар, чи повертаєтесь Optional.empty()у випадку, якщо цього не зробите. Ти ніколи не кидаєш 2 і ніколи не повернешся null.

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

  • loadItem(1).get()викине NoSuchElementExceptionабо поверне знайдений предмет.
  • Існує також loadItem(1).orElseThrow(() -> new MyException("No item 1"))використання спеціального винятку, якщо повернений Optionalпорожній.
  • loadItem(1).orElse(defaultItem)повертає або знайдений елемент, або переданий defaultItem(який також може бути null), не кидаючи винятку.
  • loadItem(1).map(Item::getName)знову поверне анну Optionalз назвою елементів, якщо вони є.
  • Є ще кілька методів, див. Javadoc дляOptional .

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


1 Я припускаю , що це що - щось на зразок try?в gnasher729s відповідь , але це не зовсім ясно для мене , як я не знаю , Swift та це , здається, функція мови , так що специфічно для Swift.

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


0

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

public Widget GetWidgetOrNull(int id) {
    // All the lines to get your item, returning null if not found
}

public Widget GetWidget(int id) {
    var widget = GetWidgetOrNull(id);

    if (widget == null) {
        throw new WidgetNotFoundException();
    }

    return widget;
}

0

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

public int GetValue(); // If you can't get the value, you'll throw me an exception
public int GetValueOrDefault(); // If you can't get the value, you'll return me 0, since it's the default for ints

TryDoSomething () також не є поганою схемою.

public bool TryParse(string value, out int parsedValue); // If you return me false, I won't bother checking parsedValue.

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

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


-1

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

Thing getThing(string x);
Thing tryGetThing (string x);

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

Thing getThingWithBrackets(string x)
{
  getThing("["+x+"]");
}
Thing tryGetThingWithBrackets (string x);
{
  return tryGetThing("["+x+"]");
}

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

Thing getThingWithBrackets(string x, bool returnNullIfFailure)
{
  return getThing("["+x+"]", returnNullIfFailure);
}

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

Thing getThingWithBrackets(string x)
{
   return getThingWithBrackets(x, false);
}
Thing tryGetThingWithBrackets(string x)
{
   return tryGetThingWithBrackets(x, true);
}

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


Ви маєте рацію: декоратори та обгортки та навіть регулярна реалізація матимуть певне дублювання коду при використанні двох методів.
donquixote

Напевно помістіть версії з і без винятку кидками в окремі пакети, і зробіть обгортки загальними?
Буде Кроуфорд

-1

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

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

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

Приклад із реального життя для мене: бібліотека з математики. Якщо у вашій математичній бібліотеці є Vectorклас, який підтримує отримання одиничного вектора в тому ж напрямку, ви повинні мати змогу ділитися на величину. Якщо величина дорівнює 0, ви маєте поділ на нульову ситуацію. У розвитку ви хочете їх наздогнати . Ви дійсно не хочете, щоб дивовижний поділ на 0 повісився. Кидання винятку - дуже популярне рішення для цього. Це майже ніколи не буває (це справді виняткова поведінка), але коли це відбувається, ти хочеш це знати.

На вбудованій платформі ви не бажаєте цих винятків. Було б більше сенсу зробити перевірку if (this->mag() == 0) return Vector(0, 0, 0);. Насправді я бачу це в реальному коді.

Тепер подумайте з точки зору бізнесу. Ви можете спробувати навчити два різні способи використання API:

// Development version - exceptions        // Embeded version - return 0s
Vector right = forward.cross(up);          Vector right = forward.cross(up);
Vector localUp = right.cross(forward);     Vector localUp = right.cross(forward);
Vector forwardHat = forward.unit();        Vector forwardHat = forward.tryUnit();
Vector rightHat = right.unit();            Vector rightHat = right.tryUnit();
Vector localUpHat = localUp.unit()         Vector localUpHat = localUp.tryUnit();

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

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

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

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