【问题标题】:Does defensive programming violate the DRY principle?防御性编程是否违反 DRY 原则?
【发布时间】:2009-06-07 05:56:16
【问题描述】:

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

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

【问题讨论】:

  • 抱歉,如果你愿意,请删除该标签。不过,我在示例中使用了它。
  • 这是我 15 岁自学时写的。现在我可能知道的流行语少了,但我写过更多实质性的程序。我想向前迈出两步……

标签: c++ dry defensive-programming


【解决方案1】:

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

【讨论】:

  • 我认为 Hooked 想说的不是检查 2 次而是只在 main() 函数中检查。
  • 这是一个不错的例子。尽管有人可能会争论您的foo 函数中的if 是否是一个断言,它是“防御”的一部分;那么我们就会以某种方式重复自己,但不那么重复,实际上两者都是必要的。
【解决方案2】:

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

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

输出——即返回结果——基本上应该被调用者信任,至少在我看来。

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

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

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

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

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

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

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

【讨论】:

  • +1 这一切都归结为接口提供的合约。防御性程序!= 掩盖问题,防御性程序== 如果违反合同,则通过抛出异常来暴露问题。
【解决方案3】:

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

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

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

【讨论】:

    【解决方案4】:

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

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

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

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

    【讨论】:

    • 我完全同意。我认为防御逻辑必须放在适当的位置才能发挥作用,就像其他任何事情一样。到处使用它(比如封装的私有成员)对我来说似乎有点过头了,而且真的会妨碍编写简洁优雅的代码的能力。
    【解决方案5】:

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

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

    因为您永远不知道“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 */
    }
    

    【讨论】:

    • 最好暴露 BarIsValid() 并让调用者自己编写循环。然后你有一个最小的 API:BarIsValid() 和 Foo(),当用户想要一个更复杂的循环时不需要扩展 API。 PS:我认为当 repeatCount 为 0 时,RepeatBar 不应该验证 bar。
    【解决方案6】:

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

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

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

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

    【讨论】:

      猜你喜欢
      • 2014-02-15
      • 1970-01-01
      • 1970-01-01
      • 2010-11-01
      • 2012-03-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-29
      相关资源
      最近更新 更多