Должен ли я добавить избыточный код сейчас, на случай, если он понадобится в будущем?


114

Так или иначе, в настоящее время я убежден, что я должен всегда стараться сделать свой код максимально надежным, даже если это означает добавление избыточного кода / проверок, которые, как я знаю , не будут сейчас полезны, но они может быть х количество лет по линии.

Например, в настоящее время я работаю над мобильным приложением с этим фрагментом кода:

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
Не имеет отношения к вопросу, но я подозреваю, что в вашем коде есть ошибка. Когда строки равны нулю, новый список создается и (я предполагаю) выбрасывается. Но когда строки не равны NULL, существующий список изменяется. Лучшим вариантом было бы настаивать, чтобы клиент передавал список, который может быть или не быть пустым.
Теодор Норвелл

9
Почему бы rowsникогда не быть нулевым? Нет веской причины, по крайней мере в .NET, для коллекции быть нулевой. Пусто , конечно, но не ноль . Я бы выбросил исключение, если оно rowsравно нулю, потому что это означает, что вызывающая сторона имеет ошибку в логике.
Kyralessa

Ответы:


176

В качестве упражнения сначала проверим вашу логику. Хотя, как мы увидим, у вас больше проблем, чем у любой логической проблемы.

Назовите первое условие A и второе условие B.

Вы сначала говорите:

Что касается конкретно второго раздела, я знаю, что если первый раздел верен, то второй раздел также будет верным.

То есть: A подразумевает B или, в более общих чертах (NOT A) OR B

А потом:

Я не могу придумать причину, почему первый раздел будет ложным, а второй - истинным, что делает второй оператор if избыточным.

То есть: NOT((NOT A) AND B). Применить закон Деморгана, чтобы получить, (NOT B) OR Aчто Б подразумевает А.

Следовательно, если оба ваших утверждения верны, то A подразумевает B, а B подразумевает A, что означает, что они должны быть равны.

Поэтому да, проверки являются излишними. Похоже, у вас есть четыре пути кода через программу, но на самом деле у вас есть только два.

Итак, теперь вопрос: как написать код? Реальный вопрос: что такое заявленный контракт метода ? Вопрос о том, являются ли условия излишними, - красная сельдь. Реальный вопрос заключается в том, «разработал ли я разумный контракт, и четко ли мой метод реализует этот контракт?»

Давайте посмотрим на объявление:

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

Это общедоступно, поэтому оно должно быть устойчивым к неверным данным от произвольных абонентов.

Он возвращает значение, поэтому оно должно быть полезно для его возвращаемого значения, а не для его побочного эффекта.

И все же название метода - глагол, предполагающий, что это полезно для его побочного эффекта.

Контракт параметра списка:

  • Нулевой список в порядке
  • Список с одним или несколькими элементами в порядке
  • Список без элементов неверен и не должен быть возможным.

Этот контракт безумен . Представьте себе написание документации для этого! Представьте себе написание тестовых случаев!

Мой совет: начни сначала. Этот API имеет интерфейс конфетного автомата, написанный на всем протяжении этого. (Выражение взято из старой истории о кондитерских машинах в Microsoft, где и цены, и выбор являются двузначными числами, и очень легко набрать «85», что является ценой товара 75, и вы получите неправильный предмет. Забавный факт: да, я действительно сделал это по ошибке, когда пытался получить жвачку из торгового автомата в Microsoft!)

Вот как разработать разумный контракт:

Сделайте ваш метод полезным для его побочного эффекта или его возвращаемого значения, а не для обоих.

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

Никогда не принимайте нули; нулевая последовательность - мерзость. Требовать от вызывающей стороны пропускать пустую последовательность, если это имеет смысл, никогда не обнулять.

Сбой сразу, если вызывающий абонент нарушает ваш контракт, чтобы научить их тому, что вы имеете в виду бизнес, и чтобы они улавливали свои ошибки при тестировании, а не на производстве.

Дизайн контракта сначала должен быть максимально разумным, а затем четко выполнять контракт. Это путь к будущему дизайну.

Теперь я говорил только о вашем конкретном случае, и вы задали общий вопрос. Итак, вот несколько дополнительных общих советов:

  • Если есть факт, который вы, как разработчик, можете сделать вывод, а компилятор не может, то используйте утверждение для документирования этого факта. Если другой разработчик, например будущий вы или один из ваших коллег, нарушит это предположение, тогда утверждение скажет вам.

  • Получить тестовый инструмент покрытия. Убедитесь, что ваши тесты охватывают каждую строку кода. Если есть открытый код, то либо у вас пропущен тест, либо у вас мертвый код. Мертвый код на удивление опасен, потому что обычно он не предназначен для мертвых! Невероятно ужасный недостаток безопасности Apple "goto fail" пары лет назад сразу приходит на ум.

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

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


24
Я никогда раньше не задумывался о таких методах, думая об этом сейчас, я всегда стараюсь охватить каждую возможность, когда в действительности, если человек / метод, вызывающий мой метод, не передает мне то, что мне нужно, тогда я не должен Чтобы попытаться исправить их ошибки (когда я на самом деле не знаю, что они намеревались), я должен просто потерпеть крах. Спасибо за это, ценный урок был усвоен!
KidCode

4
Тот факт, что метод возвращает значение, не подразумевает, что он также не должен быть полезен для его побочного эффекта. В параллельном коде способность функций возвращать оба часто жизненно важна (представьте CompareExchangeбез такой возможности!), И даже в несовпадающих сценариях что-то типа «добавить запись, если она не существует, и вернуть либо переданный в запись или тот, который существовал "может быть более удобным, чем любой подход, который не использует ни побочный эффект, ни возвращаемое значение.
суперкат

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
Сильно не согласен с этим ответом. Если функция получает неправильный ввод, это означает, что по определению в программе есть ошибка. Правильный подход - создать исключение при неверном вводе, чтобы вы обнаружили проблему и исправили ошибку. Подход, который вы описываете, просто скрывает ошибки, которые делают их более коварными и труднее выследить. Защитное кодирование означает, что вы автоматически не доверяете вводу, а проверяете его, но это не значит, что вы должны делать случайные предположения, чтобы «исправить» неверный или неожиданный ввод.
JacquesB

35

Я думаю, этот вопрос в основном не по вкусу. Да, это хорошая идея, чтобы написать надежный код, но код в вашем примере является небольшим нарушением принципа KISS (как будет много такого «будущего» кода).

Я лично, вероятно, не стал бы делать код пуленепробиваемый в будущем. Я не знаю будущего, и поэтому любой такой «будущий пуленепробиваемый» код обречен на неудачу, когда будущее все равно наступит.

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


4
Мне нравится твоя мысль о том, что ты не знаешь, что нас ждет в будущем. Прямо сейчас все, что я на самом деле делаю, - это угадываю, в чем может быть проблема, а потом снова угадываю решение.
KidCode

3
@KidCode: хорошее наблюдение. Ваше собственное мышление здесь на самом деле намного умнее, чем многие ответы здесь, включая тот, который вы приняли.
JacquesB

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/Defensive_programming ).


9

Здесь приведены две из десяти программных заповедей:

  • Ты не должен предполагать, что ввод правильный

  • Ты не должен делать код для будущего использования

Здесь проверка на ноль - это не «создание кода для будущего использования». Создание кода для будущего использования - это такие вещи, как добавление интерфейсов, потому что вы думаете, что они могут быть полезны "когда-нибудь". Другими словами, заповедь не добавлять слои абстракции, если они не нужны прямо сейчас.

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


5
Где эти запрограммированные заповеди. У вас есть ссылка? Мне любопытно, потому что я работал над несколькими программами, которые подписываются на вторую из этих заповедей, и несколькими, кто этого не делал. Неизменно, те , кто подписался повелению Thou shall not make code for future useстолкнулся с вопросами ремонтопригодности рано , несмотря на интуитивной логики заповеди. Я обнаружил, что в реальном кодировании эта заповедь эффективна только в коде, где вы управляете списком функций и сроками, и убедитесь, что вам не нужен будущий код для их достижения ... то есть никогда.
Cort Ammon

1
Тривиально доказуемо: если можно оценить вероятность будущего использования и прогнозируемое значение кода «будущего использования», а произведение этих двух больше стоимости добавления кода «будущего использования», то статистически оптимально добавить код Я думаю, что заповедь проявляется в ситуациях, когда разработчики (или менеджеры) вынуждены признать, что их навыки оценки не так надежны, как хотелось бы, поэтому в качестве защитной меры они просто предпочитают вообще не оценивать будущую задачу.
Cort Ammon

2
@CortAmmon В программировании нет места религиозным заповедям - «лучшие практики» имеют смысл только в контексте, а изучение «лучших практик» без объяснения причин лишает вас возможности адаптироваться. Я нахожу YAGNI очень полезным, но это не значит, что я не думаю о местах, где добавление точек расширения позже будет дорогостоящим - это просто означает, что мне не нужно заранее думать о простых случаях. Конечно, со временем это также меняется, так как все больше и больше допущений внедряются в ваш код, эффективно расширяя интерфейс вашего кода - это балансирование.
Луаан

2
@CortAmmon В вашем «тривиально доказуемом» случае игнорируются две очень важные затраты - стоимость самой оценки и затраты на обслуживание (возможно, ненужной) точки расширения. Вот где люди получают крайне ненадежные оценки - недооценивая оценки. Для очень простой функции может быть достаточно подумать несколько секунд, но вполне вероятно, что вы найдете целую банку червей, которая вытекает из изначально «простой» функции. Коммуникация - это ключ - по мере того, как дела развиваются, вам необходимо поговорить с вашим лидером / клиентом.
Луаан

1
@Luaan Я пытался отстаивать твою точку зрения, в программировании нет места религиозным заповедям. Пока существует один бизнес-случай, когда затраты на оценку и поддержание точки расширения достаточно ограничены, существует случай, когда указанная «заповедь» сомнительна. Исходя из моего опыта работы с кодом, вопрос о том, оставлять ли такие точки расширения или нет, никогда не подходил так или иначе в одну строковую команду.
Cort Ammon

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
В первом абзаце вы забыли упомянуть стоимость обслуживания этого кода с течением времени, когда выясняется, что фактически необходимая функция является взаимоисключающей с функцией, добавленной без необходимости. , ,
Руах

Вам также необходимо оценить стоимость ошибок, которые могут заползти в программу, потому что вы не ошибетесь при неверном вводе. Но вы не можете оценить стоимость ошибок, поскольку по определению они являются неожиданными. Таким образом, все "сделать математику" разваливается.
JacquesB

3

Основной вопрос здесь: «что произойдет, если вы делаете / не делаете»?

Как указывали другие, этот вид защитного программирования хорош, но иногда он опасен.

Например, если вы укажете значение по умолчанию, вы продолжите работу программы. Но теперь программа может делать не то, что вы хотите. Например, если он записывает этот пустой массив в файл, вы, возможно, теперь превратили свою ошибку из «сбоев, потому что я предоставил ноль случайно» в «очистку строк календаря, потому что я поставил ноль случайно». (например, если вы начинаете удалять вещи, которых нет в списке в той части, которая гласит «// бла»)

Ключ для меня никогда не портит данные . Позвольте мне повторить это; НИКОГДА. Коррумпированы. ДАННЫЕ. Если ваша программа исключений, вы получите отчет об ошибке, которую можно исправить; если он записывает неверные данные в файл, который будет позже, вы должны сеять землю солью.

Все ваши «ненужные» решения должны приниматься с учетом этой предпосылки.


2

Здесь вы имеете дело с интерфейсом. Добавив поведение «когда ввод есть null, инициализируйте ввод», вы фактически расширили интерфейс метода - теперь вместо того, чтобы всегда работать с действительным списком, вы заставили его «исправить» ввод. Является ли это официальной или неофициальной частью интерфейса, вы можете поспорить, что кто-то (скорее всего, включая вас) будет использовать это поведение.

Интерфейсы должны быть простыми, и они должны быть относительно стабильными, особенно в public staticметоде. Вы получаете немного свободы в частных методах, особенно в методах частных экземпляров. Неявно расширяя интерфейс, вы на практике сделали свой код более сложным. Теперь представьте, что вы на самом деле не хотите использовать этот путь кода, поэтому избегайте его. Теперь у вас есть немного непроверенного кода, который притворяется, будто это часть поведения метода. И я могу вам сказать прямо сейчас, что, возможно, в нем есть ошибка: когда вы передаете список, этот список видоизменяется методом. Однако, если вы этого не сделаете, вы создаете локальныйсписок, и выбросить его позже. Это противоречивое поведение, которое заставит вас плакать через полгода, когда вы пытаетесь отследить скрытую ошибку.

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

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

Вы не хотите, чтобы ввод был rowsнулевым, и вы хотите, чтобы ошибка стала очевидной для всех ваших абонентов как можно скорее .

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

Удостоверьтесь, чтобы различить два. Защитное программирование - это полезный метод для выявления ошибок в месте, где они наиболее актуальны, что значительно помогает при отладке и обеспечивает хорошую обработку исключений, предотвращая «скрытую коррупцию». Ошибка рано, ошибка быстро. С другой стороны, то, что вы делаете, больше похоже на «сокрытие ошибок» - вы манипулируете вводными данными и делаете предположения о том, что имел в виду вызывающий объект. Это очень важно для кода, ориентированного на пользователя (например, проверка орфографии), но вы должны быть осторожны, когда видите код, обращенный к разработчику.

Основная проблема заключается в том, что какую бы абстракцию вы ни делали, она будет просачиваться («Я хотел напечатать снова, а не предвкушать! Глупая проверка орфографии!»), А код для обработки всех особых случаев и исправлений по-прежнему остается кодом нужно поддерживать и понимать, а код нужно тестировать. Сравните усилия, направленные на то, чтобы убедиться, что ненулевой список передан, с исправлением ошибки, которая возникла у вас год спустя, в процессе производства - это не хороший компромисс. В идеальном мире вы бы хотели, чтобы каждый метод работал исключительно со своим собственным вводом, возвращая результат и не изменяя глобального состояния. Конечно, в реальном мире вы найдете множество случаев, когда это несамое простое и ясное решение (например, при сохранении файла), но я считаю, что поддержание чистоты методов, когда у них нет причин читать или манипулировать глобальным состоянием, значительно упрощает анализ кода. Это также дает вам больше естественных точек для разделения ваших методов :)

Это не значит, что все неожиданное должно привести к сбою приложения, а наоборот. Если вы правильно используете исключения, они, естественно, образуют безопасные точки обработки ошибок, где вы можете восстановить стабильное состояние приложения и позволить пользователю продолжить то, что они делают (в идеале, избегая потери данных для пользователя). В этих точках обработки вы увидите возможности для устранения проблем («Номер заказа 2212 не найден. Вы имели в виду 2212b?») Или предоставление пользователю контроля («Ошибка подключения к базе данных. Попробуйте еще раз?»). Даже если такой опции нет, по крайней мере, это даст вам шанс, что ничего не испортилось - я начал ценить код, который использует usingи try... finallyнамного больше, чем try...catchЭто дает вам много возможностей для поддержания инвариантов даже в исключительных условиях.

Пользователи не должны терять свои данные и работать. Это все равно должно быть сбалансировано с затратами на разработку и т. Д., Но это довольно хорошее общее руководство (если пользователи решают, покупать ваше программное обеспечение или нет - внутреннее программное обеспечение обычно не имеет такой роскоши). Даже сбой всего приложения становится намного меньшей проблемой, если пользователь может просто перезапустить и вернуться к тому, что он делал. Это настоящая надежность - Word сохраняет вашу работу все время, не повреждая документ на диске, и дает вам возможностьвосстановить эти изменения после перезапуска 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. check) Вызывающий метод должен просто убедиться, что он не передает неинициализированные значения внутри метода. Это ответственность программиста (пытаться) не быть глупым.
  2. check) Если я не принимаю во внимание, что передача 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 месяца, должна быть реализована ,


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