边编码边论证
尝试手动论证软件的正确性会生成一个比代码还长而且可能包含更多错误的正式的证明。自动化的工具很不错,但并不总是可行的。下文描述了一种居中的方法:半正式地论证正确性。
首要的方法就是将要考虑的代码分成小的部分——从单独的一行,如一个函数调用,到少于十行的块——再论证它们的正确性。论证只需足以说服你吹毛求疵的程序员伙伴就可以了。
应该选择一块区域让程序状态(即程序计数器和每个“活着的”对象的值)的每个端点都满足一个很容易描述的属性,而且每个区域的功能(状态变更)都很容易作为一个单一的任务来描述,这些会使论证更容易。这些端点属性概括了函数的前置条件和后置条件以及循环和类(关于它们的实例)的不变量这样的概念。尽力让这些区域彼此独立会简化论证,并且在区域需要修改时,这是不可或缺的。
很多为人熟知(也许没有多么遵循)、被认为是“好”的编码惯例可以让论证更容易。因而,本来只是为了论证你的代码,现在你已经开始考虑使用更好的风格和架构了。意料之中的是,大多数这样的惯例可以使用静态分析工具来检查:
1. 避免使用goto语句,因为它们让相离较远的区域高度关联。
2. 避免使用全局变量,因为它们让所有使用它们的区域相依赖。
3. 每个变量都应该有尽量小的作用域。例如,一个局部对象可以刚刚在它第一次使用之前声明。
4. 对象要相关时,让它们成为不变的。
5. 使用水平的和垂直的空白让代码更易读。例如,根据相关的结构对齐并使用空行来分离两个区域。
6. 通过为对象、类型、函数等使用描述性(但相对较短)的名字让代码自文档化。
7. 如果你需要一个套入的区域,就把它作为一个函数吧。
8. 让函数简短并专注于单个的任务。以前的24行限制规则依然适用。尽管屏幕大小和分辨率改变了,人类的认识方式从1960年来却没有怎么变过。
9. 函数应该用尽量少的参数(4个是一个比较好的上界)。这并没有限制传递给函数的数据:将相关的参数组合到一个对象中,可以获得对象一致性的好处,并且也可以节省论证,例如连续性和一致性。
10. 更通用地,每个代码单元,从一块至一个库,应该有窄接口。较少的通信可以减少所需的论证。这意味着get函数是不必要的——不要从一个对象获取信息用来做某事。取而代之的是,叫该对象用已有的信息来做该事。也就是说,封装是全部的和唯一的窄接口。
11. 为了保持类的不变性,应该不鼓励set函数的使用,因为set函数倾向于打破对象的一致性状态。
在论证其正确性的同时,论证代码也能让你更容易理解它。交流想法之中,你也会得到每个人的收益。