设计规约
设计规约
个人复习用 Skimming
规约:编码与沟通
文档化的必要性
Java API文档给出了详尽的对类内方法的描述,如实现接口,类的大概描述,构造方法的大致内容,各个方法的参数与功能,以及一些细节(参数,完整描述,决策等)
自己进行类内方法的设计时,我们可以给出数据类型定义,可以用final关键字给出部分“不可变”的设计决策,但仍需要交代更多设计决策上的内容。
- 记录“假设”——个人记忆,方便后来的使用者理解
- 代码中蕴含的“设计决策”:给编译器读
- 注释形式的“设计决策”:给自己和别人读
规约与协定
Spec为程序确定运行对错的标准,给供需双方明确了责任,在调用时需要双方遵守,是程序与客户端达成的一致。
方便定位错误,消除双方误解,区分责任。隔离变化,内部计算与客户端分隔,无需通知客户端;“防火墙”,解耦,无需了解具体实现。客户端无需理解代码只需看懂spec即可使用功能,降低代码复用门槛。
- 规定内容:输入/输出数据类型 功能和正确性 运行性能
- 只有“能做什么”,不谈“怎么实现”
行为等价性
从客户端看,两个函数是否可以相互替换。这是由规约规定的内容决定的。
规约结构(重):前置条件与后置条件
- 前置条件:对客户端的约束,在使用方法时必须满足的条件(require)
- 后置条件:对开发者的约束,方法结束时必须满足的条件(effects)
- 契约:if 前置条件 then 后置条件
- 出于良心,尽可能在前置条件不满足时fail fast提示客户端
方法前的注释也是一种规约,但需人工判定其是否满足
preconditions: @param
postconditions:@return @throws
注意:
- 不可改变输入参数(除非后置条件生命,但仍尽量避免输入参数的改动)
- 尽量不设计mutating的spec(降低漏洞可能)
- 避免使用mutable对象和可变全局变量(导致程序难以修改)
- Spec不能靠道德约束,而是靠代码与契约保证(在规约里确定不可变,等等)
*测试&验证规约:略
进行规约的设计
规约分类
规约的质量量化:确定性 陈述性 强度
强度S2>S1:前置条件更弱 后置条件更强,S2可以代替S1
无法比较强度的——两个规约可能不相交/有重叠
越强的规约,意味着implementor的自由度和责任越重,而client的责任越轻。
确定性:给定一个满足precondition的输入,输出唯一明确
欠定:同一输入可以有多个输出(通常有确定的实现)
非确定的:同一输入多次执行得到输出不同
我们认为不满足确定性的规约是欠定的,而非“非确定的”
- 操作式规约:伪代码(不宜使用,内部实现细节在代码实现体内部注释里呈现)
- 声明式规约:仅“初-终”状态描述 无内部实现(更有价值)
不要透露任何内部实现信息:局部参数名称,“遍历”,“快速排序”等类似内容
规约的图表化
某个具体实现——图上一个点
规约——图上一个范围
实现满足规约——点落在范围内,反之范围外
程序员可以在规约范围内自由选择实现方式,这是客户无需具体了解的
- 更强的后置条件意味着实现的自由度更低了➔在图中的面积更小
- 更弱的前置条件意味着实现时要处理更多的可能输入,实现的自由度低了➔面积更小
优秀规约的判定标准
对客户与开发者都是便捷的(编写/使用)
- 内聚性:Spec描述功能单一、简单、易于理解
- 信息丰富:Spec不能使客户端产生歧义
- 强度不过弱:Spec不能过弱,让client敢于使用这个方法(开发者尽可能考虑特殊情况,postcondition给出处理措施
- 强度不过强:Spec不能过强,否则对于开发者难以实现
- 使用抽象类型(泛型
):给方法的实现体与客户端更大的自由度 - 前置/后置条件tradeoff:不添加前置条件->代码内部check/责任交给用户保证
惯用做法:不限定太强的precondition,而在postcondition中抛出输入不合法的异常,并尽可能在错误的根源处fail,避免错误大规模扩散。
tradeoff总结:
- 是否使用前置条件取决于check的代价和方法的使用范围
- 如果只在类的内部使用该方法(private),那么可以不使用前置条件,在使用该方法的各个位置进行check——责任交给内部client;
- 如果在其他地方使用该方法(public),那么必须要使用前置条件,若client端不满足则方法抛出异常。