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

发布于 2024-07-24 17:09:27 字数 674 浏览 10 评论 0原文

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

这是否很糟糕:同样,作为一个外行,我不知道就性能而言,简单的逻辑语句对您有多大影响,但防御性编程肯定对程序或灵魂没有好处。

Disclaimer: I am a layperson currently learning to program. Never been part of a project, nor written anything longer than ~500 lines.

My question is: does defensive programming violate the Don't Repeat Yourself principle? Assuming my definition of defensive programming is correct (having the calling function validate input instead of the opposite), wouldn't that be detrimental to your code?

For instance, is this bad:

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?
}   

compared to this:

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

Again, as a layperson, I don't know how much simple logic statements count against you as far as performance goes, but surely defensive programming is not good for the program or the soul.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

潇烟暮雨 2024-07-31 17:09:27

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

Violating the DRY principle looks like that:

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);
    }
}

as you can see, the problem is that we have the same check twice in the program, so if the condition changes, we must modify it at two places, and chances are that we forget one of them, causing strange behaviour. DRY doesn't mean "don't execute the same code twice", but "don't write the same code twice"

笛声青案梦长安 2024-07-31 17:09:27

这一切都取决于接口提供的契约。 有两种不同的场景:输入和输出。

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

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

所有这一切都受到这个问题的影响:如果一方违反合同会发生什么? 例如,假设您有一个接口:

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

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

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

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

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

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

It all comes down to the contract the interface provides. There are two different scenarios for this: inputs and outputs.

Inputs--and by that I basically mean parameters to functions--should be checked by the implementation as a general rule.

Outputs--being return results--should be basically trusted by the caller, at least in my opinion.

All of this is tempered by this question: what happens if one party breaks the contract? For example, lets say you had an interface:

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

and that contract specifies that a null string will never be returned (it'll be an empty string at worst) then it's safe to do this:

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

Why? Well, if you're wrong and the callee returns a null then the program will crash. That's actually OK. If some object violates its contract then generally speaking the result should be catastrophic.

The risk you face in being overly defensive is that you write lots of unnecessary code (which can introduce more bugs) or that you might actually mask a serious problem by swallowing an exception that you really shouldn't.

Of course circumstances can change this.

你爱我像她 2024-07-31 17:09:27

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

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

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

Let me state first that blindly following a principle is idealistic and WRONG. You need to achieve what you want to achieve (say, safety of your application), which is usually far more important that violating DRY. Willful violations of principles are most often necessary in GOOD programming.

An example: I do double checks at important stages (e.g. LoginService - first validate input once before calling LoginService.Login, and then again inside), but sometimes I tend to remove the outer one again later after I made sure that everything works 100%, usually using unit tests. It depends.

I'd never get worked up over double condition checks though. FORGETTING them entirely on the other hand is usually multiple magnitudes worse :)

痴者 2024-07-31 17:09:27

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

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

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

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

I think defensive programming gets kind of a bad rap, since it does some things that are kind of undesirable, which include wordy code, and more significantly, papering over errors.

Most folks seem to agree that a program should fail fast when it encounters an error, but that mission critical systems should preferably never fail, and instead go to great lengths to keep on going in the face of error states.

There's a problem with that statement, of course, How can a program, even mission critical, continue when it's in an inconsistent state. Of course it can't, really.

What you want is for the program to take every reasonable step to do the right thing, even if there's something odd going on. At the same time, the program should complain, loudly, every time it encounters such an odd state. And in the event it encounters an error that is unrecoverable, it should usually avoid issuing a HLT instruction, rather it should fail gracefully, shutting down its systems safely or activating some backup system if one is available.

多像笑话 2024-07-31 17:09:27

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

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

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

In your simplified example, yes, the second format is probably preferable.

However, that doesn't really apply to larger, more complex, and more realistic programs.

Because you never know in advance where or how "foo" will be used, you need to protect foo by validating the input. If the input is validated by the caller (eg. "main" in your example) then "main" needs to know the validation rules, and apply them.

In real-world programming, the input validation rules can be fairly complex. It is not appropriate to make the caller know all the validation rules and apply them properly. Some caller, somewhere, is going to forget the validation rules, or do the wrong ones. So its better to put the validation inside "foo", even if it will get called repeatedly. This shifts the burden from the caller to the callee, which frees the caller to think less about the details of "foo", and use it more as an abstract, reliable interface.

If you truly have a pattern where "foo" will get called multiple times with the same input, I suggest a wrapper function that does the validation once, and an unprotected version that side-steps the validation:

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 */
}
温柔戏命师 2024-07-31 17:09:27

就像亚历克斯所说,这取决于情况,例如,我几乎总是在登录过程的每个阶段验证输入。

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

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

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

Like Alex said, it depends on the situation, for example, I almost always validate input at every stage of the log in process.

In other places, you don't need all that.

However, in the example you gave, I'm assuming, in the second example, that you have more than one input, 'cause otherwise it'll be redundant calling the same function 3 times for the same input which means you'll have to write the condition 3 times. Now THAT is redundant.

If the input ALWAYS has to be checked just include it in the function.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文