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


114

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

Наприклад, зараз я працюю над мобільним додатком із таким кодом:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
    }

    //2. Is rows empty? - This will be the case if this is the first appointment / some other unknown reason.
    if(rows.Count == 0)
    {
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

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

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

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

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

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

NB: Це можна вважати подібним до цього питання, однак на відміну від цього питання, я хотів би відповісти, припускаючи, що немає строків.

TLDR: Чи варто зайти до того, щоб додати зайвий код, щоб зробити його потенційно більш надійним у майбутньому?



95
У розробці програмного забезпечення речі, які ніколи не повинні відбуватися, трапляються постійно.
Роман Райнер

34
Якщо ви знаєте if(rows.Count == 0), що ніколи не відбудеться, тоді ви можете створити виняток, коли це станеться, - і перевірте, чому ваше припущення стало неправильним.
кнут

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

9
Чому б rowsколи-небудь було недійсним? Ніколи не є вагомих причин, принаймні в .NET, щоб колекція була недійсною. Порожній , звичайно, але недійсний . Я б кинув виняток, якщо він rowsє недійсним, тому що це означає, що абонент має затримку в логіці.
Kyralessa

Відповіді:


176

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

Назвіть першу умову А, а другу умову В.

Ви спочатку говорите:

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

Тобто: A означає B, або, більш кажучи, базові (NOT A) OR B

І потім:

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

Тобто: NOT((NOT A) AND B). Застосуйте закон Деморгана, щоб отримати, (NOT B) OR Aякий B означає, A.

Отже, якщо обидва ваші твердження вірні, тоді A означає B, а B означає A, а значить, вони повинні бути рівними.

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

Тож тепер питання: як написати код? Справжнє питання: що таке заявлений договір методу ? Питання про те, чи є умови зайвими - це червона оселедець. Справжнє питання полягає в тому, "чи я склав обґрунтований контракт, і чи мій метод чітко реалізує цей договір?"

Давайте розглянемо декларацію:

public static CalendarRow AssignAppointmentToRow(
    Appointment app,    
    List<CalendarRow> rows)

Він є загальнодоступним, тому він повинен бути надійним для поганих даних від довільних абонентів.

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

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

Договір параметру списку:

  • Нульовий список ОК
  • Список з одним або кількома елементами в ньому добре
  • Список, в якому немає елементів, є неправильним і не повинен бути можливим.

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

Моя порада: почніть спочатку. Цей API має інтерфейс машинки цукерки, написаний по всьому. (Вираз є зі старої історії про цукеркові машини в Microsoft, де і ціни, і вибір - двоцифрові цифри, і набрати "85", що є ціною товару 75, дуже просто, і ви отримаєте неправильний предмет. Факт забави: так, я насправді це зробив помилково, коли намагався дістати гумку з торгового автомата в Microsoft!)

Ось як скласти розумний контракт:

Зробіть свій метод корисним як для його побічного ефекту, так і для його повернення, а не для обох.

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

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

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

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

Тепер я говорив лише про ваш конкретний випадок, і ви задали загальне запитання. Ось ось кілька додаткових загальних порад:

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

  • Отримайте інструмент для тестового покриття. Переконайтесь, що ваші тести охоплюють кожен рядок коду. Якщо є непокритий код, то або у вас відсутній тест, або у вас мертвий код. Мертвий код напрочуд небезпечний, оскільки зазвичай він не призначений бути мертвим! Неймовірно жахливий недолік безпеки Apple "go to fail" пару років тому приходить відразу на думку.

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

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


24
Я ніколи не думав про подібні методи раніше, думаючи про це зараз, мені здається, я намагаюся висвітлити кожну подію, коли насправді, якщо людина / метод, що називає мій метод, не передає мені те, що мені потрібно, то я не повинен ' t намагаюся виправити свої помилки (коли я насправді не знаю, що вони мали на меті), я повинен просто вийти з ладу. Дякуємо за це, цінний урок було засвоєно!
KidCode

4
Те, що метод повертає значення, не означає, що воно також не повинно бути корисним для його побічного ефекту. У паралельному коді здатність функцій повертати обидві часто життєво важлива (уявіть собі, CompareExchangeбез такої здатності!), І навіть у не одночасних сценаріях щось на кшталт "додайте запис, якщо його немає, і поверніть або передане запис або той, що існував "може бути зручнішим, ніж будь-який підхід, який не використовує як побічний ефект, так і повернене значення.
supercat

5
@KidCode Так, Ерік дуже добре пояснює речі чітко, навіть деякі дуже складні теми. :)
Мейсон Уілер

3
@supercat Звичайно, але про це теж важче міркувати. По-перше, ви, ймовірно, захочете подивитися на те, що не змінює глобальний стан, уникаючи, таким чином, проблем з одночасністю та корупції держави. Якщо це не розумно, ви все одно хочете виділити ці два - це дає чіткі місця, де суперечливість є проблемою (і, таким чином, вважатиметься надзвичайно небезпечною), і місця, де вона обробляється. Це було однією з основних ідей у ​​оригінальному документі про ООП і є основою корисності акторів. Жодного релігійного правила - просто віддайте перевагу розділенню двох, коли це має сенс. Зазвичай це робиться.
Луань

5
Це дуже корисний пост майже для всіх!
Адріан Бузея

89

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

Обидва ifтвердження перевіряють різні речі. Обидва є відповідними тестами залежно від ваших потреб.

Розділ 1 перевіряє та виправляє nullоб'єкт. Бічна примітка: Створення списку не створює жодних дочірніх елементів (наприклад, CalendarRow).

Розділ 2 перевіряє та виправляє помилки користувачів та / або впровадження. Тільки тому, що у вас немає, це List<CalendarRow>не означає, що у вас є якісь елементи у списку. Користувачі та виконавці зроблять те, чого ви не уявляєте лише тому, що їм це дозволяється, незалежно від того, чи це має сенс для вас чи ні.


1
Насправді я сприймаю "дурні хитрості користувачів", щоб означати вклад. ТАК ви ніколи не повинні довіряти вкладенню. Навіть тільки поза вашим класом. Перевірте! Якщо ви думаєте, що це лише майбутнє питання, я би радий зламати вас сьогодні.
candied_orange

1
@CandiedOrange, це був намір, але формулювання не передало спробу гумору. Я змінив формулювання.
Адам Цукерман

3
Тут просто швидке запитання, якщо це помилка впровадження / погані дані, чи не повинен я просто вийти з ладу, замість того, щоб намагатися виправити їх помилки?
KidCode

4
@KidCode Кожен раз, коли ви намагаєтеся відновити, "виправити їх помилки", вам потрібно зробити дві речі, повернутися до відомого хорошого стану і не втрачати цінні дані. Дотримуючись цього правила, в цьому випадку виникає запитання: чи цінний вклад списку нульових рядків?
candied_orange

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

35

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

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

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


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

3
@KidCode: Добре спостереження. Ваше власне мислення тут насправді набагато розумніше, ніж багато відповідей тут, включаючи той, який ви прийняли.
ЖакB

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

23

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

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

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

Щоб отримати більше думок, прочитайте цей короткий твір: Не прибивайте програму до прямої позиції


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

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

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

Можливо, було б заманливо написати функцію на зразок цієї:

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    // Make sure the input is nonnegative
    if(n < 0) {
        n = 1; // Replace the negative input with an input that will work
    }

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

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

Замість цього краще написати чек так:

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    // Make sure the input is nonnegative
    if(n < 0) {
        throw new ArgumentException("Can't have negative inputs to Fibonacci");
    }

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

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


1
Чи не має c # особливий виняток для вказівки недійсних аргументів або аргументів поза діапазоном?
JDługosz

@ JDługosz Так! C # має ArgumentException , а Java має IllegalArgumentException .
Кевін

У питанні було використання c #. Ось C ++ (посилання на резюме) для повноти.
JDługosz

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

2
@Luaan: це не відповідальність прикладної функції.
whatsisname

11

Чи слід додати зайвий код? Немає.

Але те, що ви описуєте, не є зайвим кодом .

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

Особисто я переконаний у цій методології, але як і до всього, з чим треба бути обережним. Візьмемо, наприклад, C ++ std::vector::operator[]. Відклавши на хвилину реалізацію режиму налагодження VS, ця функція не проводить перевірку меж. Якщо ви запитаєте елемент, який не існує, результат не визначений. Користувач повинен надати дійсний векторний індекс. Це цілком навмисно: ви можете "увімкнути" перевірку меж, додавши його на дзвінок, але якби operator[]реалізація мала виконати його, ви не зможете "відмовитися". Як досить низький рівень, це має сенс.

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

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

  • живе мовою надвисокого рівня (скажімо, JavaScript, а не C)
  • сидить на межі інтерфейсу
  • не є критичним для продуктивності
  • безпосередньо приймає введення користувача

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

Ця тема вивчається далі у Вікіпедії ( https://en.wikipedia.org/wiki/Defanish_programming ).


9

Тут доречні дві з десяти команд програмування:

  • Не слід вважати, що введення правильне

  • Ви не повинні робити код для подальшого використання

Тут перевірка на null не є "внесенням коду для подальшого використання". Створення коду для подальшого використання - це такі речі, як додавання інтерфейсів, оскільки ви вважаєте, що вони можуть бути корисними "в якийсь день". Іншими словами, заповідь полягає в тому, щоб не додавати шари абстракції, якщо вони зараз не потрібні.

Перевірка на null не має нічого спільного з майбутнім використанням. Це пов'язано з командою №1: не вважайте, що введення буде правильним. Ніколи не припускайте, що ваша функція отримає деякий підмножина вводу. Функція повинна відповідати логічним способом, незалежно від того, наскільки хибні та заблукані входи.


5
Де ці команди програмування. У вас є посилання? Мені цікаво, бо я працював над декількома програмами, які підписалися на другу із цих заповідей, і кількома, хто цього не зробив. Незмінно ті, хто підписався на заповідь, швидшеThou shall not make code for future use стикалися з питаннями збереження , незважаючи на інтуїтивну логіку заповіді. Я знайшов, що в кодуванні реального життя ця заповідь ефективна лише в коді, де ви керуєте списком функцій і термінами, і переконайтеся, що вам не потрібен майбутній шукаючий код, щоб дійти до них ... що означає, ніколи.
Корт Аммон

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

2
@CortAmmon Релігійним заповідям в програмуванні немає місця - "кращі практики" мають сенс лише в контексті, а вивчення "найкращої практики" без міркувань не дає змоги адаптуватися. Я вважаю YAGNI дуже корисною, але це не означає, що я не думаю про місця, де додавання точок розширення пізніше буде дорогим - це просто означає, що мені не потрібно заздалегідь думати про прості випадки. Зрозуміло, це також змінюється з часом, оскільки все більше припущень підкреслюється для вашого коду, що ефективно збільшує інтерфейс вашого коду - це акт балансування.
Луань

2
@CortAmmon Ваш "тривіально доказовий" випадок ігнорує дві дуже важливі витрати - вартість самої оцінки та витрати на обслуговування (можливо, непотрібної) точки розширення. Ось де люди отримують надзвичайно недостовірні оцінки - занижуючи оцінки. Для дуже простої функції, можливо, буде достатньо подумати кілька секунд, але цілком ймовірно, що ви знайдете цілу банку глистів, що випливає з початково "простої" функції. Комунікація є ключовим - оскільки речі стають великими, вам потрібно поговорити з вашим лідером / замовником.
Луань

1
@Luaan Я намагався заперечити вашу думку, релігійним заповідям в програмуванні немає місця. Поки існує один діловий випадок, коли витрати на оцінку та утримання точки розширення є досить обмеженими, є випадок, коли зазначена «заповідь» є сумнівною. З мого досвіду роботи з кодом, питання про те, залишати такі точки розширення чи ні, ніколи не вдалося вписати в одну чи іншу команду одного рядка.
Корт Аммон

7

Визначення поняття "надмірний код" та "YAGNI" часто залежить від того, наскільки ви дивитесь вперед.

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

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

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


6

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

/** ...
*   Precondition: rows is null or nonempty
*/
public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    Assert( rows==null || rows.Count > 0 )
    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

[Якщо припустити, що це C #, Assert може бути не найкращим інструментом для роботи, оскільки він не компілюється у звільненому коді. Але це дебати ще один день.]

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


5

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

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

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

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


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

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

3

Основне питання тут - "що буде, якщо ви зробите / не зробите"?

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

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

Ключовим для мене є Ніколи не корумповані дані . Дозвольте це повторити; НІКОЛИ. КОРУПТ. ДАНІ. Якщо виключені ваші програми, ви отримуєте звіт про помилку, який ви можете виправити; якщо він записує у файл погані дані, що згодом, потрібно сіяти землю сіллю.

Усі ваші "непотрібні" рішення повинні прийматися з урахуванням цього.


2

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

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

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

if (rows == null) throw new ArgumentNullException(nameof(rows));

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

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

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

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

Це не означає, що все несподіване повинно призвести до збою вашої програми, навпаки. Якщо ви добре використовуєте винятки, вони, природно, утворюють безпечні точки обробки помилок, де ви можете відновити стабільний стан програми та дозволити користувачу продовжувати те, що вони роблять (в ідеалі, уникаючи втрати даних для користувача). У цих пунктах обробки ви побачите можливості виправити проблеми ("Не знайдено номер замовлення 2212. Ви мали на увазі 2212b?") Або надати користувачеві контроль ("Помилка підключення до бази даних. Спробуйте ще раз?"). Навіть коли такої опції немає, принаймні це дасть вам шанс, що нічого не зіпсувалося - я почав цінувати код, який використовує usingі try... finallyнабагато більше, ніж try...catch, це дає вам багато можливостей підтримувати інваріантів навіть у виняткових умовах.

Користувачі не повинні втрачати свої дані та працювати. Це все ж має бути збалансоване із витратами на розробку тощо, але це досить хороша загальна інструкція (якщо користувачі вирішують, купувати ваше програмне забезпечення чи ні - внутрішнє програмне забезпечення зазвичай не має такої розкоші). Навіть вся збійка програми стає набагато меншою проблемою, якщо користувач може просто перезапустити і повернутися до того, що вони робили. Це справжня надійність - Слово економить вашу роботу весь час, не псуючи документ на диску і не даючи вам можливістьвідновити ці зміни після перезавантаження Word після збоїв. Це краще, ніж відсутність помилок в першу чергу? Напевно, ні - хоча не забувайте, що роботу, затрачену на лову рідкісної помилки, можна краще витратити всюди. Але це набагато краще, ніж альтернативи - наприклад, пошкоджений документ на диску, вся робота з останнього збереження втрачена, документ автоматично замінюється змінами перед збоєм, які щойно сталися Ctrl + A та Delete.


1

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

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

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

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


1

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

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

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

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

Отже, ваш код повинен виглядати так:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    if (rows != null && rows.Count == 0) throw new ArgumentException("Rows is empty."); 

    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

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


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


1

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

Не слід додавати зайвий код у будь-який час.

Не слід додавати код, який потрібен лише в майбутньому.

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

Визначення "поводитися добре" відповідає вашим потребам. Один з прийомів, який я люблю використовувати, - це «параної» винятки. Якщо я на 100% впевнений, що певний випадок ніколи не може відбутися, я все-таки програмую виняток, але роблю це таким чином, що а) чітко каже всім, що я ніколи не сподіваюся, що це станеться, і б) чітко відображається та реєструється, і таким чином, згодом не веде до повзучої корупції.

Приклад псевдокоду:

file = File.open(">", "bla")  or raise "Paranoia: cannot open file 'bla'"

file.write("xytz") or raise "Paranoia: disk full?"

file.close()  or raise "Paranoia: huh?!?!?"

Це чітко говорить про те, що я на 100% впевнений, що я завжди можу відкривати, записувати або закривати файл, тобто я не звертаюся до розширень створення складного поводження з помилками. Але якщо (ні: коли) я не можу відкрити файл, моя програма все одно не зможе контролюватися.

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

Дуже важливим пов'язаним пошуковим терміном було б "невдало швидко", як зазначено в інших відповідях і це дуже корисно для створення надійного програмного забезпечення.


-1

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

По-перше, заголовок методу:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)

Призначити призначення в якому рядку? Це повинно бути зрозуміло відразу зі списку параметрів. Без будь - яких додаткових знань, я б очікувати , що метод PARAMS виглядати наступним чином : (Appointment app, CalendarRow row).

Далі "вхідні перевірки":

//1. Is rows equal to null? - This will be the case if this is the first appointment.
if (rows == null) {
    rows = new List<CalendarRow> ();
}

//2. Is rows empty? - This will be the case if this is the first appointment / some other unknown reason.
if(rows.Count == 0)
{
    rows.Add (new CalendarRow (0));
    rows [0].Appointments.Add (app);
}

Це фігня.

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

Але вся концепція призначення зустрічей десь є дивною (якщо тільки це не частина GUI-коду). Здається, що ваш код містить (або принаймні намагається) явну структуру даних, що представляє календар (тобто List<CalendarRows><- це слід визначити як Calendarдесь, якщо ви хочете піти цим шляхом, то ви переходите Calendar calendarдо свого методу). Якщо ви підете цим шляхом, я б очікував, що calendarзаздалегідь він буде заповнений слотами, куди після цього ви помістите (призначите) зустрічі (наприклад, calendar[month][day] = appointmentце буде відповідний код). Але тоді ви також можете взагалі вирвати структуру календаря з основної логіки і просто мати List<Appointment>там, де Appointmentоб’єкти містять атрибутdate. І тоді, якщо вам потрібно зробити календар десь у графічному інтерфейсі, ви можете створити цю структуру "явного календаря" безпосередньо перед рендерінгом.

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


-2

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

Псевдокод:

let C_now   = cost of implementing the piece of code now
let C_later = ... later
let C_maint = cost of maintaining the piece of code one day
              (note that it can be negative)
let P_need  = probability that you will need the code after N days

if C_now + C_maint * N < P_need*C_later then implement the code else omit it.

Фактори для C_maint:

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

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


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