在软件构造的过程中,每一个函数、方法都有自身具体的实现方式,而其实现方式是依据所给定的规约(spec)来实现的。本次就是来学习如何定义编程中的“规约”。
一. 编程语言中的函数(Function)和方法(Method)
“void”意味着没有返回值,而java对于返回值类型是否匹配,是在静态类型检查阶段完成。
而对于参数类型是否匹配的检查,也是在静态类型检查阶段完成。
“方法”是程序的“积木”,可以被独立开发、测试、复用。而使用“方法”的客户端,无需了解方法内部是如何具体工作的——“抽象”
二. 规约:程序之间的交流
(1)编程文件的文档
e.g. 以Java的API文档为例
其中包括:
· 类的层次结构(Class hierarchy)以及一个包含所有被实现的接口的list
· 所有直接继承的子类(Direct Subclasses),对于一个接口,所有实现其的具体的类
· 对于类的描述(description)
· 构造器(Constructor)的总结
· 一个包含所有客户能直接调用的方法(Method)
· 对于每个方法和构造器的详细的描述(Detailed description)
- 方法的声明(method signature):方法的名字,方法的返回类型,方法的参数,还包括异常。
- 完整的描述
- 参数(Parameters):对于方法的各个参数的描述
- 返回值:对于方法的返回值的描述
为什么要编写文档?
· 因为程序时对文档中内容的具体实现,如果我们不记录下他们,我们无法记住;其它程序员阅读代码的时候也可能无法理解。
· 与编译器之间的交流(Communicating with computer):代码中所蕴含的“设计决策”是给编译器阅读理解的。
· 与其它人之间的交流(Communicating with other people):注释形式的“设计决策”是给自己和别人读的。这种注释形式使得代码能更好的被理解,从而他人在修改、提高、复用代码的时候会更加简单。
(2)规约(合同)
规约(Specification)是团队合作的关键,没有规约就无法写程序,即使实现了,也不知道对错。
规约在软件工程之中扮演着“合同”一样的角色。是客户端和程序之间所达成一致的体现。
规约(Spec)给“供需双方”都确定了责任,在调用的时候双方都需要遵守
规约的优势:
· 使得不同开发者之间对于方法的理解得到统一
· 可以根据规约来定位错误
· 精确的规约有助于区分责任
· 客户无需阅读代码,只需要理解规约即可
· 规约有助于方法的实现,因为规约可以隔离“变化”而无需通知客户。
· 规约可以提高代码效率
· 规约在用户和实现者之间扮演着“防火墙”一样的角色
- 规约防止客户获得代码单元的具体实现
- 规约将实现者屏蔽在单元的使用细节之
- 防火墙的结果是“解耦(decoupling)”——只要对于代码的变化符合规约,那么这种对于代码的变化就可以不告知客户而独立进行
注意:规约只讲“能做什么”,不讲“怎么实现”
e.g. ArrayList.add()的spec
(3)行为等价性(Behavior equivalence)
定义:两个方法是否等价,即二者是否可以相互替换。
e.g. 下列二者函数的行为不同,当并不一定不等价
原因是我们需要站在客户端视角来看行为等价性,即函数的等价性要根据函数的规约来比较。
对于上述例子而言,如果val在arr中只出现一次,那么两个方法的行为就是等价的。
即针对如下规约而言,二者是等价的,因此行为等价性与具体的实现无关
(4)规约的结构:前提(pre-condition)和后置(post-condition)
一个方法的规约由一些语句组成
· 前提(Pre-condition):requires——对客户端的约束,在使用方法时所必须要满足的条件
· 后置(Post-condition):effects——对开发者的约束,方法结束时所必须满足的条件
因此,如果客户端的输入满足了前提,那么方法的后置条件必须被满足。但是如果前提并没有被满足,那么对于非法输入,方法可以做任何事情。
静态类型(static)声明也是一种规约,可据此进行静态类型检查(static checking)
方法前的注释也是一种规约,但是需要人工判定其是否满足。
在团队合作之中,对于一方法的规约而言,不应当改变输入参数,除非在后置条件内部声明过;团队中的每个程序员都应当遵循规则,尽量不涉及可变的规约(spec),否则就容易引发bug。
二. 设计规约
(1)规约分类(Classifying specification)
规约的确定性(deterministic):规约对于所有的输入只有一个可能的输出还是允许规约的实现者从一系列合法的输出中进行选择。
规约的陈述性(declarative):规约只是说明了输出应当是什么,还是说明了如何获得输出
规约的强度(strong):规约声明了对于合法的实现的集合是小还是大
规约强度的比较可以判断一个规约是否可以替换另一个。
规约S2 >= S1:
· S2的前置条件比S1弱
· S2的后置条件比S1强
那么就可以用S2代替S1
注意:对于后置条件的比较是要在前提条件相同的前提下进行比较,不然就无法进行比较
一个更强的规约意味着规约的实现者更加小心,他的责任也越重;而client的责任越轻,即更多的client可以使用这个方法。
确定的规约 vs 不确定(Underdetermined)的规约
确定的规约:给定一个满足pre-condition的输入,其输出是唯一的、明确的
不确定的规约:同一个输出可以有多个输出,即同一个输入,多次执行时得到的输出可能不同。
e.g. 上者是一个不确定的规约,下者是一个确定的规约
操作式(operational)规约 vs 声明式(declarative)规约
操作式规约:提供了方法是如何运行的 e.g. 伪代码
声明式规约:没有内部的实现描述,只有“初-终”状态
(2)规约的图表表示
每个点都代表了一个方法的实现,某个具体实现若满足规约,则其落在范围内,反之落在范围外
程序员可以在规约的范围内自由的选择实现方式。
一个更强的规约会表达为一个更小的区域。
· 更弱的前提条件:意味着实现时要处理更多的可能的输入,那么实现的自由度就会下降,故面积更小。
· 更强的后置条件:意味着实现的自由度更低了,故面积更小。
e.g.
(3)如何设计一个好的规约
内聚的(coherent):简洁,清楚,合理的结构,都意味着规约更加容易被理解
信息要丰富(informative),但是不能够产生歧义
强度:规约需要覆盖到一般的情况,但是不能够太强,因为太强的规约在很多特殊的情况下难以达到。
抽象:在规约里使用抽象类型,可以给方法的实现体与客户端更大的自由度
前置条件的检查:一般是需要在方法的内部进行检查,但是如果代价太大,那么就需要在规约之中加入前提,把责任交给client。但是客户端不喜欢太强的规约。因此惯用做法是不限定太强的pre-condition,而是在post-condition之中对于非法输入抛出异常。尽可能的在错误的根源处进行fail,避免其扩大规模。
资料来源 MIT-6.031:06、07 CMU-15-214:04、11 哈工大软件构造课程