开发人员编写代码。不幸的是,开发人员也编写缺陷,其中大多数缺陷是在最初的编码阶段加入的。修复这些缺陷成本最低的地方同样也是在开发的初始阶段。如果等到功能测试或者系统测试来捕获并修复缺陷,那么您的软件开发成本就会高得多。在本文中,作者 Scott Will、Ted Rivera 和 Adam Tate 讨论了一些基本的“防御性”编码和单元测试实践,让开发人员更容易找到缺陷 —— 更重要的是,从一开始预防缺陷产生。
防御性驾驶和防御性开发
大多数司机接受过防御性驾驶技术的教育 —— 这有很好的理由 —— 但是并不是所有开发人员都接受过防御性开发的教育,特别是那些没有用汇编语言进行过多少开发(如果不是完全没用过的话)、也没有因内存约束和处理器限制而关心过编写极其紧凑的代码的年轻开发人员。本文讨论防御性编码和单元测试概念,它们可以帮助开发人员更快生成更好的代码并且缺陷更少。
为什么防御性开发是重要的?
捕捉错误、问题和缺陷的最佳位置是在开发周期的早期。图 1 展示了最容易出现缺陷的地方,以及最容易发现它们的地方,并包括了修复这些缺陷的成本(这些成本是针对 1996 年的 —— 今天的成本显然更高)。
当然,比在编码阶段找到缺陷更好的是在一开始就防止它们。防止缺陷应该是开发人员最优先考虑的。我们将分析几个让开发人员可以在编码和单元测试时防止并检测缺陷的简单的、经过证明的方法。
在编译前(防御性设计考虑)
防止缺陷(特别是系统性缺陷)的最有效方式是仔细检查编码所依据的设计。由设计缺陷导致的缺陷 —— 虽然一般不是很多 —— 通常修补成本是最高的。事前花很少的时间针对以下几点对设计进行检查可以得到显著的长期回报。
设计考虑
设计是否有任何不清楚或者混乱的部分?如果是的话,在编写任何代码 之前 澄清这些问题。否则,您可能以一种方式解释一个设计需求,而同事则以另一种方式解释它,从而得到不兼容的实现。
如果您的代码要访问同时被其他组件访问的数据,那么保证您的设计可以处理这种情况。同时,检查设计的安全问题。
如果您的代码严重依赖于其他应用程序的代码,那么您对那个应用程序是否熟悉到可以对设计进行检查?考虑在您的设计检查小组中加入熟悉该产品的一个开发人员。在 设计阶段 发现的集成问题可以得到最有效的处理。
安装和使用考虑
如果您的代码是以前版本的一个升级,那么是否有会使升级失败的参数或者其他选项改变?有哪些其他产品与新代码交互或者集成 —— 如果这些产品本身也改变了呢?还有,您的代码是否容易安装?
操作系统和数据库考虑
您的代码是否会在新版本的操作系统或者数据库上运行?您是否知道这些新版本中加入了哪些改变以及它们是否(及如何)影响您的代码?
这只是测试
设计是否结合了可测试性?虽然您可能认为可测试性问题不是您需要关心的,但是事实上单元测试 是 开发人员的责任之一 —— 几乎所有使执行功能测试和/或系统测试更容易的任何事情也会使单元测试更容易执行。
下面是 可测试性 领域内容的几个例子。
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
这是一个简化的例子,但是请注意遵照这种习惯有多么容易,以及这样做的好处。
本文地址:http://com.8s8s.com/it/it36527.htm