什么是规约

规约是团队合作的关键。它就像一个合同:实现者负责满足合同的要求,使用该方法的客户端就可以依靠这个合同。事实上,就像真正的法律合同一样,规约对双方都有要求:当一个规约具有前提条件时,客户端需要满足这个前提条件;当客户端满足这个前提条件时,我们的方法需要满足合同中提出的要求。

为什么需要规约

在程序中最严重的bug产生于两个代码块之间交互时不同的行为之间的误解。虽然每个每个程序员心里都记着一个规约,但不是所有的程序员都把它写了下来。如果在一个团队里,不同的程序员心里有不同的规约,但都没有把它写下来时,当程序崩溃的时候,我们很难发现错误出现在哪里。而代码中的精确规约可以避免这一问题,从而免去我们不知道去哪里修改程序的痛苦。此外,规约对使用该方法的客户端是有好处的,因为他们省去了我们去读代码的任务,就可以知道这个方法要实现什么样的功能。

下边是一个例子,他的代码如下:
软件构造之为什么需要规约

客户很容易读懂BigInteger.add这个方法的规约,它将两个BigInteger类型的数据进行相加。如果我们有的只有这些代码,我们要从头开始阅读BigInteger类中的构造函数,subtract函数,compareMagnitute函数才能明白这个函数要实现什么功能。

规约对一个方法的实现者有很多好处。因为它给了实现者改变方法内部的具体实现方法并且不用告知客户的自由。同时它也可以使编程变得更快,使用较弱的规约可以排除某个方法可能被调用的某些状态,这些在输入上的限制可以允许方法的实现者跳过代价昂贵并且不必要的检查,并且实现更加有效。             

规约的行为就像是客户端和实现者之间的防火墙。它允许客户机不受该单元工作细节的影响——如果在一个方法之前有一个规约,我们就不需要读取这个方法的源代码来明白这个方法需要实现的功能。它也允许实现者不需要考虑客户端使用这个方法的细节 ——该方法的实现者不需要问客户他们准备怎么使用这个方法。

软件构造之为什么需要规约

行为等价性

考虑下边的这两种方法。它们是相同的还是不同的?

软件构造之为什么需要规约

当然,从代码上来说,他们是不同的。为了确定他们之间是否具有行为等价性,我们需要考虑我们是否可以用其中的一个实现代替另一个实现。
          
除了代码不同之外,它们实际上的行为也是不同的:
当val缺失时,FindFirst返回arr的长度,FindLast返回-1;             
当val出现两次时,FindFirst返回较小的数组索引,FindLast返回较大的数组索引
但是当val正好只在数组出现一次时,这两种方法的行为是相同:它们都返回相同的数组索引。如果用户并不考虑其他情况,当他们使用这个方法时,他们输入一个val只出现一次的数组,这时候这两种方法就是一样的,而且我们可以将其中一个方法用另一个方代替。为了使一个方法额度具体实现替换为方法的另一个实现,并知道什么情况下可以接受,我们需要一个规约,确切地说明客户端需要依赖什么在这种情况下,我们的规约应该设计为如下形式:
软件构造之为什么需要规约


规约的形式

一个方法的规范由两个子句组成:            
前提条件:由关键字require引起,它是客户端需要满足达到的义务,调用方法的条件。             
后置条件,由关键字effects引起,它是该方法的实现者的义务。如果前提条件是调用的状态,方法必须满足规约的后置条件,如返回相应的值,抛出特定的异常,修改或不修改对象等等。如果该方法的前提条件没有满足,调用该方法后,该方法可以做任何事情,如抛出异常,返回任意结果等

Java中的规约

一些编程语言将preconditions和postconditions作为该语言的一部分,当程序运行甚至是编译的时候,运行系统会自动的检查这些规约是否满足。Java还没有走到这一步,但它的静态类型声明是一个方法的前提条件和后置条件中有效的一部分,即自动检查并由编译器执行的一部分。合同的其余部分必须在方法前面的注释中描述,并且一般取决于人类检查并保证它。

Java有文档注释的传统,其中的参数是由@param引起的句子进行描述的,返回结果由@return或者@throws引起的句子描述的。一个如下所示的规约:

软件构造之为什么需要规约
在Java中如下:

 软件构造之为什么需要规约


空引用

在Java中,对象和数组的引用也可以采用特殊的值null,这意味着引用并不指向一个对象。null在Java的类型系统的一个不幸的空洞。

基本数据类型不能被赋值为null,当我们尝试给一个int类型的数据赋值null时,编译器会立即会给出错误。我们可以给非基本数据类型赋值null,如字符串或者数组等。当我们给他们赋值null时,编译器不会检查出错误,但是当我们运行时,如果我们对一个空的引用进行操作,如调用一个赋值为null的数组调用length方法时,程序会异常终止。 需要注意的是,null和空字符串或者空数组是不同的,在空字符串或空数组上,我们可以调用length方法,他们的返回值为0。

null值是麻烦并且不安全的,在编程的时候要避免使用它。在大多数Java编程中,null值是不被允许的,因此每个方法的的前提条件和后置条件都隐含了参数必须不为null的条件。如果一个方法允许返回null值,我们在规约中需要显示的声明它,但这并不是一个好主意。



一个规约说了些什么

方法的规范可以讨论方法的参数和返回值,但是它不应该谈论方法的局部变量或者包含该方法的类中的私有字段。

测试与规约

在测试中,我们讨论了只考虑规约的黑盒测试,以及在知道方法内部实现的情况下的白盒测试。但值得注意的是,即使是白盒测试也必须遵循规范。我们的实现可以提供比规范要求更强有力的保证,或者它可能具有规范未定义的特定行为。但是我们的测试用例不应该依赖于这种行为,它必须遵守合同。
例如,假设我们正在测试下边方法的规约:
软件构造之为什么需要规约

这个规约有一个强的前置条件,要求val必须被找到,但是他的后置条件有点弱,如果当数组中val出现不止一次时,他没有告诉我们应该选择哪一个val的数组索引作为值返回。即使我们在实现这个方法的时候选择最小的数组索引作为返回值,我们的测试也不能假设这种特定的行为。 

可变方法的规约

考虑下边的描述一个改变某个对象方法的规约:软件构造之为什么需要规约

首先,根据后置条件,这个方法给出了两种约束:第一个告诉我们List如何被修改,第二告诉我们返回值如何确定。  
其次,根据提条件,它允许我们将某个List中的元素添加到另一个List中,直到所有元素都添加完,但是,将某个列表元素添加到自身中的行为是未定义的。如果这两个List是相同,那么将其中一个List中阿元素加到另一个List中,会导致这个List一直增长,永远不会结束,正如下面的快照图所示一样。这时候,这个简单的算法不会终止,当列表对象变得很大,以至于它耗尽了所有可用的内存时,程序将抛出一个内存错误导致终止。
软件构造之为什么需要规约软件构造之为什么需要规约软件构造之为什么需要规约

 总结

一个规约的行为就像程序的实现者和客户之间关键的防火墙。它使得独立发展称为可能:客户可以在不知道源码的情况下使用某个方法,实现者可以在不用考虑该方法是如何被使用的情况下,自由的实现满足规约的代码。

相关文章: