Код контрактов / утверждает: что с дублирующимися проверками?

10

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

Пример ситуации: сначала я пишу следующую функцию

void DoSomething( object obj )
{
  Contract.Requires<ArgumentNullException>( obj != null );
  //code using obj
}

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

void DoSomethingElse( object obj )
{
  //no Requires here: DoSomething will do that already
  DoSomething( obj );
  //code using obj
}

Очевидная проблема: DoSomethingElseтеперь зависит от DoSomethingпроверки того, что obj не является нулевым. Так что DoSomethingкогда-нибудь решится больше не проверять, или если я решу использовать другую функцию, obj может больше не проверяться. Что приводит меня к написанию этой реализации в конце концов:

void DoSomethingElse( object obj )
{
  Contract.Requires<ArgumentNullException>( obj != null );
  DoSomething( obj );
  //code using obj
}

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

Какова наиболее распространенная практика для подобных ситуаций?

Стейн
источник
3
ArgumentBullException? Это новый :)
CVN
LOL @ мои навыки печатания ... я буду редактировать.
Стийн

Ответы:

13

Лично я бы проверил на нулевое значение в любой функции, которая потерпит неудачу, если она получает нулевое значение, а не в любой функции, которая не будет.

Итак, в приведенном выше примере, если doSomethingElse () не нужно разыменовывать obj, я бы не проверял obj на наличие нулевого значения.

Если DoSomething () разыменовывает obj, тогда он должен проверять наличие нуля.

Если обе функции разыменовывают его, они оба должны проверить. Так что, если DoSomethingElse разыменовывает obj, тогда он должен проверять на ноль, но DoSomething также должен проверять на ноль, так как он может быть вызван из другого пути.

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

Люк Грэм
источник
1
Я полностью согласен. Предпосылки каждого метода должны стоять самостоятельно. Представьте, что вы переписываете так DoSomething(), что предварительное условие больше не требуется (маловероятно в данном конкретном случае, но может произойти в другой ситуации), и удалите проверку предварительного условия. Теперь какой-то, казалось бы, совершенно не связанный метод сломан из-за отсутствия предварительного условия. Я возьму немного дублирования кода для ясности по поводу странных сбоев, подобных этому, из желания сохранить несколько строк кода в любой день.
CVN
2

Большой! Я вижу, вы узнали о кодексах для .NET. Контракты на кодирование идут намного дальше, чем ваши средние утверждения, лучшим примером которых является статическая проверка . Это может быть недоступно для вас, если у вас не установлена ​​Visual Studio Premium или выше, но важно понять, что стоит за этим, если вы собираетесь использовать контракты кода.

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

В данном примере DoSomethingElse()функция не соответствует договору, как указано DoSomething(), так как можно передать значение null, и статическая проверка укажет на эту проблему. Чтобы решить эту проблему, добавьте тот же контракт к DoSomethingElse().

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

Это должно заставить вас пересмотреть, почему вы в первую очередь разделяете поведение на две функции. Мое мнение ( вопреки распространенному мнению ) всегда заключалось в том, что нельзя разделять функции, которые вызываются только из одного места . Обнаружение инкапсуляции путем применения контрактов становится еще более очевидным. Кажется, я нашел дополнительную аргументацию для своего дела! Спасибо! :)

Стивен Джеурис
источник
Относительно вашего последнего абзаца: в реальном коде обе функции были членами двух разных классов, поэтому они разделены. Кроме того, я много раз попадал в следующую ситуацию: написать длинную функцию, решить не разбивать ее. Позже выясните, что некоторая логика дублирована где-то еще, поэтому разбейте ее в любом случае. Или через год прочитайте его снова и сочтете его нечитаемым, так что разбейте его в любом случае. Или при отладке: разделить функции = меньше ударов по клавише F10. Причин больше, поэтому лично я предпочитаю разделение, хотя иногда это может быть слишком экстремальным.
Стийн
(1) «Позже выясните, что часть логики дублируется где-то еще» . Вот почему я считаю более важным всегда «развиваться в сторону API», чем просто разделять функции. Постоянно думайте о повторном использовании не только внутри текущего класса. (2) «Или через год прочитайте его еще раз и сочтите его нечитаемым». Поскольку функции имеют имена, это лучше? У вас будет еще больше читабельности, если вы используете то, что комментатор в моем блоге назвал «параграфами кода». (3) «функции разделения = меньше ударов по клавише F10» ... я не понимаю, почему.
Стивен Джеурис
(1) согласился (2) удобочитаемость - это личное предпочтение, так что это не то, что обсуждается для меня .. (3) выполнение функции из 20 строк требует нажатия F10 20 раз Для просмотра функций, содержащих 10 из этих строк в функции разбиения, мне нужно только один раз нажать F10. Да, в первом случае я могу поставить точки останова или выбрать «переход к курсору», но это все же больше усилий, чем во втором случае.
Стийн
@stijn: (2) согласился; p (3) спасибо за разъяснения!
Стивен Джеурис