哈尔滨工业大学软件构造课程笔记第三章第二节

3.2 设计规约

1.编程语言中的函数和方法

[] NAME (TYPE NAME, TYPE NAME) {
      
STATEMENTS 
} 
To call: 
NAME(arg1, arg2);

参数类型是否匹配,在静态类型检查阶段完成

public static TYPE NAME() {
      
STATEMENTS; 
return EXPRESSION; 
}
void means “no type”

返回值类型是否匹配,也在静态类型检查阶段完成

“方法”是程序的“积木”,可以被独立开发、测试、复用使用“方法”的客户端,无需了解方法内部具体如何工作—“抽象”
哈尔滨工业大学软件构造课程笔记第三章第二节_第1张图片

2.说明Spec:通信编程

(1)编制文档

Java API文档:一个示例
▪类层次结构和实现的接口列表。
▪直接子类,并为一个接口,实现类。
▪课堂描述
▪构造函数总结
▪方法总结列出所有我们可以调用的方法
▪详细描述每个方法和构造函数
-方法签名:我们可以看到返回类型、方法名和参数。我们也看到例外。目前,这些通常意味着该方法可能会出现错误。
-完整的描述。-参数:方法参数的描述。
-以及方法返回内容的描述。

记录的假设
把一个变量的类型写下来是对它的一个假设:例如,这个变量总是指向一个整数。
代码中变
量的数据类型定义

声明变量final也是一种文档形式,声明变量在初始赋值后不会更改。
final关键字定义了设计决策:“不可改变”

代码本身就蕴含着你的设计决策,但是远远不够

编程进行交流沟通
为什么要写出“假设”?第一:自己记不住;第二:别人不懂。

编写程序时必须考虑两个目标:
代码中蕴含的“设计决策”:给编译器读
注释形式的“设计决策”:给自己和别人读

(2)(方法的)规范和合同

规格(或称合同)
没规约,没法分派任务,无法写程序;即使写出来,也不知道对错

规范作为契约:实现者负责会议合同,和一个客户端,使用该方法可以依靠合同

明确双方的责任
定义正确实现的含义

Spec给“供需双方”都确定了责任,在调用的时候双方都要遵守

为什么spec?
现实情况:
很多bug来自于双方之间的误解
不写下来,那么不同开发者的理解就可能不同
没有规约,难以定位错误

好处:
精确的规约,有助于区分责任
客户端无需阅读调用函数的代码,只需理解spec即可

规约可以隔离“变化”,无需通知客户端
规约也可以提高代码效率(E.g., 实现者不需要写代码确保输入的正确性,调用者的责任)
规约:扮演“防火墙”角色
解耦,不需了解具体实现
哈尔滨工业大学软件构造课程笔记第三章第二节_第2张图片方法和它的用户之间的协议
输入/输出的数据类型
功能和正确性
性能

只讲“能做什么”,不讲“怎么实现”
-接口(API),不是实现
哈尔滨工业大学软件构造课程笔记第三章第二节_第3张图片

(3)行为等价性

行为等价性
为了确定行为等价性,问题是我们是否可以用一个实现来代替另一个 (是否可相互替换)

对等的概念是在客户的眼中 (站在客户端视角看行为等价性)

根据规约判断是否行为等价

单纯的看实现代码,并不足以判定不同的implmentation是否是“行为等价的”

需要根据代码的spec(开发者与client之间形成的contract)判定行为等价性

在编写代码之前,需要弄清楚spec如何协商形成、如何撰写

(4)规范结构:前置条件和后置条件

规约结构
▪方法的规范由以下几个条款组成:
-前置条件,由关键词require表示
-后置条件,由关键字效果表示
-异常行为:如果违反了先决条件,它会做什么

前置条件:对客户端的约束,在使用方法时必须满足的条件

后置条件:对开发者的约束,方法结束时必须满足的条件

契约:如果前置条件满足了,后置条件必须满足

整个结构是一个逻辑暗示:如果在调用方法时前置条件成立,那么在方法完成时后置条件必须成立。

前置条件不满足,则方法可做任何事情。
哈尔滨工业大学软件构造课程笔记第三章第二节_第4张图片哈尔滨工业大学软件构造课程笔记第三章第二节_第5张图片java中的spec
静态类型声明是一种规约,可据此进行静态类型检查static checking。

方法前的注释也是一种规约,但需人工判定其是否满足

▪参数由@param子句描述,结果由@return和@throw子句描述。
▪在可能的情况下,在@param中加入前置条件,在@return和@throw中加入后置条件。

spec写什么
▪一个方法的说明书可以谈论方法的参数和返回值,但它不应该谈论方法的局部变量或方法类的私有字段。
-你应该考虑的实现隐形的规范的读者。
-在Java中,该方法的源代码通常对您的规范的读者是不可用的,因为Javadoc工具从您的代码中提取规范注释并将其呈现为HTML。
哈尔滨工业大学软件构造课程笔记第三章第二节_第6张图片变更方法的spec
除非在后置条件里声明过,否则方法内部不应该改变输入参数
应尽量遵循此规则,尽量不设计mutating的spec,否则就容易引发bugs。

程序员之间应达成的默契:除非spec必须如此,否则不应修改输入参数

尽量避免使用mutable的对象

可变对象使简单的契约变得复杂
程序中可能有很多变量指向同一个可变对象(别名)

无法强迫类的实现体和客户端不保存可变变量的“别名”

不能靠程序员的“道德”,要靠严格的“契约”

作为这种非本地契约现象的一个症状,考虑一下Java集合类,通常在客户端和类的实现者上用非常清楚的契约记录下来。

可变的对象减少了易变性
共享一个可变对象会使契约复杂化

关键就在于“不可变”,在规约里限定住

(5)*测试和验证规范

黑盒测试
黑盒测试:以独立于实现的方式检查被测试的程序是否遵循指定的规范。
▪你的测试用例不应该依赖于任何具体的实施行为。测试用例必须遵守契约,就像其他客户端一样。

3.设计spec

(1)分类spec

比较spec
规约的确定性
规约的陈述性
规约的强度

强规格vs弱规格
规约的强度S2>=S1
s2前置条件更弱
s2后置条件更强

就可以用S2替代S1

规则:
-弱化前提:减少对客户的要求不会让他们失望。
-加强后置条件,即作出更多承诺。

哈尔滨工业大学软件构造课程笔记第三章第二节_第7张图片
确定性与非确定性规格
确定性:当出现满足前提条件的状态时,结果是完全确定的。
-只有一个返回值和一个最终状态是可能的。
-没有有效的输入,但有一个以上的有效输出。

确定的规约:给定一个满足precondition的输入,其输出是唯一的、明确的
欠定的规约:同一个输入可以有多个输出

非确定的规约:同一个输入,多次执行时得到的输出可能不同
为了避免混淆,我们将把不确定的规范称为欠确定。

规范中的不确定性提供了由实现方在实现时做出的选择。
欠定的规约通常有确定的实现

说明性与操作性规范

操作式规约,例如:伪代码
声明式规约:没有内部实现的描述,只有“初-终”状态

声明式规约更有价值

内部实现的细节不在规约里呈现,放在代码实现体内部注释里呈现。

声明性规范
标准:最清晰的,对于客户和代码维护者来说。

(2)图解spec

图解spec
这个空间中的每个点表示一个方法实现
哈尔滨工业大学软件构造课程笔记第三章第二节_第8张图片规范定义了所有可能实现空间中的一个区域。
某个具体实现,
若满足规约,则落在其范围内;否则,在其之外。
程序员可以在规约的范围内自由选择实现方式
客户端无需了解具体使用了哪个实现

更强的规约,表达为更小的区域

更强的后置条件意味着实现的自由度更低了- ➔在图中的面积更小

更弱的前置条件意味着实现时要处理更多的可能输入,实现的自由度低了➔面积更小

(3)设计好spec

一个好的“方法”设计,并不是你的代码写的多么好,而是你对该方
法的spec设计的如何。
– 一方面:客户端用着舒服
– 另一方面:开发者编着舒服

规范应该是内聚的

Spec描述的功能应单一、简单、易理解

调用的结果应该是信息丰富的

不能让客户端产生理解的歧义

规范应该足够强大

规范应该在一般情况下为客户提供足够强大的保证——它需要满足客户的基本需求。
-在指定特殊情况时,我们必须格外小心,以确保它们不会破坏原本有用的方法。

太弱的spec,客户端不放心、不敢用 (因为没有给出足够的承诺)。
开发者应尽可能考虑各种特殊情况,在后置条件给出处理措施

规范也应该足够弱

太强的spec,在很多特殊情况下难以达到,给开发者增加了实现的难度

规范应该使用抽象数据类型

在规约里使用抽象类型,可以给方法的实现体与客户端更大的自由度

先决条件还是后置条件

是否应该使用前置条件?在方法正式执行之前,是否要检查前置条件已被满足?

客户端不喜欢太强的precondition,不满足precondition的输入会导致失败。

惯用做法是:不限定太强的precondition,而是在postcondition中抛出异常:输入不合法

尽可能在错误的根源处fail,避免其大规模扩散

衡量标准:检查参数合法性的代价多大?

归纳:是否使用前置条件取决于(1) check的代价;(2) 方法的使用范围
– 如果只在类的内部使用该方法(private),那么可以不使用前置条件,在使用该方法的各个位置进行check——责任交给内部client; – 如果在其他地方使用该方法(public),那么必须要使用前置条件,若client端不满足则方法抛出异常。

4.总结

规范充当过程的实现者与其客户机之间的关键防火墙。

它使单独的开发成为可能:客户端可以自由地编写使用过程的代码,而不需要查看它的源代码,而实现者可以自由地编写实现过程的代码,而不需要知道它将如何被使用,

防止bug
好的规范清楚地记录了客户端和实现者所依赖的相互假设。bug通常来自于接口上的分歧,而规范的存在降低了这种差异。
-在规范中使用机器检查过的语言特性,比如静态类型和异常,而不仅仅是人可读的注释,可以进一步减少bug。

容易理解的
简短、简单的规范比实现本身更容易理解,并使其他人不必阅读代码。

准备好改变
Specs在你的代码的不同部分之间建立契约,允许那些部分独立地改变,只要他们继续满足契约的要求

声明性规范在实践中最有用

先决条件(它削弱了规范)使客户的工作更加困难,但是如果应用得当,它们是软件设计人员的一个重要工具,允许实现者做出必要的假设。

防止bug。
-没有规范,即使是对我们程序的任何部分的最微小的改变都可能是推翻整个事情的多米诺骨牌。
-通过静态检查、仔细的推理、测试和代码审查,结构良好、一致的规范可以最大限度地减少误解,最大限度地提高我们编写正确代码的能力。

容易理解的
-编写良好的声明性规范意味着客户端不必阅读或理解代码。

准备好改变
适当的弱规范给实现者以自由,而适当的强规范给客户端以自由。
-我们甚至可以改变规格本身,而不需要重新访问每一个使用它们的地方,只要我们只是加强它们:削弱先决条件和加强后置条件。

你可能感兴趣的:(哈工大,软件构造,学习笔记,java)