6

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

同样,作为外行,我不知道就性能而言,有多少简单的逻辑语句对您不利,但防御性编程肯定对程序或灵魂不利。

4

6 回答 6

9

违反 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 并不是说​​“不要执行两次相同的代码”,而是“不要两次编写相同的代码”

于 2009-06-07T06:38:52.553 回答
6

这一切都归结为接口提供的合同。有两种不同的场景:输入和输出。

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

至少在我看来,输出(即返回结果)基本上应该受到调用者的信任。

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

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

并且该合同指定永远不会返回空字符串(最坏的情况是空字符串),那么这样做是安全的:

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

为什么?好吧,如果你错了,被调用者返回 null 那么程序就会崩溃。其实没关系。如果某个对象违反了它的合同,那么一般来说结果应该是灾难性的。

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

当然,情况可以改变这一点。

于 2009-06-07T06:07:13.683 回答
4

我先声明,一味地遵循原则是理想主义的,是错误的。您需要实现您想要实现的目标(例如,应用程序的安全性),这通常比违反 DRY 更为重要。在 GOOD 编程中,故意违反原则通常是必要的。

一个例子:我在重要阶段进行双重检查(例如 LoginService - 在调用 LoginService.Login 之前首先验证输入一次,然后再次在内部验证输入),但有时我会在确保一切正常 100% 后再次删除外部的,通常使用单元测试。这取决于。

不过,我永远不会为双重条件检查而烦恼。另一方面,完全忘记它们通常会更糟:)

于 2009-06-07T06:00:25.783 回答
3

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

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

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

你想要的是让程序采取每一个合理的步骤来做正确的事情,即使发生了一些奇怪的事情。同时,程序每次遇到这种奇怪的状态时都应该大声抱怨。如果遇到不可恢复的错误,它通常应该避免发出HLT指令,而是应该优雅地失败,安全地关闭其系统或激活一些可用的备份系统。

于 2009-06-07T07:10:54.673 回答
1

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

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

因为您永远不知道“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 */
}
于 2009-06-07T06:08:33.807 回答
1

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

在其他地方,您不需要所有这些。

但是,在您给出的示例中,我假设在第二个示例中,您有多个输入,因为否则对于相同的输入调用相同的函数 3 次将是多余的,这意味着您将拥有将条件写 3 次。现在那是多余的。

如果必须始终检查输入,只需将其包含在函数中。

于 2009-06-07T06:43:36.663 回答