目录
2、Specification and Contract (of a method)
3、Behavioral equivalence(行为等价性)
一、简介
规约(spec):spec是软件构造中一种常用的手段,它规定了每个方法的作用,是在编程之前对各部分模块的总体设计。有了spec,才能分派任务,确定“供需双方”的责任,在调用时双方都要遵守,从而使程序与客户端之间达成一致。并定义正确实现的含义。一个软件在设计阶段就应该完成规约的设计,并且根据规约设计出测试程序,这会大大减少软件的生产周期。
编程语言中的程序和方法:“方法”是程序的“积木”,可以被独立开发、测试、复用;使用“方法”的客户端,无需了解方法内部具体如何工作,即抽象的概念。
二、规约
1、Documenting in programming
变量:写出数据类型定义记录关于它的一个假设,例如此变量将始终引用整数;使用final关键字,变量在初始赋值后永远不会更改,它定义了设计决策:“不可改变”。
函数/方法:代码本身就蕴含着你的设计决策,但是远远不够。
写出假设:有效防止第一自己记不住、别人看不懂。
编写程序牢记两个目标:代码中蕴含给编译器的的“设计决策”、注释形式给自己和别人读的“设计决策”。
2、Specification and Contract (of a method)
2.1必要性
很多bug来自于双发之间的误解,不将规定写下,那么不同开发者的理解就可能不同,出现错误时难以定位。有了规约可以分派任务,确定“供需双方”的责任,在调用时双方都要遵守,从而使程序与客户端之间达成一致。并定义正确实现的含义。
2.2好处/作用
· 客户端无需阅读调用函数的代码,只需理解spec即可
·规约可以隔离“变化”,无需通知客户端
· 规约也可以提高代码效率
· 规约 扮演“防火墙”角色
2.3内容/要求
·输入/输出的数据类型
·功能和正确性
·性能
·只讲“能做什么”,不讲“怎么实现”
3、Behavioral equivalence(行为等价性)
要确定行为等效性,问题是我们是否可以用一个实现替代另一个实现。站在客户端视角看,两个函数行为不同但对用户来说是否等价。
单纯的看实现代码,并不足以判定不同的implmentation是否是“行为等价的” ;需要根据代码的spec(开发者与client之间形成的contract)判定行为等价性;在编写代码之前,就需要弄清楚spec如何协商形成、如何撰写。
4、规格结构:前置条件和后置条件
前置条件:对客户端的约束,在使用方法时必须满足的条件
后置条件:对开发者的约束,方法结束时必须满足的条件
契约:如果前置条件满足了,后置条件必须满足。前置条件不满足,则方法可做任何事情!
Java中的spec:除非在后置条件里声明过,否则方法内部不应该改变输入参数;应尽量遵循此规则,尽量不设计mutating的spec,否则就容易引发bugs;程序员之间应达成的默契:除非spec必须如此,否则不应修改输入参数;尽量避免使用mutable的对象。
三、设计规约
1、Classifying Spec
1.1规约的强度
Spec变强是指:更放松的前置条件+更严格的后置条件。越强的规约,意味着implementor的自由度和责任越重,而client的责任越轻。
1.2确定性
确定的规约:给定一个满足precondition的输入,其输出是唯一的、明确的
欠定的规约:同一个输入,多次执行时得到的输出可能不同。欠定的规约通常有确定的实现。
1.3陈述性
操作式规约:如伪代码
声明式规约:没有内部实现的描述,只有“初—终”状态。声明式规约更有价值。
2、Diagramming Spec
某个具体实现,若满足规约则落在其范围内,否则,在其之外。程序员可以在规约的范围内自由选择实现方式,客户端无需了解具体使用了哪个实现。更强的规约,表达为更小的区域。
3、设计良好的规约
一个好的“方法”设计,并不是你的代码写的多么好,而是你对该方法的spec设计得如何:一方面client用着舒服;另一方面开发者编着舒服。
3.1规约应是内聚的
Spec描述的功能应单一、简单、易理解。
3.2规约应是信息丰富的
不能让客户端产生理解的歧义
3.3规约应足够强
太弱的spec,client不放心、不敢用 (因为没有给出足够的承诺)。开发者应尽可能考虑各种特殊情况,在post-condition给出处理措施。
3.4规约应足够弱
太强的spec,虽然client会很高兴,在很多特殊情况下难以达到,但给开发者增加了实现的难度。
3.5规约应使用抽象类型
在规约里使用抽象类型,可以给方法的实现体与客户端更大的自由度
3.6先决条件or后决条件?
客户端不喜欢太强的precondition,不满足precondition的输入会导致失败。惯用做法是:不限定太强的precondition,而是在postcondition中抛出异常:输入不合法。尽可能在错误的根源处fail,避免其大规模扩散。
衡量标准:检查参数合法性的代价多大?
总结:是否使用前置条件取决于(1) check的代价;(2) 方法的使用范围
·如果只在类的内部使用该方法(private),那么可以不使用前置条件,在使用 该方法的各 个位置进行check——责任交给内部client;
·如果在其他地方使用该方法(public),那么必须要使用前置条件,若client端不满足则方 法抛出异常。