Общий функциональный вопрос (C++ / Java / C#)

Этот вопрос, вероятно, не зависит от языка, но я сосредоточусь на указанных языках.

Работая с устаревшим кодом, я часто видел примеры функций, которые (на мой взгляд, очевидно) выполняют слишком много работы внутри них. Я говорю не о 5000 монстрах LoC, а о функциях, которые реализуют в них проверки на предмет предварительных требований.

Вот небольшой пример:

void WorriedFunction(...) {
   // Of course, this is a bit exaggerated, but I guess this helps
   // to understand the idea.
   if (argument1 != null) return;
   if (argument2 + argument3 < 0) return;
   if (stateManager.currentlyDrawing()) return;

   // Actual function implementation starts here.

   // do_what_the_function_is_used_for
}

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

// Call the function.
WorriedFunction(...);

Теперь - как решить следующую проблему?

Как, вообще говоря, - должна ли эта функция делать только то, о чем она просила, и переносить "проверки готовности" к стороне вызывающей стороны:

if (argument1 != null && argument2 + argument3 < 0 && ...) {
   // Now all the checks inside can be removed.
   NotWorriedFunction();
}

Или - должно ли оно просто генерировать исключения для каждого несоответствия предварительных требований?

if (argument1 != null) throw NullArgumentException;

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

Если у вас есть альтернативные решения, не стесняйтесь рассказать мне о них:)

Спасибо.

5 ответов

Решение

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

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

Записываете ли вы это в своей голове, на бумаге, в дизайнерском документе, в комментариях или в реальных проверках кода - вопрос вкуса.

Но практическая проблема, долгосрочность, жизнь легче, если вы кодируете предварительное условие и постусловие как явные проверки. Если вы кодируете такие проверки, поскольку ожидается, что код не будет работать с ложным предварительным условием, или глючит ложное постусловие, проверки до и после условия должны заставить программу сообщать об ошибке таким образом, чтобы упростить ее. обнаружить точку отказа. То, что НЕ ДОЛЖНО делать код, это просто "возврат", ничего не сделавший, как показывает ваш пример, поскольку это подразумевает, что он каким-то образом выполнялся правильно. (Конечно, можно определить код для выхода, ничего не сделав, но если это так, то предварительные и последующие условия должны это отражать.)

Очевидно, вы можете написать такие проверки с помощью оператора if (ваш пример опасно близок):

if (!precondition) die("Precondition failure in WorriedFunction"); // die never comes back

Но часто наличие предварительного или постусловия указывается в коде путем определения специальной функции / макроса / оператора... для языка, называемого утверждением, и такая специальная конструкция обычно вызывает прерывание программы и обратный след при утверждении ложно

Код должен был быть написан следующим образом:

void WorriedFunction(...)  
 {    assert(argument1 != null); // fail/abort if false [I think your example had the test backwards]
      assert(argument2 + argument3 >= 0);
      assert(!stateManager.currentlyDrawing());
      /* body of function goes here */ 
 }  

Сложные функции могут захотеть сообщить своим вызывающим, что какое-то условие не удалось. Это настоящая цель исключений. Если присутствуют исключения, технически постусловие должно сказать что-то вроде "функция может выйти с исключением при условии xyz".

Это зависит.

Я хотел бы разделить мой ответ между делом 1, 3 и делом 2.

дело 1, 3

Если вы можете безопасно восстановиться после проблемы с аргументом, не создавайте исключений. Хорошим примером являются TryParse методы - если входные данные неправильно отформатированы, они просто возвращают false, Другим примером (за исключением) являются все методы LINQ - они выдают, если source является null или один из обязательных Func<> являются null, Тем не менее, если они принимают обычай IEqualityComparer<T> или же IComparer<T>они не бросают, а просто используют реализацию по умолчанию EqualityComparer<T>.Default или же Comparer<T>.Default, Все зависит от контекста использования аргумента и от того, можете ли вы безопасно восстановить его.

случай 2

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

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

Это интересный вопрос. Проверьте концепцию " Дизайн по контракту", вы можете найти это полезным.

"Или - должно ли оно просто генерировать исключения для каждого несоответствия предварительных требований?"

Да.

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

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

Другие вопросы по тегам