概念
防御式编程,顾名思义,就是保护自己不受其他人错误的影响,防御那些意料之外的错误。比如对一个函数来说,即使它被传递了一个错误的数据,它还是可以继续正常工作,哪怕这个错误数据是其它函数的错,好的程序永远不会输出垃圾信息。防御式编程帮助我们更容易发现错误,改正错误,当然,最好是一开始就不要引入错误,也就是可以利用一些方法,规则尽可能减少错误的产生。好了,下面说方法。
方法
首先,要抵御外来的侵略,也就是来自外部的错误的输入。第一步自然是要把错误检查出来,第二步决定怎么办。怎么办取决于程序的需求,是更看重健壮性还是更看重正确性。这两者通常是矛盾的。像消费类产品往往更看重健壮性,有结果总比关机好。所以可以在程序里尽量做点啥来让它能继续运行。比如超限的输入可以改到限制内,或者用前一个正确的输入,或者等后一个正确的输入;返回一个中性的值,或者返回前一个正确的值等等。而对安全性要求高的应用则更看重正确性,宁愿没有结果也不能有错误的结果。这个时候遇到错误可能就写一下log然后直接关机了。
防御式编程里常用的方法有断言(assertion),异常(exception),建立防护栏(barricade),还有一些debug的辅助手段等。
断言
断言用来检查那些应该永远也不会出现的情形。若是一个断言发生,就意味着一个应该永远不会发生的情形发生了,也就是发现了一个bug。断言是用来发现bug的非常有用的方法,只有发现了bug才能去修改,所以可以在可疑的地方都放一个。通常来说,一些先决条件(precondition)和后置条件(postcondition)都是要断言一下的。对于高要求的代码来说,这样还不算完。断言完了,修改好了,最好也要加上错误处理,因为我们不可能找到所有的错误。
说到错误处理,这是程序设计中不可或缺的一部分。我们要决定哪些错误是就地解决,哪些错误是要上报,上报到哪一级,这些级别要划分清楚,如同公司的管理一样,职责权限分明,搞不定没权限就要上报。还有一种方式是集中式错误处理,所有的错误都给到一个全局的错误处理模块,如同在公司里设立了一个专门处理错误的机构,当然,它的触手会伸到公司的每一个角落,所以,要权衡利弊。
异常
异常机制很多程序语言都提供了,也可以算是错误处理的一种方法,处理原则可以参考错误处理。还有些要注意的点比如不要在构造和析构函数里抛出异常;不要破坏抽象层次,把很底层的异常扔到上层;要注意引用的库的异常;不要有空的捕捉块等等。
防护栏
防护栏的功能如同防火墙,事实上它以前就叫防火墙,是后来防火墙这个词被广泛用于网络攻击后才改名的。防护栏的功能就是栏外的数据随便来,过了防护栏里面的数据就都是安全可靠的,如同安检,违禁物品带不过去的。这里重要的一点就是要确定哪些要放在栏外,哪些放在栏内。通常核心代码会放在栏内,而用户输入等会放在栏外。这个概念还可以用到类上,类的公有方法就是一道栏,私有方法被保护在栏内。有了防护栏之后,栏外的数据都需要错误处理,而栏内的只要断言就好了。
辅助手段
debug的一些辅助手段包括不要用发布版来限制开发版,开发版的程序要想办法尽可能多的去主动发现问题,当然,也要做一些准备方便在发布的时候移除这些辅助的代码,比如用预编译宏,用调试桩等。
代码留存
写了这么多防御式的代码后,最后遗留的一个问题就是发布的时候哪些代码可以留下,哪些要移除。几条指导性的方针是检查重要错误的留下,检查小错误的可以不要;导致数据丢失的不要,能让程序“优雅的”退出的留下;记录错误的log要留;用户会看到的错误信息要改的友好。所谓优雅的退出就是发现严重的错误后能记录错误信息,保留重要数据,安全的退出。
防御式编程不是万能药,防御的种种方法都会增加系统的复杂性,而且谁也不能保证防御的代码就不会有bug,所以哪些地方需要防御,做到什么程度都是设计者要考虑的问题。还记得吗,最好的防御是从一开始就不要引入错误。
同样,最后附上Checklist:
Checklist: Defensive Programming
General
- Does the routine protect itself from bad input data?
- Have you used assertions to document assumptions, including preconditions and postconditions?
- Have assertions been used only to document conditions that should never occur?
- Does the architecture or high-level design specify a specific set of error handling techniques?
- Does the architecture or high-level design specify whether error handling should favor robustness or correctness?
- Have barricades been created to contain the damaging effect of errors and reduce the amount of code that has to be concerned about error processing?
- Have debugging aids been used in the code?
- Has information hiding been used to contain the effects of changes so that they won't affect code outside the routine or class that is changed?
- Have debugging aids been installed in such a way that they can be activated or deactivated without a great deal of fuss?
- Is the amount of defensive programming code appropriate--neither too much nor too little?
- Have you used offensive programming techniques to make errors difficult to overlook during development?
Exceptions
- Has your project defined a standardized approach to exception handling?
- Have you considered alternatives to using an exception?
- Is the error handled locally rather than throwing a non-local exception if possible?
- Does the code avoid throwing exceptions in constructors and destructors?
- Are all exceptions at the appropriate levels of abstraction for the routines that throw them?
- Does each exception include all relevant exception background information?
- Is the code free of empty catch blocks? (Or if an empty catch block truly is appropriate, is it documented?)
Security Issues
- Does the code that checks for bad input data check for attempted buffer overflows, SQL injection, html injection, integer overflows, and other malicious inputs?
- Are all error-return codes checked?
- Are all exceptions caught?
- Do error messages avoid providing information that would help an attacker break into the system?