目录
第5讲 设计规约
5.1 规约
5.1.1 规约
规约包含函数签名和注释。规约蕴含着设计决策,前者供编译器理解,后者供自己和别人理解。规约为供需双方都确定了责任,调用时双方都需要遵守。精确的规约有助于区分责任,调用方只需要理解规约就可以了解函数的输入输出、功能和性能。规约可以隔离变化,有助于解耦,也有助于提高性能。
规约只描述能做什么,不描述如何实现。
5.1.2 行为等价性
行为等价性是指在用户的视角看来,两个实现是否行为等价。即:对于给定的条件,是否每个输入都对应着相同的输出或者影响。
可以根据规约判断行为是否等价。在给定的规约下,若多个实现都符合相同的规约,则这些实现等价。需要注意的是,单纯的看代码,并不足以判定不同的实现是否是“行为等价的”,需要根据代码的规约判定行为等价性。因此,在编写代码之前,需要弄清楚规约如何协商形成、如何撰写。
5.1.3 前置条件和后置条件
方法的规约包含三个部分:
- 前置条件:对客户端的约束,在使用方法时必须满足的条件;
- 后置条件:对开发者的约束,方法结束时必须满足的条件;
- 异常行为:前置条件不满足时的行为。
前置条件满足时,必须保证后置条件满足;若前置条件不满足,则方法可以做任何事情。
在Java中,规约的实现方法如下:
static int find(int[] arr, int val)
requires: val occurs exactly once in arr.
effects: returns index i such that arr[i] == val.
/**
* Find a value in an array.
* @param arr array to search, requires that val occurs exactly once in arr
* @param val value to search for
* @return index i such that arr[i] == val
*/
static int find(int[] arr, int val);
前置条件和后置条件不一定总是在@param和@return之后,这点需要注意。
5.1.4 规约的强度
对于规约、,有,当且仅当
- 的前置条件比更弱或相等;
- 的后置条件比更强或相等。
较强的规约可以替代较弱的规约。
5.2 设计规约
设计规约时,应当包括方法的参数和返回值,但不应该包括方法的局部变量或方法类的私有字段。除非在规约进行了声明,不应当更改方法的输入参数。
设计规约时,需要尽量避免使用可变的类型。例如:调用者和方法本身都保存了返回值的指针,则对于这个值的修改会导致意想不到的结果。不应当依靠调用者的“良心”,而应当尽量使用不可变类型或者进行防御性拷贝。
5.2.1 规约的分类
可以通过确定性、陈述性和强度比较规约。确定的规约是指给定一个满足前置条件的输入,其输出是唯一的、明确的,相对的是欠定的规约,欠定的规约一般有确定的实现。相对于操作式规约(如伪代码),声明式规约更有价值。
5.2.2 规约的图示
对于一个具体实现,若满足规约,则落在其范围内;否则,在其之外。在图示中,更强的规约表达为更小的区域。
5.2.3 设计规约
设计规约的原则如下:
- 内聚的:规约描述的功能应单一、简单、易理解;
- 信息丰富的:不能让客户端产生理解的歧义;
- 足够强的规约:开发者应尽可能考虑各种特殊情况,在后置条件中给出处理措施;
- 足够弱的规约:太强的规约,在很多特殊情况下难以达到,给开发者增加了实现的难度;
- 在规约里使用抽象类型:以给方法的实现体与客户端更大的自由度;
惯用做法是,不限定太强的前置条件 ,而是在后置条件中抛出异常:输入不合法。尽可能在错误的根源处 fail ,避免其大规模扩散。