什么是规约
规约是团队合作的关键。它就像一个合同:实现者负责满足合同的要求,使用该方法的客户端就可以依靠这个合同。事实上,就像真正的法律合同一样,规约对双方都有要求:当一个规约具有前提条件时,客户端需要满足这个前提条件;当客户端满足这个前提条件时,我们的方法需要满足合同中提出的要求。
为什么需要规约
在程序中最严重的bug产生于两个代码块之间交互时不同的行为之间的误解。虽然每个每个程序员心里都记着一个规约,但不是所有的程序员都把它写了下来。如果在一个团队里,不同的程序员心里有不同的规约,但都没有把它写下来时,当程序崩溃的时候,我们很难发现错误出现在哪里。而代码中的精确规约可以避免这一问题,从而免去我们不知道去哪里修改程序的痛苦。此外,规约对使用该方法的客户端是有好处的,因为他们省去了我们去读代码的任务,就可以知道这个方法要实现什么样的功能。
客户很容易读懂BigInteger.add这个方法的规约,它将两个BigInteger类型的数据进行相加。如果我们有的只有这些代码,我们要从头开始阅读BigInteger类中的构造函数,subtract函数,compareMagnitute函数才能明白这个函数要实现什么功能。
规约对一个方法的实现者有很多好处。因为它给了实现者改变方法内部的具体实现方法并且不用告知客户的自由。同时它也可以使编程变得更快,使用较弱的规约可以排除某个方法可能被调用的某些状态,这些在输入上的限制可以允许方法的实现者跳过代价昂贵并且不必要的检查,并且实现更加有效。
规约的行为就像是客户端和实现者之间的防火墙。它允许客户机不受该单元工作细节的影响——如果在一个方法之前有一个规约,我们就不需要读取这个方法的源代码来明白这个方法需要实现的功能。它也允许实现者不需要考虑客户端使用这个方法的细节 ——该方法的实现者不需要问客户他们准备怎么使用这个方法。
行为等价性
考虑下边的这两种方法。它们是相同的还是不同的?
规约的形式
Java中的规约
一些编程语言将preconditions和postconditions作为该语言的一部分,当程序运行甚至是编译的时候,运行系统会自动的检查这些规约是否满足。Java还没有走到这一步,但它的静态类型声明是一个方法的前提条件和后置条件中有效的一部分,即自动检查并由编译器执行的一部分。合同的其余部分必须在方法前面的注释中描述,并且一般取决于人类检查并保证它。
Java有文档注释的传统,其中的参数是由@param引起的句子进行描述的,返回结果由@return或者@throws引起的句子描述的。一个如下所示的规约:
空引用
在Java中,对象和数组的引用也可以采用特殊的值null,这意味着引用并不指向一个对象。null在Java的类型系统的一个不幸的空洞。
基本数据类型不能被赋值为null,当我们尝试给一个int类型的数据赋值null时,编译器会立即会给出错误。我们可以给非基本数据类型赋值null,如字符串或者数组等。当我们给他们赋值null时,编译器不会检查出错误,但是当我们运行时,如果我们对一个空的引用进行操作,如调用一个赋值为null的数组调用length方法时,程序会异常终止。 需要注意的是,null和空字符串或者空数组是不同的,在空字符串或空数组上,我们可以调用length方法,他们的返回值为0。
null值是麻烦并且不安全的,在编程的时候要避免使用它。在大多数Java编程中,null值是不被允许的,因此每个方法的的前提条件和后置条件都隐含了参数必须不为null的条件。如果一个方法允许返回null值,我们在规约中需要显示的声明它,但这并不是一个好主意。
一个规约说了些什么
方法的规范可以讨论方法的参数和返回值,但是它不应该谈论方法的局部变量或者包含该方法的类中的私有字段。
测试与规约
这个规约有一个强的前置条件,要求val必须被找到,但是他的后置条件有点弱,如果当数组中val出现不止一次时,他没有告诉我们应该选择哪一个val的数组索引作为值返回。即使我们在实现这个方法的时候选择最小的数组索引作为返回值,我们的测试也不能假设这种特定的行为。
可变方法的规约
考虑下边的描述一个改变某个对象方法的规约: