免责声明:我是一名正在学习编程的非专业人士。从来没有参与过项目,也没有写过超过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);
}
}
同样,作为一个非专业人士,我不知道有多少简单的逻辑陈述会对你的性能有多大影响,但对于程序或灵魂而言,防御性编程肯定不好。
答案 0 :(得分: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并不意味着“不要两次执行相同的代码”,而是“不要两次写相同的代码”
答案 1 :(得分:6)
这一切都归结为界面提供的合同。有两种不同的情况:输入和输出。
输入 - 我基本上是指函数的参数 - 应作为一般规则由实现检查。
输出 - 返回结果 - 应该基本上由来电者信任,至少在我看来。
所有这一切都受到这个问题的影响:如果一方违约,会发生什么?例如,假设您有一个界面:
class A {
public:
const char *get_stuff();
}
并且该合约指定永远不会返回空字符串(最坏的情况下它将是一个空字符串)然后执行此操作是安全的:
A a = ...
char buf[1000];
strcpy(buf, a.get_stuff());
为什么呢?好吧,如果你错了,并且被调用者返回null,那么程序将崩溃。那是实际上好的。如果某个对象违反了合同,那么一般来说结果应该是灾难性的。
你过度防守所面临的风险是你写了很多不必要的代码(可能会引入更多的错误),或者你实际上可能通过吞下你真正不应该做的异常来掩盖一个严重的问题。
当然情况可能会改变这一点。
答案 2 :(得分:4)
首先我要说明,盲目遵循原则是理想主义和错误的。您需要实现您想要实现的目标(例如,您的应用程序的安全性),这通常比违反DRY更重要。在GOOD编程中,最常需要故意违反原则。
一个例子:我在重要阶段进行双重检查(例如LoginService - 在调用LoginService.Login之前首先验证输入一次,然后再调用内部),但有时候我确定在确定所有内容之后再次删除外部100%工作,通常使用单元测试。这取决于。
但是,我从来没有通过双重条件检查来解决问题。另一方面,完全忘记它们通常会使情况恶化多少:)答案 3 :(得分:3)
我认为防御性编程有点糟糕,因为它会做一些不受欢迎的事情,其中包括罗嗦的代码,更重要的是,它会报错。
大多数人似乎同意程序在遇到错误时应该快速失败,但是关键任务系统最好永远不会失败,而是在面对错误状态时继续努力。
当然,这个陈述存在一个问题,一个程序,即使是关键任务,如何在一个不一致的状态下继续运行。当然它不能,真的。你想要的是让程序采取一切合理的步骤来做正确的事情,即使有奇怪的事情发生。同时,程序每次遇到这种奇怪的状态时都应该抱怨大声。如果遇到无法恢复的错误,通常应避免发出HLT
指令,而应优先失败,安全关闭系统或激活某些备份系统(如果有的话)。
答案 4 :(得分: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 */
}
答案 5 :(得分:1)
就像亚历克斯所说,这取决于具体情况,例如,我几乎总是在登录过程的每个阶段验证输入。
在其他地方,你并不需要这一切。
然而,在你给出的例子中,我假设,在第二个例子中,你有多个输入,'因为否则它将是冗余调用相同的功能3次相同的输入,这意味着你我必须写这个条件3次。现在这是多余的。
如果必须检查输入ALWAYS,只需将其包含在函数中。