Нарушает ли защитное программирование принцип DRY?

StackOverflow https://stackoverflow.com/questions/961297

  •  12-09-2019
  •  | 
  •  

Вопрос

Отказ от ответственности:Я непрофессионал, в настоящее время учусь программировать.Никогда не был частью проекта и не написал ничего длиннее ~ 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);
    }
}

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

Это было полезно?

Решение

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

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

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

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

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

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

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

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

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

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

Другие советы

Нарушение принципа DRY выглядит примерно так:

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 не означает "не выполняйте один и тот же код дважды", но "не пишите один и тот же код дважды".

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

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

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

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

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

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

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

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

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

Поскольку вы никогда не знаете заранее, где и как будет использоваться "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 */
}

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top