Нарушает ли оборонительное программирование принцип СУХОГО? - PullRequest
6 голосов
/ 07 июня 2009

Отказ от ответственности: я непрофессионал, в настоящее время учусь программировать. Никогда не был частью проекта и не писал ничего длиннее ~ 500 строк.

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

Например, это плохо:

int foo(int bar)
{
    if (bar != /*condition*/)
    {
        //code, assert, return, etc.
    }
}

int main()
{
    int input = 10;
    foo(input); //doesn't the extra logic
    foo(input); //and potentially extra calls
    foo(input); //work against you?
}   

по сравнению с этим:

int main()
{
    if (input == /*condition*/)
    {
        foo(input);
        foo(input);
        foo(input);
    }
}

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

Ответы [ 6 ]

9 голосов
/ 07 июня 2009

Нарушение принципа СУХОЙ выглядит так:

int foo(int bar)
{
    if (bar != /*condition*/)
    {
        //code, assert, return, etc.
    }
}

int main()
{
    int input = 10;
    if (input == /*condition*/)
    {
       foo(input);
       foo(input);
       foo(input);
    }
}

Как вы можете видеть, проблема в том, что у нас в программе одна и та же проверка дважды, поэтому если условие изменяется, мы должны изменить его в двух местах, и есть вероятность, что мы забудем одно из них, вызывая странное поведение. DRY не означает «не выполняйте один и тот же код дважды», но «не пишите один и тот же код дважды»

6 голосов
/ 07 июня 2009

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

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

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

Все это смягчается вопросом: что произойдет, если одна из сторон нарушит договор? Например, допустим, у вас был интерфейс:

class A {
  public:
    const char *get_stuff();
}

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

A a = ...
char buf[1000];
strcpy(buf, a.get_stuff());

Почему? Ну, если вы не правы, и вызываемый возвращает ноль, то программа завершится сбоем. Это на самом деле ОК . Если какой-либо объект нарушает его контракт, то, вообще говоря, результат должен быть катастрофическим.

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

Конечно, обстоятельства могут изменить это.

4 голосов
/ 07 июня 2009

Позвольте мне сначала заявить, что слепое следование принципу является идеалистическим и НЕПРАВИЛЬНЫМ. Вам нужно достичь того, чего вы хотите достичь (скажем, безопасности вашего приложения), что, как правило, гораздо важнее, чем нарушение режима DRY. Преднамеренное нарушение принципов чаще всего необходимо при ХОРОШЕМ программировании.

Пример: я делаю двойные проверки на важных этапах (например, LoginService - сначала проверяю ввод один раз перед вызовом LoginService.Login, а затем снова внутри), но иногда я имею тенденцию снова удалять внешний после того, как убедился работает на 100%, обычно с использованием юнит-тестов. Это зависит.

Я бы никогда не взволновался из-за двойной проверки условий. С другой стороны, забывать их целиком на несколько порядков хуже:)

3 голосов
/ 07 июня 2009

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

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

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

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

1 голос
/ 07 июня 2009

Как сказал Алекс, это зависит от ситуации, например, я почти всегда проверяю ввод на каждом этапе процесса входа в систему.

В других местах вам все это не нужно.

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

Если необходимо проверять вход ВСЕГДА, просто включите его в функцию.

1 голос
/ 07 июня 2009

В вашем упрощенном примере да, второй формат, вероятно, предпочтительнее.

Однако это не относится к более крупным, более сложным и более реалистичным программам.

Поскольку вы никогда не знаете заранее, где и как будет использоваться «foo», вам необходимо защитить foo, проверяя вводимые данные. Если входные данные проверены вызывающей стороной (например, «main» в вашем примере), тогда «main» должен знать правила проверки и применять их.

В реальном программировании правила проверки ввода могут быть довольно сложными. Не следует сообщать вызывающему абоненту все правила проверки и применять их должным образом. Кто-то из абонентов где-то собирается забыть правила проверки или сделать неправильные. Поэтому лучше поместить валидацию в "foo", даже если она будет вызываться повторно. Это переносит нагрузку с вызывающего абонента на вызываемого, что позволяет вызывающему абоненту меньше думать о деталях «foo» и использовать его больше как абстрактный, надежный интерфейс.

Если у вас действительно есть шаблон, в котором «foo» будет вызываться несколько раз с одним и тем же вводом, я предлагаю функцию-оболочку, которая выполняет проверку один раз, и незащищенную версию, которая обходит проверку:

void RepeatFoo(int bar, int repeatCount)
{
   /* Validate bar */
   if (bar != /*condition*/)
   {
       //code, assert, return, etc.
   }

   for(int i=0; i<repeatCount; ++i)
   {
       UnprotectedFoo(bar);
   }
}

void UnprotectedFoo(int bar)
{
    /* Note: no validation */

    /* do something with bar */
}

void Foo(int bar)
{
   /* Validate bar */
   /* either do the work, or call UnprotectedFoo */
}
...