开发人员编写代码。不幸的是,开发人员也编写缺陷,其中大多数缺陷是在最初的编码阶段加入的。修复这些缺陷成本最低的地方同样也是在开发的初始阶段。如果等到功能测试或者系统测试来捕获并修复缺陷,那么您的软件开发成本就会高得多。在本文中,作者 Scott Will、Ted Rivera 和 Adam Tate 讨论了一些基本的“防御性”编码和单元测试实践,让开发人员更容易找到缺陷 —— 更重要的是,从一开始预防缺陷产生。
防御性驾驶和防御性开发
大多数司机接受过防御性驾驶技术的教育 —— 这有很好的理由 —— 但是并不是所有开发人员都接受过防御性开发的教育,特别是那些没有用汇编语言进行过多少开发(如果不是完全没用过的话)、也没有因内存约束和处理器限制而关心过编写极其紧凑的代码的年轻开发人员。本文讨论防御性编码和单元测试概念,它们可以帮助开发人员更快生成更好的代码并且缺陷更少。
为什么防御性开发是重要的?
捕捉错误、问题和缺陷的最佳位置是在开发周期的早期。图 1 展示了最容易出现缺陷的地方,以及最容易发现它们的地方,并包括了修复这些缺陷的成本(这些成本是针对 1996 年的 —— 今天的成本显然更高)。
图 1. 缺陷:引入阶段及发现阶段(包括成本)
当然,比在编码阶段找到缺陷更好的是在一开始就防止它们。防止缺陷应该是开发人员最优先考虑的。我们将分析几个让开发人员可以在编码和单元测试时防止并检测缺陷的简单的、经过证明的方法。
在编译前(防御性设计考虑)
防止缺陷(特别是系统性缺陷)的最有效方式是仔细检查编码所依据的设计。由设计缺陷导致的缺陷 —— 虽然一般不是很多 —— 通常修补成本是最高的。事前花很少的时间针对以下几点对设计进行检查可以得到显著的长期回报。
设计考虑
设计是否有任何不清楚或者混乱的部分?如果是的话,在编写任何代码 之前 澄清这些问题。否则,您可能以一种方式解释一个设计需求,而同事则以另一种方式解释它,从而得到不兼容的实现。
如果您的代码要访问同时被其他组件访问的数据,那么保证您的设计可以处理这种情况。同时,检查设计的安全问题。
如果您的代码严重依赖于其他应用程序的代码,那么您对那个应用程序是否熟悉到可以对设计进行检查?考虑在您的设计检查小组中加入熟悉该产品的一个开发人员。在 设计阶段 发现的集成问题可以得到最有效的处理。
安装和使用考虑
如果您的代码是以前版本的一个升级,那么是否有会使升级失败的参数或者其他选项改变?有哪些其他产品与新代码交互或者集成 —— 如果这些产品本身也改变了呢?还有,您的代码是否容易安装?
操作系统和数据库考虑
您的代码是否会在新版本的操作系统或者数据库上运行?您是否知道这些新版本中加入了哪些改变以及它们是否(及如何)影响您的代码?
这只是测试
设计是否结合了可测试性?虽然您可能认为可测试性问题不是您需要关心的,但是事实上单元测试 是 开发人员的责任之一 —— 几乎所有使执行功能测试和/或系统测试更容易的任何事情也会使单元测试更容易执行。
下面是 可测试性 领域内容的几个例子。
设计是否允许运行时外部工具访问“状态”变量(例如,当前状态),特别是那些测试程序需要用来验证代码是否正确工作以帮助确定问题的变量?
是否对跟踪和日志给予了足够的重视?您让其他人分析缺陷越容易,您在发现缺陷后修正它们就越容易(而且在单元测试中发现自己的问题也会更容易)。
您是否考虑了所有可能调用您的代码的上下文?如果您可以将错误消息与调用它的用户函数上下文相关联,那么用户就更有可能理解这个错误。
设计是否结合了您的测试自动化工具所需要的特定的“钩子(hook)”?
再多想一想您肯定可以在这个清单中加入更多的内容,特别是那些对您的产品或者组织特定的内容。
防御性编码技术:编译器是您的朋友
当您完成对设计的检查后,就轮到编码了。就让我们面对它,除了设计错误外,编码是惟一引入缺陷的地方。无论如何,您的测试程序和客户是不会加入缺陷的 —— 只有 您 会。我们都知道时间很紧张,但是如果您没有时间在第一次就把它编写正确,那么您怎么能找到时间去修正它呢?花上一些时间,这会使您在以后的编码工作中更轻松。
防止缺陷的最好方法之一是使用编译器。令人恐惧的是,开发人员在编译时通常选择使用最低程度的警告输出,所以请启用编译的全部警告 —— 把即使将编译器配置为检查 所有方面 编译时也不产生一个警告当成编写代码的一个挑战。此外,对代码使用多种编译器使很多程序员获益 —— 这种方法有时会捕获不同的语法错误。
编码习惯
下面我们将抛砖引玉介绍几个好的编码习惯。我们不是要为您定义“最佳编码习惯” —— 我们只是要您形成自己遵守的代码编写习惯。下面是几个供参考的最佳习惯的例子。
在使用前初始化所有变量
您是否有一组可接受的默认值,特别是对于可能被用户、其他组件或者其他程序有选择地修改的数据?同时,我们强烈要求您列出在最外围例程中要使用的所有本地变量,然后再专门初始化它们。这样不会对您编写代码时的想法留下任何疑问。虽然这可能要多花一些时间并且像是没有理由地增加了几行代码,但是与只是在“运行中(on the fly)”声明本地变量相比,大多数优化编译器不会对此生成任何额外的运行时代码。清单 1 显示了在一个例程中最初几行代码的一个例子:
清单 1. 初始化本地变量
public unsigned short TransMogrify( UFEventLink IncomingLink )
{
//
// local variables
//
unsigned short usRc;
String sOurEventType;
String sTheirEventType;
//
// beginning of code
//
usRc = 0;
sOurEventType = null;
sTheirEventType = null;
//
// a miracle occurs...
//
return( usRc );
} // end "TransMogrify"
使用一个“编码标准”文档
如果您有一个编码标准文档,就使用它。您可以在 Internet 上找到许多种编码标准。找到一种简单的、切中要害、并为您留下一定的活动空间的标准。Sun 的网站有一个关于 Java 编程的编码规范的文章(请参阅 参考资料),它给出了拥有标准的下列几点理由:
一个软件生存期百分之八十的成本都用在维护上。
几乎没有软件在整个使用期间都是由原作者维护的。
编码规范改进了软件的可读性,使工程师可以更快和更充分地理解新代码。
如果您将源代码作为产品交付,那么需要保证它有像您创建的所有其他产品一样的包装和整洁性。
即使不赞成“标准”的想法,至少采用这个简单的建议:对变量名使用“匈牙利命名法”,这会使您的代码更容易阅读和维护(。
保证返回代码的一致性
在调试时有一种会制造麻烦的情况是:调用程序屏蔽(或者覆盖)一个表示错误的返回代码。一定要想好您要向调用您的代码的例程返回什么值,并保证从您所调用的例程返回的所有错误代码都得到恰当处理。如果返回代码 n 在一个地方意味着一件事,就不要在其他的地方用返回代码 n 表示另一件事。
对每个例程使用“单点退出”
这一点怎么强调也不过分:对每个例程使用单点退出 —— 就是说,没有多重返回!这是最容易忽视的、也是您可以采用的最好的习惯。如果例程只从一个地方返回,那么就可以用一种非常容易的方法保证在返回前完成所有必要的清理工作,这也使调试更容易。清单 2 显示了一个包含多重返回的代码示例。注意重复代码、甚至忘记“清理”项目是多么容易。
清单 2. 单点退出示例
1 public String getName( )
2 {
3 //
4 // local variables
5 //
6 String returnString;
7
8
9 //
10 // beginning of code
11 //
12 returnString = textField.getText( );
13 if ( null == returnstring )
14 {
15 badCount++;
16 totalCount++;
17 return( null )
18 }
19
20 returnString = returnString.trim( );
21 if ( returnString.equals( "" ) )
22 {
23 badCount++;
24 totalCount++;
25 return( null );
26 }
27
28 totalCount++;
29 return( returnString );
30
31 } // end getName
在第 15 行,badCount 增加了,因为 getText( ) 返回 null。在第 23 行,badCount 代码又重复了。现在想像一下如果这个例子需要完成更复杂的“清理”时会有多混乱。
清单 3 显示了一种更好的方法:
清单 3. 单点退出示例 —— 修正后
1 public String getName( )
2 {
3 //
4 // local variables
5 //
6 String returnString;
7
8
9 //
10 // beginning of code
11 //
12 returnString = textField.getText( );
13 if ( null != returnstring )
14 {
15 returnString = returnString.trim( );
16 if ( returnString.equals( "" ) )
17 returnString = null;
18 }
19
20 //
21 // "cleanup"
22 //
23 if ( null == returnString )
24 badCount++;
25 totalCount++;
26
27 return( returnString );
28
29 } // end getName
这是一个简化的例子,但是请注意遵照这种习惯有多么容易,以及这样做的好处。