Общий функциональный вопрос (C ++ / Java / C #) - PullRequest
7 голосов
/ 16 марта 2011

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

При работе с некоторым унаследованным кодом я часто видел примеры функций, которые (на мой взгляд, очевидно) выполняют слишком много работы внутри них. Я говорю не о 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 ]

6 голосов
/ 16 марта 2011

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

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

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

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

Очевидно, что такие проверки можно написать соператор 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».

3 голосов
/ 16 марта 2011

Это зависит.

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

3 голосов
/ 16 марта 2011

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

2 голосов
/ 16 марта 2011

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

Да.

1 голос
/ 16 марта 2011

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

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

...