上一篇关注了Java编程语言中的数据类型、变量、值及可变、不可变的类型、值、引用。本文则总结了方法/函数/操作的定义方法:规约。
编程语言中的函数、方法
参数类型、返回值类型是否匹配在静态检查阶段完成。
方法是程序的积木,对一部分程序的抽象,使用者无需了解其中细节
规约:交流为目的的编程
编程中的记录
- 如Java语言的API:其记录了类层次、接口列表以及对方法、构造函数的描述等。
- 代码中的一些操作也是记录:如final、变量的类型体现了设计的决策。
但这些仍然太过具体。需要单独的假设性质文字以方便自己和他人阅读。
规约(spec)
必要性:没有规约无法分派任务,完成后也不知道是否达到目的;可以统一不同开发者的理解。
- 优点:精确的规约有助于区分责任;客户端只需理解spec即可
- 作用:
- 规约可以隔离“变化”,无需通知客户端
- 规约也可以提高代码效率。
扮演防火墙角色:客户端不需要知道实现,实现者不需要知道如何被引用(如不需要确保输入正确性)
行为等价性
基于以上抽象,行为等价性衡量是否可以一种实现来代替另一种实现。
站在客户端视角:符合同一规约——等价
规约结构
写在方法之前,只讨论输入输出的数据类型、功能和正确性、性能等,不讨论具体实现
应该包含前置条件、后置条件、和函数期望完成的行为。
@param 输入参数的含义
@return 返回参数的含义
@throws 抛出异常的含义
前置条件,对客户端的约束,在使用方法时必须满足的条件
后置条件,后置条件:对开发者的约束,方法结束时必须满足的条件
异常行为:如果违反了前置条件,它会做什么
契约:如果前置条件满足了,后置条件必须满足。
如果在调用方法时前置条件不成立,则实现不受后置条件的约束。它可以自由地做任何事情,包括不终止,抛出异常,返回任意结果,进行任意修改,等等。
- 静态类型声明是一种规约,可据此进行静态类型检查。
- 方法前的注释也是一种规约,但需人工判定其是否满足。
(规约前为/**,/*为普通注释)
方法的规范可以讨论方法的参数和返回值,但它不应该讨论方法的局部变量或方法类的私有字段。对规约的读者来说是方法的实现是不可见的。
可变方法的规范:
- 除非在后置条件里声明过,否则方法内部不应该改变输入参数。应尽量遵循此规则,尽量不设计可变的规约,否则就容易引发bug。
除非spec必须如此,否则不应修改输入参数。
- 可变对象可以使简单的规范非常复杂,尽量避免使用Mutable的对象。可变对象降低了可更改性。程序中可能有很多变量指向同一个可变对象(别名),无法强迫类的实现体和客户端不保存可变变量的“别名”。涉及可变对象的契约现在就依赖于每个引用可变对象的人的良好行为,这使得情况变得复杂。
设计规约
评价规范
- 规约的确定性
- 规约的陈述性
- 规约的强度
如何比较两个规约,以判断是否可以用一个规约替换另一个?
规约的强度S2>=S1,就可以用S2替代S1。
更强标准:
- 前置条件更弱(放松)
- 后置条件更强(严格)
越强的规约,意味着实现者的自由度和责任越重,而客户端的责任越轻。
图示规约
这个空间中的每个点都代表一个方法实现。
某个具体实现,若满足规约,则落在其范围内;否则,在其之外。
程序员可以在规约的范围内自由选择实现方式,客户端无需了解具体使用了哪个实现。
更强的规约,表达为更小的区域:
- 更强的后置条件意味着实现的自由度更低了➔在图中的面积更小
- 更弱的前置条件意味着实现时要处理更多的可能输入,实现的自由度低了➔面积更小
设计好的规约
一个好的“方法”设计,并不是你的代码写的多么好,而是你对该方法的spec设计得如何。
一方面:客户端用着舒服
- 另一方面:开发者编着舒服
- Spec描述的功能应单一、简单、易理解
在强弱中做出取舍:
- 太弱的spec,client不放心、不敢用 (因为没有给出足够的承诺)。
- 太强的spec,在很多特殊情况下难以达到,给开发者增加了实现的难度。
在规约里使用抽象类型,可以给方法的实现体与客户端更大的自由度。
合适地使用前置条件和后置条件:
先决条件(precondition)
不写Precondition,就要在代码内部check;若代价太大,在规约里加入precondition,把责任交给client。
客户端不喜欢太强的precondition,不满足precondition的输入会导致失败。
惯用做法是: 不限定太强的precondition, 而是在postcondition中抛出异常:输入不合法。 这使得更容易在调用者代码中找到导致错误或错误的假设。