设计规约
一.Functions & methods in programming languages
1.Method
- 方法的参数
- 方法的返回值
“方法”是程序的“积木”,可以被独立开发、测试、复用。使用“方法”的客户端,无需了解方法内部具体如何工作—“抽象”。
一个完整的“方法”需要有方法的规约和方法的具体实现体,如下图所示:
二.Specification:Programming for communication
1.Documenting in programming
为什么要写出假设?
- 自己记不住
- 别人不懂
程序既需要实现与计算机交流也要实现与其他人交流,这就决定了一个好的“假设”是必不可少的。
2.Specification and Contract(of a method)
什么是规约?
- 规约是团队工作的关键。没有规约就没法分派任务,无法写出程序;即使写出来也不知道对错。
- 规约是用户和程序员之间的一种契约。程序员的设计实现必须满足契约的要求,同时用户可以凭借契约去正确地使用方法。
- 规约给“供需双方”都确定了责任,在调用的时候双方都要遵守。
为什么要使用规约?
使用规约的现实需求:
- 1.程序的很多bug来自于双方之间的误解。
- 2.不将规约写下来,那么不同开发者对程序的理解就可能不同。
- 3.没有规约,难以定位错误。
使用规约的好处:
- 1.精确的规约,有助于区分责任。
- 2.客户端无需阅读调用函数的代码,只需理解规约即可。
- 3.规约可以隔离“变化”,无需通知客户端。
- 4.规约也可以提高编码效率。(E.g., 实现者不需要写代码确保输入的正确性,调用者的责任)
规约的例子
规约在用户和实现者之间扮演着防火墙的角色
- 首先,客户不需要直到方法的具体实现方式。客户只需要知道如何使用方法即可,而不需要了解方法的内部具体是如何实现的。
- 其次,实现者不需要知道方法是如何被用户使用的。只需要在规约中说明方法的具体使用规则即可,用户必须按照使用规则来使用方法。
3.Behavioral equivalence
确定行为等价性的关键问题就是两个方法的实现体是否可以相互替换。行为等价性是站在客户端视角来看的。
为了使一个方法的实现体替换另一个方法成为可能,我们需要利用规约来判断是否行为等价。当两个方法的实现体不同,但两个方法都符合同一个规约时,就称这两个方法满足行为等价。
4.Specification structure: pre-condition and post-condition
规约的结构
- 前置条件。前置条件是对客户端的约束,在使用方法时必须满足的条件。通过关键字requires声明。
- 后置条件。后置条件是对开发者的约束,方法结束时必须满足的条件。通过关键字effects声明。
- 异常处理。当前置条件被违反时该如何处理。
契约:如果前置条件满足了,后置条件必须满足。
当前置条件不满足时,方法可做任何事情。即“你违约在先,我自然不遵守承诺”。
当前置条件被违反时,说明客户端有bug,尽管实现者没有义务提醒,但可通过快速失败使bug更容易被找到和修复。
Java中的规约
- 静态类型声明是一种规约,可据此进行静态类型检查static checking。
- 方法前的注释也是一种规约,但需人工判定其是否满足。
Java中方法的注释
- 参数由@param声明,结果由@return和@throws声明。
- 将规约的前置条件写入@param中,后置条件写入@return和@throws中。
一个方法的规约可以涉及方法的参数和返回值,但是一定不能涉及方法内部的局部变量或者是方法内部私有的部分。因为如果这样,用户和实现者之间的“防火墙”就会被打破。
可变方法的规约
- 除非在后置条件里声明过,否则方法内部不应该改变输入参数。
- 应尽量遵循此规则,尽量不设计mutating的spec,否则就容易引发bugs。
- 程序员之间应达成的默契:除非spec必须如此,否则不应修改输入参数。
可变对象使得规约复杂化
- 程序中可能有很多变量指向同一个可变对象(别名)。这就需要对象保持一致。
- 无法强迫类的实现体和客户端不保存可变变量的“别名”。
阐述此类规约的几种方式
- 1.将规约的实现完全建立在客户端开发者的“良心”之上,不可靠!
- 2.将规约的实现建立在开发者这一边的“良心”…都靠不住!
- 3.利用“不可变”的性质
在此类规约的设计中,不应将责任完全推给某一方。关键在于“不可变”,在规约里限定住。
三.Designing specificattion
1.Classifying specifications
规约的比较原则
- 1.规约的确定性(描述的输出是否确定)。
- 2.规约的陈述性(只是描述了输出,还是描述了如何计算输出)。
- 3.规约的强度。
规约的强度
当满足如下条件时,我们说规约S2的强度比S1的强度高:
- S2的前置条件比S1弱。
- S2的后置条件比S1强。
当一个规约的强度比另一个规约的强度高,我们就可以用强度高的规约替换强度低的规约。
规约变强:更放松的前置条件+更严格的后置条件。如下图所示:
但是当两个规约中一个规约的前置条件和后置条件都比另一个规约的前置条件和后置条件弱,那么这两个规约之间无法进行比较。如下图所示:
2.Diagramming specifications
用图的形式来表示规约
图中的点代表一个方法的具体实现。一个规约定义了一个所有可能实现的区域。某个具体实现,若满足规约,则落在其范围内;否则,在其之外。
程序员可以在规约的范围内自由选择实现方式;客户端无需了解具体使用了哪个实现。
更强的规约表述为更小的区域
- 更强的后置条件意味着实现的自由度更低了➔在图中的面积更小。
- 更弱的前置条件意味着实现时要处理更多的可能输入,实现的自由度低了➔面积更小。
3.Designing good specifications
一个好的“方法”设计,并不是代码写的多么好,而是对该方法的规约设计的如何。
- 一方面:用户用着舒服
- 另一方面:开发者编着舒服
一个好的规约需要满足的条件
- 1.内聚的。
规约描述的功能应单一、简单、易理解。 - 2.信息丰富的。
不能让客户端产生理解的歧义。 - 3.强度足够强的。
太弱的规约,用户不放心、不敢用 (因为没有给出足够的承诺)。
开发者应尽可能考虑各种特殊情况,在后置条件给出处理措施。 - 4.强度足够弱的。
太强的规约,在很多特殊情况下难以达到,给开发者增加了实现的难度(用户当然非常高兴)。 - 5.使用抽象类型。
在规约里使用抽象类型,可以给方法的实现体与客户端更大的自由度。 - 6.正确处理好前置条件和后置条件的使用。
是否使用前置条件取决于(1) check的代价;(2) 方法的使用范围
–如果只在类的内部使用该方法(private),那么可以使用前置条件(方法内部不需要判断输入是否满足,认为client会保证前置条件),在使用该方法的各个位置进行check——责任交给内部client;
–如果在其他地方使用该方法(public),那么可以不使用/放松前置条件(在方法内部检查输入是否满足),若client端不满足则方法抛出异常。