Перевірка вхідного параметра у виклику: дублювання коду?


16

Де найкраще визначити вхідні параметри функції: абонента або самої функції?

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

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

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

Мене цікавлять не нульові умови, а валідація будь-яких вхідних змінних (від'ємне значення для sqrtфункціонування, ділення на нуль, неправильне поєднання стану та поштового індексу чи що-небудь ще)

Чи є деякі правила, як вирішити, де перевірити стан введення?

Я думаю про деякі аргументи:

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

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

Відповіді:


15

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

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

Взяти такий приклад:

public void DeletePerson(Person p)
{            
    _database.Delete(p);
}

Що означає контракт DeletePerson? Програміст може лише припускати, що у випадку Personпередачі його буде видалено. Однак ми знаємо, що це не завжди так. Що робити, якщо pце nullзначення? Що робити, якщо pв базі даних не існує? Що робити, якщо база даних відключена? Таким чином, видається , що DeletePerson добре не виконує свій контракт. Іноді він видаляє людину, а іноді викидає NullReferenceException або DatabaseNotConnectedException, а іноді нічого не робить (наприклад, якщо особу вже видалено).

Такі інтерфейси API, як відомо, важко використовувати, тому що, коли ви називаєте цей "чорний ящик" методу, можуть статися всілякі жахливі речі.

Ось декілька способів покращити контракт:

  • Додайте підтвердження та додайте виняток до договору. Це робить контракт більш сильним , але вимагає, щоб абонент виконував перевірку. Різниця, однак, полягає в тому, що тепер вони знають свої вимоги. У цьому випадку я повідомляю це за допомогою коментаря C # XML, але ви можете замість цього додати throws(Java), використати Assertабо використовувати інструмент контракту, наприклад, Код контрактів.

    ///<exception>ArgumentNullException</exception>
    ///<exception>ArgumentException</exception>
    public void DeletePerson(Person p)
    {            
        if(p == null)
            throw new ArgumentNullException("p");
        if(!_database.Contains(p))
            throw new ArgumentException("The Person specified is not in the database.");
    
        _database.Delete(p);
    }
    

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

  • Додайте перевірку та код захисно. Це робить контракт втрачаючим , адже зараз цей метод робить більше, ніж просто видаляє особу. Я змінив ім'я методу, щоб відобразити це, але це може бути не потрібно, якщо ви відповідаєте своєму API. Цей підхід має свої плюси і мінуси. Про те, що тепер ви можете зателефонувати, TryDeletePersonпроходячи у всіляких недійсних даних, і ніколи не турбуйтеся про винятки. Зрозуміло, що користувачі вашого коду, ймовірно, будуть називати цей метод занадто сильно, або це може ускладнити налагодження у випадках, коли p недійсний. Це можна вважати легким порушенням Принципу єдиної відповідальності , тому майте на увазі, якщо вибухне полум’яна війна.

    public void TryDeletePerson(Person p)
    {            
        if(p == null || !_database.Contains(p))
            return;
    
        _database.Delete(p);
    }
    
  • Поєднайте підходи. Іноді вам потрібно трохи обох, коли ви хочете, щоб зовнішні абоненти уважно дотримувались правил (змусили їх відповідати кодом), але ви хочете, щоб ваш приватний код був гнучким.

    ///<exception>ArgumentNullException</exception>
    ///<exception>ArgumentException</exception>
    public void DeletePerson(Person p)
    {            
        if(p == null)
            throw new ArgumentNullException("p");
        if(!_database.Contains(p))
            throw new ArgumentException("The Person specified is not in the database.");
    
        TryDeletePerson(p);
    }
    
    internal void TryDeletePerson(Person p)
    {            
        if(p == null || !_database.Contains(p))
            return;
    
        _database.Delete(p);
    }
    

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


Дякую за дуже приємну відповідь з прикладом. Мені подобається точка підходу "оборонного" та "суворого контракту".
srnka

7

Це питання конвенції, документації та випадку використання.

Не всі функції рівні. Не всі вимоги рівні. Не всі перевірки рівні.

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

Що робити, якщо проект знаходиться на C ++? Конвенція / традиція в C ++ полягає в документуванні передумов, а лише їх верифікації (якщо вони взагалі є) у налагодженнях.

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

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

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


Дякую за вашу відповідь. Чи можете ви, будь ласка, надати посилання на рекомендацію у стилі Guava? Я не можу гугл і дізнатися, що ти мав на увазі під цим. +1 для підтвердження меж.
srnka

Додано посилання. Це насправді не повний посібник зі стилів, а лише частина документації ненульових утиліт.
Себастьян Редл

6

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

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


Дякую за відповідь Отже, ви вважаєте, що ця функція повинна перевіряти як дійсні, так і недійсні параметри вводу у кожному випадку. Щось відрізняється від підтвердження книги прагматичного програміста: "перевірка вхідного параметра - це відповідальність за абонента". Приємно подумати "Функція повинна знати, що вважається дійсним ... Абоненти можуть цього не знати" ... Тож вам не подобається використовувати попередні умови?
srnka

1
Ви можете використовувати попередні умови, якщо хочете (див . Відповідь Себастьяна ), але я вважаю за краще захищатись і обробляти будь-який можливий внесок.
Бернар

4

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

Більше того, якщо функція оновлена ​​таким чином, що впливатиме на перевірку параметра, вам доведеться шукати кожне виникнення перевірки абонента, щоб оновити їх. Це не прекрасно :-).

Ви можете посилатися на пункт про охорону

Оновлення

Дивіться мою відповідь на кожен сценарій, який ви надали.

  • коли обробка недійсної змінної може змінюватися, добре перевірити її в стороні виклику (наприклад, sqrt()функція - у деяких випадках я можу захотіти працювати зі складним числом, тому я ставлюся до стану у виклику)

    Відповідь

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

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

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

  • коли умова перевірки однакова у кожного абонента, краще перевірити її всередині функції, щоб уникнути дублювання

    Відповідь

    Так, це хороша практика, щоб уникнути розповсюдження перевірки параметрів у вашому коді.

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

    Відповідь

    Буде добре, якщо абонент - це функція, ви не думаєте?

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

  • правильне рішення залежить від конкретного випадку

    Відповідь

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


Дякую за відповідь Sqrt () був лише прикладом, та ж поведінка з вхідним параметром може використовуватися багатьма іншими функціями. "якщо функція оновлена ​​таким чином, що впливатиме на перевірку параметра, вам доведеться шукати кожне виникнення перевірки виклику" - я не згоден з цим. Тоді ми можемо сказати те саме про повернене значення: якщо функція оновлюється таким чином, що впливатиме на значення повернення, ви повинні виправити кожного абонента ... Я думаю, що функція повинна мати одну чітко визначену задачу для виконання ... Інакше зміна абонента необхідна в будь-якому випадку.
srnka

2

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

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

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

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


+1 Дякую за відповідь. Хороша рефлексія: "Перевірка в номері виклику призводить до дублювання коду і виконується багато непотрібних робіт". І у реченні: "У більшості випадків явні тести не потрібні, оскільки внутрішня логіка та передумови абонента вже забезпечують" - що ви маєте на увазі під виразом "внутрішня логіка"? Функціональність DBC?
srnka

@srnka: Під "внутрішньою логікою" я маю на увазі обчислення та рішення у функції. По суті це реалізація функції.
Барт ван Іґен Шенау

0

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

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


Насправді, може бути краще перевірити параметр, і, якщо параметр недійсний, киньте виняток самостійно. Ось чому: клоуни, які дзвонять у вашу рутину, не намагаючись переконатися, що вони дали їй дійсні дані, - ті самі, які не будуть намагатися перевіряти код повернення помилки, який свідчить про те, що вони передали недійсні дані. Викидання винятку ВИГОЛОВАЄ проблему, яку потрібно виправити.
Джон Р. Стром
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.