我正在和我的一位同事讨论你的代码应该如何防御。我都是专业的防御性编程,但你必须知道在哪里停下来。我们正在开发一个将由其他人维护的项目,但这并不意味着我们必须检查开发人员可以做的所有疯狂的事情。当然,你可以这样做,但这会给你的代码增加很大的开销。
你怎么知道在哪里画线?
我正在和我的一位同事讨论你的代码应该如何防御。我都是专业的防御性编程,但你必须知道在哪里停下来。我们正在开发一个将由其他人维护的项目,但这并不意味着我们必须检查开发人员可以做的所有疯狂的事情。当然,你可以这样做,但这会给你的代码增加很大的开销。
你怎么知道在哪里画线?
用户直接或间接输入的任何内容都应始终进行完整性检查。除此之外,assert
这里和那里的几个 s 不会受到伤害,但无论如何,你不能对疯狂的程序员编辑和破坏你的代码做太多事情!-)
我倾向于根据语言更改我在代码中的防御量。今天我主要在 C++ 中工作,所以我的想法正朝着那个方向发展。
在 C++ 中工作时,防御性编程是不够的。我对待我的代码就好像我在保护核机密,而其他所有程序员都想得到它们。断言、抛出、编译时错误模板黑客、参数验证、消除指针、深入的代码审查和一般的偏执狂都是公平的游戏。C++ 是一种邪恶而美妙的语言,我既喜欢它又严重不信任它。
我不喜欢“防御性编程”这个词。对我来说,它建议这样的代码:
void MakePayment( Account * a, const Payment * p ) {
if ( a == 0 || p == 0 ) {
return;
}
// payment logic here
}
这是错误的,错误的,错误的,但我一定已经看过数百次了。函数一开始就不应该用空指针调用,悄悄地接受它们是完全错误的。
这里的正确方法是有争议的,但最小的解决方案是通过使用断言或抛出异常来大声失败。
编辑:我不同意这里的一些其他答案和评论——我不认为所有函数都应该检查它们的参数(对于许多函数来说,这根本是不可能的)。相反,我认为所有函数都应该记录可接受的值,并声明其他值将导致未定义的行为。这是有史以来最成功且使用最广泛的库(C 和 C++ 标准库)所采用的方法。
现在让反对票开始......
我不知道真的有什么办法可以回答这个问题。这只是你从经验中学到的东西。您只需要问自己一个潜在问题可能有多普遍并做出判断。还要考虑到您不一定必须始终进行防御性编码。有时,只需注意代码文档中的任何潜在问题是可以接受的。
但最终,我认为这只是一个人必须遵循他们的直觉的东西。没有正确或错误的方法来做到这一点。
如果您正在处理组件的公共 API,那么进行大量参数验证是值得的。这让我养成了到处做验证的习惯。那是一个错误。所有这些验证代码都没有经过测试,并且可能使系统变得比它需要的更复杂。
现在我更喜欢通过单元测试来验证。验证肯定会针对来自外部来源的数据进行,但不会针对来自非外部开发人员的调用。
我总是 Debug.Assert 我的假设。
我个人的意识形态:程序的防御性应该与潜在用户群的最大天真/无知成正比。
防御使用您的 API 代码的开发人员与防御普通用户没有什么不同。
除此之外,除了确保您的应用程序在出现问题时能够很好地恢复,并且您始终向开发人员提供充足的信息以便他们了解正在发生的事情之外,没有什么可做的了。
防御性编程只是以按合同设计的编码方式履行合同的一种方式。
另外两个是
当然,您不应该为开发人员可能做的每一件疯狂的事情辩护,但是您应该在上下文中说明它将使用先决条件做预期的事情。
//precondition : par is so and so and so
function doSth(par)
{
debug.assert(par is so and so and so )
//dostuf with par
return result
}
我认为你必须提出你是否也在创建测试的问题。您应该在编码时采取防御措施,但正如 JaredPar 所指出的那样——我也相信这取决于您使用的语言。如果它是非托管代码,那么您应该非常防御。如果管理得当,我相信你还有一点回旋余地。
如果您有测试,而其他一些开发人员试图抽取您的代码,测试将会失败。但是话又说回来,这取决于您的代码的测试覆盖率(如果有的话)。
我尝试编写的代码不仅仅是防御性的,而且是敌对的。如果出现问题并且我可以修复它,我会的。如果不是,则抛出或传递异常并使其成为其他人的问题。任何与物理设备交互的东西——文件系统、数据库连接、网络连接都应该被认为是不可靠的并且容易发生故障。预测这些故障并捕获它们是至关重要的
一旦你有了这种心态,关键是你的方法要保持一致。您是否希望交回状态代码以解决调用链中的问题,或者您是否喜欢异常。混合模特会杀了你,或者至少会让你喝酒。沉重。如果您使用的是其他人的 api,则将这些东西隔离到根据您使用的术语捕获/报告的机制中。使用这些包装接口。
如果这里的讨论是如何针对未来的(可能是恶意的或无能的)维护者进行防御性编码,那么您可以做的事情是有限的。通过测试覆盖率和自由使用断言你的假设来执行合同可能是你能做的最好的事情,并且应该以一种理想的方式来完成,它不会使代码混乱,并使未来的非邪恶维护者的工作更加困难。代码。断言很容易阅读和理解,并清楚地说明给定代码段的假设是什么,因此它们通常是一个好主意。
对用户行为进行防御性编码完全是另一个问题,我使用的方法是认为用户是来抓我的。每个输入都经过我所能管理的仔细检查,并且我尽一切努力让我的代码安全 - 尽量不要坚持任何未经严格审查的状态,尽可能纠正,如果不能优雅退出,等等。如果您只需考虑外部代理可能对您的代码进行的所有笨拙的事情,它就会使您处于正确的心态。
针对其他代码(例如您的平台或其他模块)进行防御性编码与用户完全相同:他们是来抓你的。操作系统总是会在不合时宜的时候换掉你的线程,网络总是会在错误的时间消失,而且总的来说,邪恶无处不在。您不需要针对所有潜在问题进行编码——维护成本可能不值得增加安全性——但考虑一下肯定不会有坏处。如果有一个你想到但由于某种原因认为不重要的场景,在代码中显式注释通常不会有什么坏处。
系统应该有精心设计的边界,在那里进行防御性检查。应该决定在哪里验证用户输入(在什么边界)以及在哪里需要检查其他潜在的防御问题(例如,第三方集成点、公开可用的 API、规则引擎交互或由不同的程序员团队编码的不同单元)。在许多情况下,更多的防御性检查违反了 DRY,并且只是增加了维护成本而没有什么好处。
话虽如此,在某些方面你不能太偏执。应该非常严格地防范缓冲区溢出、数据损坏和类似问题的可能性。
我最近有一个场景,其中用户输入数据通过远程外观接口传播,然后是本地外观接口,然后是其他一些类,最终到达实际使用它的方法。我在问自己一个问题:什么时候应该验证价值?我只将验证代码添加到最终类,其中实际使用了该值。在传播路径上的类中添加其他验证代码片段对我来说过于防御性编程。一个例外可能是远程门面,但我也跳过了它。
好问题,我在做健全性检查和不做它们之间摇摆不定。它是 50/50
在这种情况下,我可能会采取中间立场,我只会“防弹”任何例程:
(a) 从项目中的多个地方调用
(b) 有可能改变的逻辑
(c) 不能使用默认值
(d) 例程不能优雅地“失败”
黑夜