软件构建(七)——防御式编程

防御式编程这一概念来自防御式驾驶。在防御式驾驶中要建立这样一种思维,那就是你永远也不能确定另一位司机将要做什么。这样才能确保在其他人做出危险动作时你也不会受到伤害。你要承担起保护自己的责任,哪怕是其他司机犯的错误。本文将讲述如何面对严酷的非法数据的世界、在遇到“绝不会发生”的事件以及其他程序员犯下的错误时保护你自己。

1. 防御式编程

防御式编程的主要思想是:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据。更一般地说,其核心想法是要承认程序都会有问题,都需要被修改。防御式编码的最佳方式就是在一开始不要在代码中引入错误。使用途代式设计、编码前先写伪代码、写代码前先写测试用例、低层设计检查等活动,都有助于防止引入错误。因此,要在防御式编程之前优先运用这些技术。

1.1 保护程序免遭非法输入数据的破坏

对已形成产品的软件而言,应该做到“垃圾进,什么都不出”、“进来垃圾,出去是出错提示”或“不许垃圾进来”。通常有三种方法来处理进来垃圾的情况。

  • 检查所有来源于外部的数据的值:检查从文件、用户、网络或其他外部接口中获取的数据
  • 检查子程序所有输入参数的值
  • 决定如何处理错误的输入数据

1.2 使用断言(assert)

断言可以用于在代码中说明各种假定,澄清各种不希望的情形。但通常断言只是在开发阶段用于帮助查清相互矛盾的假定、预料之外的情况以及传给子程序的错误数据等。在生成产品代码时,不要把断言编译进目标代码,以免降低性能和让用户看到断言报错信息。下面是关于使用断言的一些指导性建议。

  • 用错误处理代码来处理预期会发生的状况,用断言来处理绝不应该发生的状况
  • 避免把需要执行的代码放到断言中:断言只检查变量的值,而不要在断言中运行函数
  • 用断言来注解并验证前条件和后条件:所谓前条件是子程序或类的调用方代码在调用子程序或实例化对象之前要确保为真的属性,后条件是子程序或类在执行结束后要确保为真的属性
  • 对于高健壮性的代码,应该先使用断言再处理错误

1.3 错误处理技术

有很多种解决方案用于处理那些预料中可能要发生的错误,下面列举一些可用的方案,实际中还经常把这些技术集合起来使用。

  • 返回中立值:遇到错误时继续执行操作并简单地返回一个没有危害的数值。但对于关键领域(如医疗、航天),关闭程序也比显示错误的数据要好
  • 换用下一个正确的数据:如读数据库时发现一条损坏的记录,则继续读下去直到找到一条正确记录为止
  • 返回与前次相同的数据:在前后变化不会太大的场景使用,如游戏重绘使用上一帧的图像
  • 换用最接近的合法值:如小于0的值用0代替
  • 把警告信息记录到日志文件中
  • 返回一个错误码:简单地报告有错误发生,并信任调用链上游的某个子程序会处理该错误
  • 调用全局的错误处理子程序或对象
  • 当错误发生时显示出错消息以提高用户体验:采用这种做法时要考虑模块划分,多语言化,还要小心不要告诉系统的潜在攻击者太多东西
  • 用最妥当的方式在局部处理错误
  • 关闭程序:适用于人身安全攸关的应用程序

1.4 异常

异常是把代码中的错误或异常事件传递给调用方代码的一种特殊手段。如果在一个子程序中遇到了预料之外的情况,但不知道该如何处理的话,它就可以抛出一个异常,就好比是举起双手说“我不知道该怎么处理它一一我真希望有谁知道该怎么办!” 一样。对出错的前因后果不甚了解的代码,可以把对控制权转交给系统中其他能更好地解释错误并采取措施的部分。

  • 用异常通知程序的其他部分,发生了不可忽略的错误
  • 只在真正例外的情况下才抛出异常:仅在其他编码实践方法无法解决的情况下才使用异常
  • 不能用异常来推卸责任:如果某种的错误情况可以在局部处理,那就应该在局部处理掉它
  • 避免在构造函数和析构函数中抛出异常,除非你在同一地方把它们捕获
  • 在恰当的抽象层次抛出异常
  • 在异常消息中加入关于导致异常发生的全部信息
  • 避免使用空的catch语句:不要试图敷衍一个不知该如何处理的异常
  • 了解所所用函数库可能抛出的异常
  • 考虑创建一个集中的异常报告机制
  • 把项目中对异常的使用标准化

1.5 在架构上设置隔栏来隔离错误

隔栏是一种容损策略,这与与船体外壳上装备隔离舱或者建筑物中的防火墙是类似的。以防御式编程为目的而进行隔离的一种方法,是把某些接口选定为“安全”区域的边界。对穿越安全区域边界的数据进行合法性校验,并当数据非法时做出敏锐的反映。可以把这种方法看做是手术室里使用的一种技术——任何东西在允许进入手术室之前都要经过消毒处理。因此手术室内的任何东西都可以认为是安全的。这其中最核心的设计决策是规定什么可以进入手术室,什么不可以进入,还有把手术室的门设在哪里。

1.6 编写辅助调试的代码

防御式编程的另一重要方面是使用辅助调试的代码,可以用于帮助快速地检测错误。一般人除非被某个错误反复纠缠,否则是懒得花精力去写这些辅助调试代码的,但如果越早引入辅助调试的代码,它能够提供的帮助也越大。还有一种“进攻时编程”的方式来处理异常情况:在开发阶段让错误显现出来,而在产品代码运行时能够自我修复——在开发时惨痛地失败,能让你在发布产品后不会败得太惨。

如果使用了辅助调试的代码,那要做好清理的计划。例如使用类似ant或make这样的编译控制工具,或者使用内置的预处理器(如C/C++的#define的,如果语言不支持预编译器就考虑用变通方法写自己的预处理器),在发布的产品代码中剔除掉调试代码。

说了这么多防御式编程的手段,但是过度地防御也会引起问题,例如引入的额外代码增加了软件的复杂度,引入的代码也可能引入其他bug。因此,要考虑好什么地方你需要进行防御,然后因地制宜地调整你进行防御式编程的优先级。

参考文献:电子工业出版社《代码大全(第2版)》第8章

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器