防御性编程是许多程序员都听说过的一个术语,对于某些程序,防御性编程是必不可少的。对于其他程序,它可能偶尔使用一下。除此之外,还有攻击性编程。
在本文中,我们将首先研究“正常编程”。我们首先研究它,是因为有些人误以为它是"防御性"编程。然而,无论你是否进行"防御性"编程,"正常编程"都是你应该做的事情。
然后,我们将研究防御性编程,接着研究进攻性编程。
正常编程
正常编程意味着在代码中进行所有必要的检查。它还意味着始终处理某些类型的错误。
代码中必要的检查
有些代码需要很多条件。你可能觉得你对条件的数量“过于谨慎”。
一个例子就是检查NULL或者None(导致发生价值数十亿美元的错误?)。空值和空值检查非常棘手,许多代码库都需要if判断,
到处都有这些语句。
另一个例子是验证用户输入。你需要进行多次检查以确保用户输入有效。你的程序需要非常严格地处理它。否则,你将面临安全漏洞。
但这不是防御性编程。
相反,忘记一个空值检查之类的事情就是一个错误。它们不是你“以防万一”而做的不必要的检查,它们是必要的检查。值NULL
有时会出现,这是正常的。如果你忘记了一个,你就有了一个错误。毫无疑问。
必要的错误处理
错误处理在程序中非常重要,你总是需要考虑你的程序应该如何响应错误。
这也取决于错误的类型。
一般来说,大多数程序都会处理超出其控制范围的“预期错误”。例如:
- 由于网络连接断开,无法发送网络请求。
- 由于用户删除了文件而无法找到它。
如果程序因为这些错误而崩溃,用户体验将非常糟糕。而且,处理这些问题相对容易。
因此,即使没有进行防御性编程,大多数程序也会处理这些问题。因此,这再次被视为“正常编程”,而不是防御性编程。
另一种错误是 bug。在大多数程序中,这些错误被视为“不可恢复”。大多数程序的经验法则是在这些错误上崩溃,而不是处理它们。
防御性编程
在我看来,防御性编程与容错有关。这意味着要尽一切努力确保你的程序继续工作。
防御性编程用例示例
正如Adrian Georgescu 在其关于 NASA 编码标准的文章中所写,防御性编程的一个例子是用于太空探索任务的代码。
代码开发完成后就会被送往太空。一旦出错,数十亿美元的工作成果就会付诸东流。
对于这种代码,你需要采取极端措施。无论如何,代码必须正确运行,不能崩溃。
这与普通程序非常不同。对于普通程序,错误通常不是什么大问题。即使程序有错误,它仍然可以使用。在最坏的情况下,可以通过致电客户服务手动解决问题。如果程序无法使用,您可以使其崩溃并重新启动。如果它是一个后端程序,则可能有多个服务器在运行它。如果它是一个客户端,用户可以自己重新启动程序。在非常糟糕的情况下,您可以更新服务器代码。您甚至可以手动转到物理服务器并重新启动它。
但对于某些关键软件,你不能这样做。软件必须始终正常运行。
问题是人并不完美。我们会产生错误。更不用说还会发生程序无法控制的其他错误(例如操作系统错误)。这意味着程序可能会失败。
但是,有些软件不提供这个选项。
因此,您需要竭尽全力防止失败。
如何进行防御性编程
防御性编程主要意味着尽一切可能确保你的程序正常运行并将继续正常运行。这可以包括:
- 拥有非常好的软件开发实践。
- 在代码中进行多次检查,以再次三番地确认一切是否正常工作。
- 可选地,具有错误恢复机制。这样,如果出现问题,程序也许可以恢复。
良好的软件开发实践
第一步是尽可能使代码没有错误并且易于使用。
这意味着你需要如下东西:
- 非常严格的质量保证
- 非常彻底的测试
- 非常彻底的运行时监控
- 非常严格的编码和开发标准。事实上,你可能会完全禁止某些模式或语言特性,例如递归。
- 良好的总体软件质量
- 易于理解的源代码
- 行为可预测的逻辑
这些要点对于所有软件都很重要,毕竟,如果你的源代码没有经过充分测试或不易理解,它可能会存在错误,这违背了防御性编程的初衷。
额外检查
采用防御性编程的代码往往有许多额外的检查。这些检查是为了发现错误。如果代码完全没有错误,就不需要这些检查。不是为了发现错误的检查属于“正常编程”,而不是“防御性编程”。
代码中存在条件,用于检查某些内容(例如程序中的某些状态)是否有效。如果检查失败,则表明存在错误。
在那时候:
- 如果程序正在开发中,您可以让其崩溃(发生异常)并修复错误。这与在开发过程中在攻击性编程中使用断言的原理相同。
- 如果程序正在生产中,您可以运行错误恢复(如果您已经实施了它)以便程序可以继续工作。
常见的情况是让程序崩溃并修复错误,例如在python代码的try-except中处理异常情况。在开发过程中,你希望测试和额外检查的组合能够捕获所有错误。然后,当程序投入生产时,它应该能够按预期运行。
这些检查的另一个好处是它们可以尽早发现错误。你检查的状态越多,你就能越早发现错误。这使得调试更容易。这也意味着你可以更早地开始错误恢复。
最后,您可以实现一些错误恢复。然后,如果检查失败,您可以运行错误恢复代码。
您可以根据需要进行任意数量的检查。您必须根据风险分析决定要检查的内容。一些重要的检查可能是涉及重要计算和数据的结果。
检查函数参数
您可以检查函数调用时是否使用了有效的参数。参数应具有正确的类型和范围。
检查数据计算结果
另一个例子是检查涉及数据的结果。
通常,您只会在第一次收到某些数据时对其进行检查。例如,如果用户提交了一些数据,您会对其进行检查以确保其有效。
然后,您将处理这些数据。您可能会对其进行格式化或以某种方式对其进行转换。您将进行测试以确保这些过程正常运行。
理论上,您不需要检查最终结果。初始数据是有效的。您处理它的代码运行正常。因此,最终结果应该是正确的。
但是,如果您正在进行防御性编程,您可能也会检查最终结果。
从意外错误中恢复
到目前为止,提到的步骤均试图减少程序中的错误数量。但是,错误可能仍然存在。因此,您可能需要实施错误恢复机制。
这可能需要深思熟虑。它甚至可能需要成为功能规划的一部分。如果程序在恢复过程中需要响应用户,情况就会如此。面向用户的行为最好是和产品经理合作确定,而不仅仅是由程序员确定。
此外,错误恢复可能是代码的很大一部分。作为一个虚构的例子,考虑一个接受产品订单网络请求的后端。服务器在处理订单时可能会出错。为了处理这种情况,您可以执行以下操作:
- 让初始服务器记录订单信息以免丢失。
- 为故障服务器提供一些恢复机制。例如,其他进程可能会重新启动它。或者,服务器可以尝试在内部修复其自身状态。
- 可以将订单交给不同的服务器,或者错误服务器可以在修复之后尝试再次处理它。
以下是一些可能的恢复机制的示例。如果代码中的某些内容失败:
- 也许您可以尝试在程序中手动修复或重置状态。
- 也许您可以尝试再次运行该操作。如果问题是竞争条件,下次可能会成功。
- 如果是子程序出错,也许你可以重新启动它。如果问题是子程序中的无效状态,那么重新启动它可能会起作用。
- 也许你可以在服务器上托管一个备份程序。如果客户端产生错误的结果,那么也许它可以调用服务器来执行计算。
- 也许你可以使用功能比主程序少的备份程序。如果主程序出错,也许可以运行仅提供基本操作的备份程序。
当然,如果程序的关键部分有错误,那么你可能在运行时无法做任何事情。唯一的解决方案可能是修复代码,然后再次发布。
您还需要进行风险分析。您需要考虑以下事项:
- 哪些代码可能存在错误?
- 它发生错误的可能性有多大?
- 这个错误会产生什么影响?
- 防止错误发生或实施错误恢复机制的成本是多少?
防御性编程的缺点
防御性编程有明显的缺点。例如:
- 它需要更多的代码。至少,与没有防御性编程的类似程序相比,您将拥有更多的检查。
- 性能可能会变差。这是因为执行额外的检查需要时间。
- 额外检查会增加代码量,如果这些额外代码的架构设计较差,将难以阅读和维护。
- 错误恢复机制可能需要较长时间来规划和实施。
何时使用防御性编程
是否使用防御性编程取决于您的程序。
如前所述,某些程序需要最大程度的可用性、可靠性和安全性。这些类型的程序可能需要大量防御性编程。
对于大多数其他程序,您不需要防御性编程。“正常编程”就足够了。尽管如此,您可以自由地在代码的某些关键区域使用一些防御性编程技术。由您来决定。
无论你做什么,记住要务实。使用风险分析。考虑:
- 哪些代码可能存在错误?
- 它发生错误的可能性有多大?
- 这个错误会产生什么影响?
- 防止错误发生或实施错误恢复机制的成本是多少?
然后,在必要的情况下使用适量的防御性编程。如果没有必要,尽量避免过度使用防御性编程。
攻击性编程
攻击性编程的目标是尽早发现错误并崩溃,前提是尽早崩溃是有帮助的,或者说避免造成更加严重的损失。
这意味着您可以立即收到错误通知。此外,崩溃时的堆栈跟踪更接近问题根源。这有助于调试。
如何进行攻击性编程
要进行攻击性编程,您需要:
- 进行正常编程,这是基本的
- 不要从错误中恢复(避免防御性编程)
- 以错误明显且易于查找的方式编写代码,这便于维护
- 出现错误时立即让程序崩溃
就像普通编程一样,你仍然需要条件来处理非错误的事情。例如,你需要条件来进行NULL
检查。
同样,您可能应该处理"不是错误的错误"。例如,当用户提供无效数据时,或者当您无法在文件系统中找到文件时。大多数情况下,在这些情况下崩溃是不合理的。换句话说,您可能应该遵循“正常编程”的方式来处理这些问题。
此外,你还应该以一种容易发现错误的方式编写代码。以下是一些技巧。
避免回退代码和默认值
默认状态、默认参数和后备代码等可能会隐藏错误。
例如,您可能调用一个带有错误参数的函数。您可能不小心将null
参数改成了字符串,例如,您使用python代码将''.format(None)改为了'None'。这是一个错误。但是,由于默认参数,参数无论如何都是字符串。这个错误不会被捕获,因此程序可能会做错事。
类似的事情也适用于“后备代码”。一个例子是继承和子类化。您可能忘记在子类中实现一个方法。然后,您调用该方法并执行父类的方法。这是非预期的行为,是一个错误。
为了防止这种情况,请避免使用默认状态、默认值和后备实现等。
避免检查因错误而崩溃的代码
有时,有缺陷的代码会自行崩溃。您无需做任何额外的事情。让代码保持原样,让它崩溃。
例如,考虑下面的代码。array
永远不应该是null
。 如果是null
,那就是一个错误。
如果你对其进行了防御性检查,代码就不会崩溃:
def getValue(array) {
if array and len(array) > 0 {
return array[0];
}
}
但如果没有防御性检查,代码就会崩溃。
def getValue(array) {
return array[0];
}
如果您希望代码尽早崩溃,在这种情况下,请保持原样,不进行防御性检查。
使用条件或断言来检查错误
与上面的观点相反,有些错误不会导致程序崩溃。
例如,你的程序中可能存在一些不正确的状态。你的程序可能不会因此崩溃。
再举一个例子,一些在正常情况下不应该执行的代码可能会执行。
在这些情况下,你可以使用手动检查。然后,如果你发现错误,你可以手动让程序崩溃。
例如:
def execute(arg) {
switch(arg) {
case 'a':
// do something
break;
case 'b':
// do something
break;
default:
// 这行代码不应该被执行,所以如果程序执行了这行代码,就引发异常
raise Exception('Default case should never execute.');
}
}
更传统上,这些类型的“错误检查”使用断言而不是条件。
断言是查找错误的工具。如果它们失败,则表示存在错误。条件是控制流工具。如果条件“失败”,则并不意味着存在错误。这意味着应该执行不同的代码块。
因此,您可以使用断言来代替条件语句。有关如何执行此操作的详细信息,请参阅您所用编程语言的文档。
在某些编程语言中,断言会导致程序崩溃。然而,在其他编程语言中,它们不会使程序崩溃。它们可能只会将错误消息打印到控制台或其他东西上。两者都可用。然而,攻击性编程建议在可能的情况下硬崩溃。
此外,某些编程语言允许您在生产中关闭断言以获得更好的性能。
攻击性编程的弊端
与防御性编程类似,进攻性编程也有缺点。
一个缺点是必须避免某些类型的代码,例如默认参数。默认参数有有效的用例。它们提供“合理的默认值”。它们可以使一些代码更易于使用。
另一个缺点是程序崩溃。这可能是您不期望在应用程序中发生的事情。
另一个缺点是性能。在整个代码中大量使用断言语句会显著降低性能。
因此,许多编程语言在断言失败时不会崩溃。此外,它们还可以选择从生产代码中删除断言。使用此选项,您将失去生产中攻击性编程的好处。您只能在开发过程中获得好处。
何时使用攻击性编程
攻击性编程能帮你尽早发现 bug。这是一个重大特性。
因此,在开发过程中使用它是个不错的选择。通常,你会在这里或那里放置断言语句来确保某些事情是正确的。
至于实际,则视情况而定。考虑两种编程方式的利弊,然后做出决定。
务实
当选择处理错误的方法时,你需要务实。
对于大多数程序来说,“正常编程”是您需要做的最低限度的事情。
对于某些程序,你可能会使用防御性编程。特别是对于需要高水平的程序:
- 可用性
- 安全
- 可靠性
但也要了解缺点。主要缺点是性能相对较差和开发时间较长。
进攻性编程有助于您发现错误。这在开发过程中(甚至生产过程中)非常有用。
您可以根据需要混合搭配这些方法。您甚至可以在代码的不同区域使用不同的方法。由您决定。
最后说明
这就是本文的全部内容。希望您觉得它有用。
如果遗漏了任何要点,或者您不同意任何内容,或者有任何评论或反馈,请在下面发表评论。
谢谢!