01 什么是防御性编程?
防御式编程(Defensive Programming)是提高软件质量技术的有益辅助手段。防御式编程思想的理解可以参考防御式驾驶:在防御式驾驶中要建立这样一种思维,那就是你水远也不能确定另一位司机将要做什么。这样才能确保在其他人做出危险动作时你也不会受到伤害。你要承担起保护自己的责任,哪怕是其他司机犯的错误。防御式编程,简单来说,就是怀疑一切,认为自身代码之外的环境都是不可信的,在这种情况下,考虑代码该怎么写。
The whole point of defensive programming is guarding against errors you don't expect.---Steve McConnell, Code Complete.
02 防御性编程思路
2.1 边界防御:检查所有的外部输入
2.1.1 定义
在防御式编程的理念中,所有的外部输入都是不可信的,需要校验是否在可允许的范围内。这里需要检查的项包括:空指针、数组越界、不合法入参等。特别是当我们在写一个公共方法时,不确定这个方法会在未来某个时刻,被某个外部系统调用,做好输入检查既能保护自身程序运行的健壮性,又可以让外部系统放心调用。另外这个外部输入不光包括传入参数、还包括任何从方法外部获取到的数据、数据库查询到的数据等、获取到的配置文件等。
| 计算机领域有着一句GIGO(Garbage In Garbage Out)俗语,意思就是有垃圾数据进来后,出来的也是垃圾数据。 而就目前而言,对于已经成型的产品可能单单是这种原则并不适用,而是应该做到垃圾进,什么也不出、垃圾进,出去的是错误提示、垃圾进,经过筛选提取,出去的是有用信息或是不许垃圾进来。换句话说,GIGO于今天的标准看来已然是差劲程序的标志了。 |
2.1.2 建议
建议 | 详情 |
检查所有来源于外部的数据的值 |
|
检查子程序所有输入的参数的值 |
|
决定如何处理错误的输入数据 |
|
2.2 断言机制:检查永远不可能发生的情况
2.2.1 定义
断言是指在开发期间使用的、让程序在运行时进行自检的代码(通常为宏或一个子程序)。断言为真则程序正常运行,断言为假则意味着代码中发生了错误。断言对于大型复杂程序或可靠性要求极高的程序来说尤为重要。通过使用断言,程序员能更快速排查出因修改代码或者别的原因,定位程序里不匹配的接口和错误等。
2.2.2 建议
建议 | 详情 |
建立自己的断言机制 | https://spockframework.org/spock/docs/1.3/all_in_one.html |
用断言去处理那些不可发生的错误 |
|
避免将需要执行的子程序放到断言中 |
|
2.3 错误处理:在正确性和健壮性之间取舍
2.3.1 定义
处理预期内可能出现的问题,在正确性和健壮性之间做好取舍。正确性和健壮性往往是相互矛盾的,当我们检查出错误数据后,还需要决定如何处理它。防御性编程不会掩盖错误,也不会隐藏bug,这需要在健壮性和正确性之间做权衡:
程序的健壮性 | 程序的正确性 |
定义:健壮性具体指的是系统在不正常的输入或不正常的外部环境下仍能表现出正常的程度。系统在不正常的输入或不正常的外部环境下仍能正常运行,哪怕输出结果是错误的或者不完整的。 健壮性的原则:
| 程序永不返回不准确的结果,即使这样做会不返回结果或是直接退出程序。 |
After you have checked for bad data, decide how to handle it. Defensive Programming is NOT about swallowing errors or hiding bugs. It’s about deciding on the trade-off between robustness (keep running if there is a problem you can deal with) and correctness (never return inaccurate results). Choose a strategy to deal with bad data: return an error and stop right away (fast fail), return a neutral value, substitute data values, … Make sure that the strategy is clear and consistent.
2.3.2 建议
建议 | 详情 |
返回中立值 |
|
换用下一个正确的数据 |
|
返回与前次相同的数据 | |
换用最接近的合法值 | |
把警告信息记录到日志文件中 |
|
返回一个错误状态码 | |
调用错误处理子程序或对象 |
|
当错误发生时显示出错消息 |
|
关闭程序 |
2.4 异常处理:消除错误扩散
2.4.1 定义
异常是把代码中的错误或异常事件传递给调用方代码的一种特殊手段。
If code in one routine encounters an unexpected condition that it doesn’t know how to handle, it throws an exception
2.4.2 建议
建议 | 详情及案例 |
用异常通知程序的其他部分,发生了不可忽略的错误 |
|
只在真正例外的情况下才抛出异常 |
|
不能用异常来推卸责任 |
|
在恰当的抽象层次抛出异常 |
|
谨慎处理异常 | 需要明确异常处理的后果。 |
避免使用空的catch语句 | |
考虑创建一个集中的异常报告机制 | |
考虑异常的替换机制 |
2.5 隔栏
2.5.1 定义
外部接口数据假定是肮脏的不可信的,中间这些类(子程序)构成隔栏,负责清理和验证数据并返回可信的数据,最右侧的类(子程序)全部在假定数据干净(安全)的基础上工作,这样可以让大部分的代码无须再担负检查错误数据的职责!
船体外壳上装备隔离舱,如果船只与冰山相撞导致船体破裂,隔离舱就会被封闭起来,从而保护船体的其余部分不会受到影响。在发生火灾的时候,建筑物里的防火墙横沟阻止火势从建筑物的一个部位向另外一个部位蔓延。
2.5.2 建议
建议 | 详情及案例 |
在输入数据的时候将其转换为恰当的数据类型 | 输入的数据通常是字符串或者数字的形式,这些数据有时候可能需要被映射为true或者false这样的布尔类型或者像COLOR_RED、COLOR_BLUE、COLOR_GREEN这样的枚举。在程序中长时间传入类型不明的数据,会增加程序的复杂度和崩溃的风险。 |
将某些接口选定为“安全”区域的边界 | 对于穿过安全区域边界的数据进行合法性校验,并且对非法的数据做出敏捷的反映。 在类的层次中也可以采用隔栏:类的共用方法可以假设数据是不安全的,它们需要检查数据并进行清理。一旦该类的公用方法接收了数据,那么类的私有方法就可以假定数据都是安全的。 |
2.6 辅助调试代码
2.6.1 定义
防御式编程的另一重要方面就是使用调试助手(辅助调试代码),应用在开发期间应牺牲一些速度和对资源的使用,来换取一些可以让开发更顺畅的内置工具。
2.6.2 建议
建议 | 详情及案例 |
应尽早的引入辅助调试代码 |
|
采用进攻式编程 |
|
计划移除调试辅助的代码 | 避免调试用的代码和程序原代码纠缠不清,下面列举一些可以选择的移除方法:
|
03 避免过度设计
过度的防御式编程,也会带来新的问题:首先是预防不可能会发生的错误。过多的防御式代码,会导致整体程序显得臃肿、难以维护,代码里充斥着大量的判断和非业务代码;同时,程序的性能也会受此影响。
04 总结
防御式编程是一种安全的编程思想,本质上是要求开发人员对代码和线上环境报以辩证的态度和敬畏之心。它通过以下途径,从而来提升系统健壮性:提高工程质量——减少bug和问题;提高源码可读性—— 源码应该变得可读且可理解,并且能经受code review;让软件能通过预期的行为来处理不可预期的用户操作。作为一名优秀的开发者,不能将希望完全寄托于测试,而是在设计、开发阶段,对系统的异常和边界有充分的认知和考量,这是防御式编程带给我们的思考。
05 防御性编程CHECKLIST
5.1 一般事宜
-
子程序是否保护自己免遭有害输入数据的破坏?
-
你用断言来说明编程假定吗?其中包括了前条件和后条件吗?
-
断言是否只是用来说明从不应该发生的情况?
-
你是否在架构或高层设计中规定了一组特定的错误处理技术?
-
你是否在架构或高层设计中规定了是让错误处理更倾向于健壮性还是正确性?
-
你是否建立了隔栏来遏制错误可能造成的破坏?是否减少了其他需要关注错误处理的代码的数量?
-
代码中用到辅助调试的代码了吗?
-
如果需要启用或禁用添加的辅助助手的话,是否无需大动干戈?
-
在防御式编程时映入的代码量是否适宜–既不过多,也不过少?
-
在开发阶段是否采用了进攻式编程来使错误难以被忽视?
5.2 异常
-
你在项目中定义了一套标准化的异常处理方案吗?
-
是否考虑过异常之外的其他替代方案?
-
如果可能的话,是否在局部处理了错误而不是把它当成一个异常抛到外部?
-
代码中是否避免了在构造函数和析构函数中抛出异常?
-
所有的异常是否都与抛出它们的子程序处于同一抽象层次上?6).每个异常是否都包含了关于异常发生的所有背景信息?
-
代码中是否没有使用空的catch语句?(或者如果使用空的catch语句确实很合适,那么明确说明了吗?)
5.3 安全事宜
-
检查有害输入数据的代码是否也检查了故意的缓冲区溢出、SQL注入、HTML注入、证书溢出一级其他恶意输入数据?
-
是否检查了所有的错误返回码
-
是否捕获了所有的异常?
-
出错消息中是否避免出现有助于攻击者攻入系统所需的信息?
5.4 要点
-
最终产品代码中对错误的处理方式要比“垃圾进,垃圾出”复杂的多。
-
防御式编程技术可以让错误更容易发现、更容易修改,并减少错误对产品代码的破坏。
-
断言可以帮助人尽早发现错误,尤其是在大型系统和高可靠性的系统中,以及快速变化的代码中。
-
关于如何处理错误输入的决策是一项关键的错误处理决策,也是一项关键的高层设计决策。
-
异常提供了一种与代码正常流程角度不同的错误处理手段。如果留心使用异常,它可以成为程序员们知识工具箱中的一项有益补充,同时也应该在异常和其他错误处理手段之间进行权衡比较。
06 参考资料
【1】code complete, chapter 8