什么是规约
规约是软件构造中一种常用的手段,它规定了每个方法的作用,是在编程之前对各部分模块的总体设计。规约在软件构造中起到了“防火墙”的作用,调用它的客户端不需要知道这个方法是如何实现的,实现者也不需要知道调用者用它做什么,起到了解耦的效果。
规约的内容与结构
规约的内容
- 输入/输出的数据类型。
- 功能和正确性期望。
- 性能要求。
规约的结构
- 前置条件:对客户端的约束,在使用方法时必须满足的条件。
- 后置条件:对开发者的约束,方法结束时必须满足的条件。
- 契约:如果前置条件满足了,后置条件必须满足;前置条件不满足,则方法可做任何事情,也就是“你违约在先,我自然不需要遵守承诺”。
- 输入/输出的数据类型。
- 功能和正确性期望。
- 性能要求。
规约的结构
- 前置条件:对客户端的约束,在使用方法时必须满足的条件。
- 后置条件:对开发者的约束,方法结束时必须满足的条件。
- 契约:如果前置条件满足了,后置条件必须满足;前置条件不满足,则方法可做任何事情,也就是“你违约在先,我自然不需要遵守承诺”。
当前置条件被违反时,说明客户端有bug, 尽管实现者没有义务提醒,但可通过快速失败使bug更容易被找到和修复,例如抛出异常。
行为等价性
以下面两个函数为例,这两个函数都要求接受array和val两个参数,且val在array中只出现一次,返回val出现的索引。
static int findFirst(int[] array,int val){
for(int i = 0; i < array.length; i++){
if(array[i] == val){
return i;
}
}
return array.length;
}
static int findLast(int[] array,int val){
for(int i = array.length; i > 0; i--){
if(array[i] == val){
return i;
}
}
return -1;
}
我们判断行为等价性,也就是判断这两个函数是否可以相互替换。我们可以看到,这两个函数在行为上是不同的,但是对于客户端来说,这两个函数都符合如下规约,因此它们是等价的:
Java中的规约
Java中的静态类型声明是一种规约,可据此进行静态类型检查static checking。方法前的注释也是一种规约,但需人工判定其是否满足。
Java中方法参数写在
@param
后面,返回值写在
@return
后面,可能抛出的异常写在
@throws
后面。其中,
@param
后面的是前置条件,
@return
和
@throws
后面的是后置条件,例如上面的规约写成Java注释为:
可变方法的规约
-
除非在后置条件里声明过,否则方法内部不应该改变输入参数。
-
程序员之间应达成的默契:除非规约必须如此,否则不应修改输入参数。
-
mutable对象会使规约复杂化。
可变对象会让简单的问题变复杂,例如
程序中可能有很多变量指向同一个可变对象,我们也
无法强迫类的实现体和客户端不保存可变变量 的“别名”,因而我们一般在规约中约定,方法的参数均为不可变对象,例如:
测试方法的规约
黑盒测试
依据规约设计测试用例,不考虑实现,同其他客户端一样,例如:
设计良好的规约
比较规约
规约的确定性
- 除非在后置条件里声明过,否则方法内部不应该改变输入参数。
- 程序员之间应达成的默契:除非规约必须如此,否则不应修改输入参数。
- mutable对象会使规约复杂化。
规约的确定性指描述的输出是否确定。
规约的陈述性规约的陈述性指仅仅描述了输出,还是描述了计算过程(不应该这样做,这样相当于暴露了内部实现过程)。
规约的强度规约的强度用于判断哪种规约更好。
规约的强度S2>=S1就可以用S2替换S1,规约的强度指更放松的前置条件和更严格的后置条件。也就是更少的要求,更多的承诺。
规约的要求 内聚的 规约描述的功能应单一、简单、易理解,例如如下的规约实际上描述了两件事,因而应该分开形成两个方法。 信息丰富的信息丰富指不能让客户端产生理解的歧义,例如如下规约:
一旦返回null,无法判断是key不存在,还是key对应的值就是null。 规约应当足够强 太弱的规约,客户端不放心、不敢用 (因为没有给出足够的承诺),例如下图。 开发者应尽可能考虑各种特殊情况,在前置条件中给出处理措施。 例如该规约没有说明遇到null时应该对list如何处理,是否保留null? 规约应当足够弱 太强的spec,在很多特殊情况下难以达到,给开发者增加了实现的难度。例如这是一个过强的规约,因为无法保证一定能打开这个文件,可能存在这个文件不存在和文件系统损坏等问题。
规约应当使用抽象数据类型 在规约里使用抽象类型,可以给方法的实现体与客户端更大的自由度。在Java中,这里的抽象数据结构一般使用接口,例如这里应该把ArrayList改为List。
前置条件 不写前置条件,就要在代码内部进行检查。如果检查的代价太大,就在规约里加入前置条件, 把责任交给客户端。但是 客户端不喜欢太强的前置条件,因为不满足前置条件的输入会导致失败。 惯用做法是: 不限定太强的precondition,而是在postcondition中抛出异常:输入不合法(尽可能在错误的根源处抛出异常,避免其大规模扩散)。 是否使用前置条件取决于(1) check的代价;(2) 方法的使用范围。- 如果只在类的内部使用该方法(private),那么可以使用前置条件(方法内部 )不需要判断输入是否满足,认为客户端会保证前置条件),在使用该方法的各个位置进行检查——责任交给内部客户端。
- 如果在其他地方使用该方法(public),那么可以不使用/放松前置条件(在方法内部检查输入是否满足),若client端不满足则方法抛出异常。



