系列文章目录
第五章:设计规约
文章目录
一、编程语言中的函数与方法
1.方法
方法是程序的积木,可以被独立开发,测试,复用,使用方法的客户端,无需了解方法内部具体是如何工作的–“抽象”
下图给出一个方法的例子,我们可以看到,可以分为方法的规约与方法的实现体两个部分。
二、 规约:程序的交流
1. 程序文档(举例)
在上面我们给出了java API documentation
其中包含了许多信息:
- 类的继承关系class hierarchy 与实现的接口 interfaces
- 直接的子类direct subclasses,对于接口interface来说,是interface的实现类。
- 类的描述
- 构造器摘要
- 我们可以调用的所有方法列表
- 对于构造器和方法的细节描述
(1). 方法的返回类型,名称,参数,已经抛出异常
(2). 整体的描述
(3).对参数的描述
(4). 对返回值的描述
2. 规约
规约其实描述了程序与客户端之间达成的协议。 在调用的过程中,双方都要遵守。
规约的好处:
方便开发者消除误解,理解代码与定位错误。
- 精确的规约可以有助于区分责任
- 客户无需阅读调用函数的代码,即其中的具体实现,只需要了解规约,知道如何使用即可
- 规约可以隔离变化:当修改程序后无需通知客户端,客户还可按照之前的方式继续调用
- 防火墙:防止恶意篡改程序
- 解耦:无需了解具体的实现
3. 行为等价性
4. 规约的结构:前置条件和后置条件
前置条件(requires):对客户端的约束,在使用方法的时必须满足的条件。
后置条件(effects):对开发者的约束,方法结束时必须满足的条件。
契约:当前置条件满足,后置条件必须被满足。
对于java来说:
- 静态类型的声明是一种规约,可以根据此进行静态类型的检查。
- 方法前的注释也是一种规约,但是需要我们人工去判断是否满足。
(1). 对于前置条件可以用@param来进行描述
(2). 对于后置条件可以用@return @throws来进行描述
例如:
需要强调的是在规约中只可以提及参数与返回值,而不可以去透露任何本地变量与私有属性的信息。否则就会出现内存泄漏的情况。
三、 设计规约
1. 规约的分类
(1). 规约的强弱
规约的强度S2>=S1:
S2的前置条件更弱,后置条件更强
可以用S2代替S1
而在下面的这几种情况中,它们的强弱无法比较。
(2). 规约的确定与不确定
确定的规约指:给定一个满足precondition的输入,他的输出是惟一的,明确的。
不确定的规约指:同一输入可以用多个输出
(3). 规约的操作式与声明式
操作式规约:伪代码,给出方法的具体实现流程。
声明式规约:没有内部实现的描述,只有始末状态。
一般情况下不应该使用操作式的规约,操作式的规约是为了解释maintainer的实现。
2.图形化规约
如上图所示:
每一个在上图中的点都代表着一个方法是实现。
程序员可以在规约的范围内自由选择实现的方式。
客户端无需了解具体使用了哪一个实现。
更强的规约,表达着更小的区域。
结合在上一讲中对规约强弱的区分:
3.设计一个好的规约
(1). 内聚性coherent
spec描述的功能应该单一,简单,容易理解。
(2). 返回值应该信息丰富
不能让客户端产生歧义。
(3). spec应该足够强
太弱的规约,客户不够放心。
(4). spec应该足够弱
太强的spec,在很多特殊情况下无法实现,给开发者增加了难度。
(5). 应该使用抽象类型
在规约中使用抽象类型,可以给方法的实现体和客户端更大的自由。
在java中,使用抽象类型指的是使用interface类型。
(6). 前置和后置条件
在正式执行前,要检查前置条件是否被满足。
不写前置条件,就要在代码内部实现check;
如果实现代价太大,在规约中加入前置条件,把责任交给客户端。
归纳:
是否使用前置条件取决于:
- check 的代价
- 方法的使用范围:
如果只在类的内部使用该方法(private),那么可以不设置前置条件,在使用该方法的各个位置进行check。
如果在其他地方使用该方法(public),那么必须设置前置条件,若客户端不满足则抛出异常。