C++ 防御性编程是否违反了DRY原则?

C++ 防御性编程是否违反了DRY原则?,c++,dry,defensive-programming,C++,Dry,Defensive Programming,免责声明:我是一个外行,目前正在学习编程。从未参与过项目,也没有写过超过500行的东西 我的问题是:防御性编程是否违反了“不要重复自己”原则?假设我对防御性编程的定义是正确的(让调用函数验证输入,而不是相反),这会对代码有害吗 例如,这是否很糟糕: int foo(int bar) { if (bar != /*condition*/) { //code, assert, return, etc. } } int main() { int inp

免责声明:我是一个外行,目前正在学习编程。从未参与过项目,也没有写过超过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);
    }
}

同样,作为一个外行,我不知道简单的逻辑语句在性能方面对你有多不利,但防御性编程肯定对程序或灵魂不利。

让我首先声明,盲目遵循原则是理想主义和错误的。您需要实现您想要实现的目标(比如说,应用程序的安全性),这通常比安全性更重要。在好的编程中,故意违反原则是最必要的

举个例子:我在重要的阶段进行双重检查(例如,LoginService-在调用LoginService.Login之前先验证输入一次,然后在内部再次验证),但有时我倾向于在确保所有操作都100%正常工作后,再删除外部输入,通常使用单元测试。视情况而定


但我从来不会因为双重条件检查而生气。另一方面,完全忘记它们通常更糟糕:)

这一切归结于界面提供的契约。这有两种不同的场景:输入和输出

输入——我基本上是指函数的参数——应该作为一般规则由实现进行检查

输出——作为返回结果——应该基本上受到调用方的信任,至少在我看来是这样

所有这些都被一个问题所缓和:如果一方违约会发生什么?例如,假设您有一个接口:

class A {
  public:
    const char *get_stuff();
}
并且该契约指定永远不会返回空字符串(最坏情况下为空字符串),那么这样做是安全的:

A a = ...
char buf[1000];
strcpy(buf, a.get_stuff());
为什么??如果你错了,被调用方返回空值,那么程序就会崩溃。其实没关系。如果某个对象违反了它的契约,那么一般来说,结果应该是灾难性的

过度防御所面临的风险是,您编写了大量不必要的代码(这可能会引入更多的bug),或者您可能会吞下一个不应该出现的异常,从而实际上掩盖了一个严重的问题


当然,环境会改变这一点。

在您的简化示例中,是的,第二种格式可能更可取

然而,这并不适用于更大、更复杂和更现实的程序

因为您永远不知道“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 */
}
void RepeatFoo(int-bar,int-repeatCount)
{
/*验证栏*/
如果(bar!=/*条件*/)
{
//代码、断言、返回等。
}

对于(int i=0;i,违反干燥原理的情况如下:

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不是指“不执行同一代码两次”,而是“不编写同一代码两次”

正如Alex所说,这取决于具体情况,例如,我几乎总是在登录过程的每个阶段验证输入

在其他地方,你不需要这些

然而,在你给出的例子中,我假设,在第二个例子中,你有不止一个输入,因为否则,对同一个输入调用同一个函数3次是多余的,这意味着你必须写条件3次。现在这是多余的


如果总是需要检查输入,只需将其包含在函数中。

我认为防御性编程有点不好,因为它会做一些不受欢迎的事情,包括冗长的代码,更重要的是,掩盖错误

大多数人似乎都同意程序在遇到错误时应该快速失败,但任务关键型系统最好永远不要失败,而是在遇到错误状态时竭尽全力继续运行

这句话有一个问题,当然,一个程序,即使是任务关键型的,当它处于不一致的状态时,怎么能继续。当然它不能,真的

您想要的是程序采取一切合理的步骤来做正确的事情,即使发生了一些奇怪的事情。同时,程序应该在每次遇到这种奇怪的状态时大声抱怨。如果遇到无法恢复的错误,通常应该避免发出
HLT
instr相反,它应该优雅地失败,安全地关闭其系统,或者激活一些备份系统(如果有的话)。

最好公开BarIsValid()并让