这篇文章是对软件构造课程第五节Designing Specification的总结。
这一节的内容是为后续ADT的铺垫,设计ADT的关键之一就是方法,而Spec就是对方法的规范化描述。本节转向“方法/函数/操作”如何定义—— 编程中的“动词”、规约。
一、编程语言中的函数与方法
“方法”是程序的“积木”,可以被独立开发、测试、复用。使用“方法”的客户端,无需了解方法内部具体如何工作—“抽象”。Spec就是沟通方法的桥梁。
一个完整的方法包括两个部分——方法的规约Spec与实现体implementation,如图
二、Spec为编程过程中的沟通而生
1.编程中的文档注释:对所编写程序的解释与记录。完整的编程分为两个部分,一为代码中蕴含的“设计决策”:给编译器读,二为注释形式的“设计决策”:给自己和别人读。如果没有文档注释,那么所编写的程序会最终变为垃圾,因为随着时间的流逝,没人会理解这段代码,也没人会愿意花时间去重读所有的源码。
2.Spec:规约是程序与客户端之间的一份合同,没规约,没法写程序;即使写出来,也不知道对错。同时Spec给“供需双方”都确定了责任,在调用的时候双方都要遵守。规约就是告知客户端,这个方法能做什么以及怎么用,告知开发人员,这个方法需要达到什么要求。核心:输入、输出、功能与正确性、性能。
3.行为等价性Behavioral equivalence
行为等价性是站在客户端视角看两个方法的等价性,根据规约来判断,即如果两个方法满足同一个规约,则称这两个方法在这个规约下是行为等价的。抛开规约谈行为等价性毫无意义。
例如,
上面两个方法在这个规约下,就是行为等价的。
4.Spec的结构——如何书写Spec
1.前置条件(precondition)vs后置条件(postcondition)
前置条件:requires,对客户端的约束,在使用方法时必须满足的条件。
后置条件:effects,对开发者的约束,方法结束时必须满足的条件
契约:如果前置条件满足了,后置条件必须满足。反之,前置条件不满足,则方法可做任何事情。
在Java中Spec的结构:
三个部分:简单描述方法,前置条件说明(@param),后置条件说明(@return and @throws)
例如,
关于Mutator方法,首先就是尽量避免设计,如果需要设计,一定在Spec中写明。
三、规约的衡量指标
规约的衡量由以下三个方面构成:规约的确定性(deterministic)、规约的陈述性(declarative)以及规约的强度(strong)
1.规约的强弱比较
Spec变强:更放松的前置条件+更严格的后置条件。更强的规约意味着客户端的输入集变大,开发者的输出集变小。更强的规约是对甲方的纵容和对乙方的施压。更强的规约也意味着“more clients can use it and fewer implementations can satisfy it”。
根据Spec的强弱,也可以做实现的替换:规约的强度S2>=S1,an implementation that satisfies S2 can be used to satisfy S1 as well, and it’s safe to replace S1 with S2 in program
例子
2.规约的确定性与欠定性(Deterministic vs underdetermined)
确定的规约:给定一个满足precondition的输入,其输出是唯一的、明确的
非确定的规约:同一个输入,多次执行时得到的输出可能不同
3.规约的陈述性
操作式规约:Spec包含了对方法是如何实现的描述,例如:伪代码
声明式规约:没有内部实现的描述,只有 “初-终”状态。声明式规约更有价值
内部实现的细节不在规约里呈现,放在代码实现体内部注释里呈现。
4.规约图
5.设计好的规约
a.Spec描述的功能应单一、简单、易理解
b.太弱的spec,client不放心、不敢用 (因为没有给出足够的承诺)。开发者应尽可能考虑各种特殊情况,在post-condition给出处理措施。
c.太强的spec,在很多特殊情况下难以达到,给开发者增加了实现的难度(client当然非常高兴)。
d.对于输入的检查:1.放在precondition中,交给客户端来检查。2.开发者在内部检查。优先选择在内部检查,如果代价太大,再选择交给客户端。