第五章 设计规约
第五章 设计规约
函数和方法(Functions & methods)
-
参数类型是否匹配,在静态类型检查阶段完成
-
返回值类型是否匹配,也在静态类型检查阶段完成
-
规约可以被检测,方法的注释无法检测
-
“方法”是程序的“积木”,可以被独立开发、测试、复用
-
使用“方法”的客户端,无需了解方法内部具体如何工作 — “抽象”
-
方法的结构
规约(Specification)
编程中的文档(Documenting in programming)
-
记录设计决策
- 变量数据类型定义:写下一个变量的类型记录了一个关于它的假设:例如,这个变量将始终引用一个整数。
- final 关键字定义了设计决策:“不可改变”
- 代码本身就蕴含着你的设计决策,但是远远不够
-
为什么要写出设计决策?
- 第一:自己记不住
- 第二:别人不懂
-
两种设计决策
- 代码中蕴 含的"设计决策":给编译器读
- 注释形式的“设计决策”:给自己和别人读
规约和合同(Specification and Contract)
-
没规约,没法写程序;即使写出来,也不知道对错
-
Spec 给“供需双方”都确定了责任,在调用的时候双方都要遵守
-
为什么写规约?
-
很多bug来自于双方之间的误解
-
不写下来,那么不同开发者的理 解就可能不同
-
没有规约,难以定位错误
- 代码中的精确规范让您可以将责任归咎于代码片段,并且可以免去您对修复应该去哪里的困惑。
-
-
规约的好处
-
精确的规约,有助于区分责任
-
客户端无需阅读调用函数的代码,只需理解spec即可
-
规约可以隔离“变化”,无需通知客户端
- 实现者可以自由变化满足规约即可
-
规约也可以提高代码效率
-
规约:扮演“防火墙”角色
- 实现解耦,客户端代码和单元代码独立更改,只要更改符合规范即可
-
-
规约包含
-
输入/输出的数据类型
-
功能和正确性
- 只讲"能做什么",不讲 “怎么实现”
-
性能
-
行为等价性(Behavioral equivalence)
-
行为等价性判定
- 根据规约判断是否行为等价
- 都符合规约则等价
规约
-
结构
-
前置条件Precondition(keyword requires)
- 对客户端的约束,在使用方法时必须满足的条件
-
后置条件Postcondition(keyword effects)
- 对开发者的约束,方法结束时必须满足的条件
-
异常行为行为(Exceptional behavior)
- 如果违反前提条件会怎样
-
-
形式组成
-
静态类型声明(前置条件和后置条件的一部分)
- 可据此进行静态类型检查
-
方法前的注释
- 人工判定其是否满足
-
-
说明
-
如果前提条件成立,则该方法必须通过返回适当的值、抛出指定的异常、修改或不修改对象等来遵守后置条件。
-
如果调用方法时前置条件不成立,则实现不受后置条件约束。
-
健壮性在不满足前提条件的时候发挥作用:通过让程序尽早出错来使bug更容易发现和修复
-
Java中前置条件和后置条件的标志
-
前置条件
-
@param
- 后接变量名
-
-
后置条件
-
@return
-
@throws
- 后接抛出异常类名
-
-
-
方法的规范可以谈论方法的参数和返回值,但绝不能谈论方法的局部变量或方法类的私有字段。
-
改变参数
- 除非在后置条件里声明过,否则方法内部不应该改变输入参数
- 尽量不设计mutating的spec,否则就容易引发bugs。
-
尽量避免使用mutable的对象
-
mutable对象使简单的合同变复杂
- 程序中可能有很多变量指向同一个可变对象(别名)
- 无法强迫类的实现体和客户端不保存可变变量 的"别名"
-
mutable对象降低了可变性
- mutable对象要求开发者或客户端不要修改,否则要做显式的拷贝,防止相互干扰。而Immutable对象可根据需要,自己引入缓存做性能改进。
-
-
设计规约
规约性质
-
规约的确定性
- 规范是否只为给定的输入定义了一个可能的输出,或者允许实现者从一组合法的输出中进行选择?
-
规约的陈述性
- 规范只是描述输出应该是什么,还是明确说明如何计算输出?
-
规约的强度
- 规范有一小部分合法的实现,还是一大类?
比较规约
-
规约的强度 S2 >=S1
-
二者之一或全部
-
S2前置条件弱于S1
- 对用户的要求少了
-
S2的后置条件强于S1
- 判断在满足S1的前置条件下,后置条件强度的变化
- 对用户的承诺更多了
-
-
S2的实现可以实现S1
-
-
两个弱或两个都强无法比较
-
规约越强,实现的方法越少,自由度越低
-
更多用户可以使用
-
越强的规约,意味着implementor的自由度和责任越重,而client的 责任越轻。
规约图
-
该空间中的每个点代表一个方法实现
-
规范定义了所有可能实现空间中的区域
-
一个给定的实现要么根据规范运行,满足前置条件-隐含-后置条件契约(它在区域内),或者不满足(区域外)
-
更强的规约,表达为更小的区域,强规约被弱规约包含
-
无法比较的规约可相互交叉,也可不相交
-
示例
-
find First 和find Last为两种实现
-
设计好的规约
-
一个好的“方法”设计,并不是你的代码写的多么好, 而是你对该方法的spec设计得如何。
- 一方面:client用着舒服
- 另一方面:开发者编着舒服
-
好的特性
-
内聚的(coherent)
- Spec描述的功能应单一、简单、易理解
-
规约应该足够强
-
考虑各种特殊情况,在后置条件中给出处理措施
-
例如
没有充分阐明遇到null之后参数是否变化
-
-
-
规约应该足够弱
-
太强的spec,在很多特殊情况下难以达到,给开发者增加了实现的难度
-
例如
太强了,如果文件不存在,或者缺少权限就打不开了- 规范应该说一些更弱的东西:它尝试打开一个文件,如果成功,该文件具有某些属性。
-
-
-
使用抽象类型
-
在规约里使用抽象类型,可以给方法的实现体与客户端更大的自由度
-
例如
这会强制客户端传入一个 ArrayList,并强制实现者返回一个 ArrayList。
-
-
-
是否使用前置条件
-
衡量标准
-
检查参数合法性的代价
- 不写Precondition,就要在代码内部check;若代价太大, 在规约里加入precondition, 把责任交给client
-
方法的使用范围
- 如果只在类的内部使用该方法(private),那么可以不使用前置条件,在使用 该方法的各个位置进行check–责任交给内部client;
- 如果在其他地方使用该方法(public),那么必须要使用前置条件,若client端不满足则方法抛出异常。
-
-
惯用做法是:不限定太强的precondition,而是在postcondition中抛出异常:输入不合法
- 这使得在调用者代码中更容易找到导致传递错误参数的错误或错误假设。
- 尽可能在错误的根源处fail,避免其大规模扩散
-
-