软件构造复习第五章 设计规约

Functions & methods in programming languages

方法的参数规约

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

注意:参数类型是否匹配,在静态类型检查阶段完成(static checking)
方法的返回值规约

public static TYPE Name(){
	STATEMENTS;
	return EXPRESSIONl;
}
//void 意味着该方法不返回数据,也就是 no type

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

方法(Method)

  • 方法是程序的积木
  • 可以被独立开发、测试、复用
  • 使用方法的客户端,无需了解方法内部具体如何工作—抽象Abstraction

下面的代码是一个完整的方法示例

public class Hailstone{
	/**
	* Compute a hailstone sequence.
	* @param n Starting number for sequence. Assumes n > 0.
	* @return hailstone sequence starting with n and ending with 1.
	*/
	public static List<Integer> hailstoneSequenece(int n){
		List<Integer> list = new ArrayList<Integer>();
		while(n != 1){
			list.add(n);
			if(n % 2 == 0){
				n = n / 2;
			}else{
				n = 3 * n + 1;
			}
		}
		list.add(n);
		return list;
	}
}

注意:上述方法中:
2-7行 → \rightarrow 方法的规约Spec
8-18行 → \rightarrow 方法的实现体Implementation

Specification: Programming for communication

Documenting in programming

代码本身蕴含着设计决策如变量的数据类型定义,final等关键字的使用等等。但是这样的假定记录(Documenting Assumptions)远远不够。
Documenting Assumptions的目的是为了

  • 提醒自己
  • 让别人能看懂自己的代码

所以,程序在编写时应该满足两个目标

  1. 代码中蕴含的“设计决策”:给编译器读 Communicating with the computer
  2. 注释形式的“设计决策”:给自己和别人读Communicating with other people

Specification and Contract (of a method)

规约Specification:是团队合作的关键所在,没有规约,没法写程序;即使写出来,也不知道对错。
规约扮演着合同/契约Contract的角色,是程序与客户端之间达成的一致。
规约给“供需双方”都确定了责任,在调用的时候双方都要遵守
以下是一些需要规约存在的原因
现实

  • 很多bug来自于双方之间的误解、
  • 不写下来,那么不同开发者的理解就可能不同
  • 没有规约,难以定位错误

优势

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

规约示例Java class BigInteger 中的add()
在这里插入图片描述
规约还可以:

  • 隔离“变化”,无需通知客户端
  • 提高代码效率
  • 扮演“防火墙”角色
    在这里插入图片描述
    规约实际上就是Agreement between an object and its user
  • 输入/输出的数据类型
  • 功能和正确性
  • 性能
  • 只讲能做什么,不讲怎么实现

Behavioral equivalence行为等价性

行为等价性实际上就是两个函数/方法是否可相互替换(站在客户端的视角看行为等价性)

static int findFirst(int[] arr, int val){
	for(int i = 0; i < arr.length; i++){
		if(arr[i] == val) return i;
	}
	return arr.length;
}
static int findLast(int[] arr, int val){
	for(int i = arr.length -1; i >= arr.length; i--){
		if(arr[i] == val) return i;
	}
	return -1;
}

这两个函数是否等价?
虽然行为不同,但对用户来说是否等价?
这时候,需要依据规约来判断行为是否等价
如果规约为

static int find(int[] arr, int val)
	requires: 	val occurs exactly once in arr
	effects:	returns index i such that arr[i]=val

这两个函数符合这个规约,故它们 等价
简单的总结即:

  • 单纯的看实现代码,并不足以判定不同的implmentation是否是“行为等价的”
  • 需要根据代码的spec(开发者与client之间形成的contract)判定行为等价性
  • 在编写代码之前,需要弄清楚spec如何协商形成、如何撰写

Specification structure: pre-condition and post-condition

pre-condition 前置条件:对客户端的约束,在使用方法时必须满足的条件
post-condition后置条件:对开发者的约束,方法结束时必须满足的条件
契约Contract:如果前置条件满足了,后置条件必须满足。前置条件不满足,则方法可做任何事情

例如上一部分中的规约

static int find(int[] arr, int val)
	requires: 	val occurs exactly once in arr
	effects:	returns index i such that arr[i]=val
  • if arr is empty, return 0
  • if arr is empty, throw an exception
  • if val occurs twice in arr, throw an exception
  • if val occurs twice in arr, set all the values in arr to 0, then throw an exception
  • if arr is not empty but val doesn’t occur, pick an index at random, set it to val, and return that index

由于客户端违反了spec,所以上述的处理都是可行的。但是最好的办法还是让bug easier to find and fix by failing fast,比如抛出异常的处理方法。

Java中的规约包括两部分

  1. 静态类型声明是一种规约,可据此进行静态类型检查static checking
  2. 方法前的注释也是一种规约,但需人工判定其是否满足

Parameters(参数) → \rightarrow @param
Results(结果) → \rightarrow @return and @throws
将前置条件放入@param
后置条件放入@return and @throws
现在可以修改之前的规约

static int find(int[] arr, int val)
	requires: 	val occurs exactly once in arr
	effects:	returns index i such that arr[i]=val
//修改后
/**
* Find a value in an array
* @param arr array to search,requires that val occurs exactly once in arr
* @param val value to search for
* @return index i such that arr[i] = val
*/
static int find(int[] arr, int val)

规约中可以提到参数,返回值等信息,但是不应该提到方法内的变量或者类中的私有属性

针对mutating methods修改器方法,需要注意
除非在后置条件里声明过,否则方法内部不应该改变输入参数
应尽量遵循此规则,尽量不设计mutating的spec,否则就容易引发bugs。程序员之间应达成的默契:除非spec必须如此,否则不应修改输入参数

尽量避免使用mutable的对象的原因:

  1. 程序中可能有很多变量指向同一个可变对象(别名 aliases
  2. 无法强迫类的实现体和客户端不保存可变变量的别名

Testing and verifying specifications

通过白盒测试来测试程序是否依据规约实现
测试用例不应该针对具体的实现,而是遵守规约即可。
在这里插入图片描述

Designing specifications设计规约

Classifying specifications

比较规约的强度:
如果规约S2>=S1则意味着:

  1. 前置条件更
  2. 后置条件更

这样就可以用S2代替S1
简单来说就是spec变强需要更放松的前置条件+更严格的后置条件
例如:

//Original spec:
static int findExactlyOne(int[] a, int val)
	requires:	val occurs exactly once in a
	effects:	returns index i such that a[i] = val
//A stronger spec:
static int findOneOrMoreAnyIndex(int[] a, int val)
	requires:	val occurs at least once in a
	effects:	returns index i such that a[i] = val
//A much stronger spec
static int findOneOrMoreFirstIndex(int[] a, int val)
	requires:	val occurs at least once in a
	effects:	returns lowest index i such that a[i] = val

下面是另外一种情况

static int findCanBeMissing(int[] a, int val)
	requires:	nothing
	effects:	returns index i such that a[i] = val, or -1 if no such i

当满足了其他规约的前置条件,后置条件

  1. 和findExactlyOne相比,无差别
  2. 和findOneOrMoreAnyIndex相比,无差别
  3. 在满足findOneOrMoreFirstIndex的前置条件的情况下,该后置条件弱化了:没有返回lowest index → \rightarrow 不可比较

越强的规约,意味着implementor的自由度和责任越重,而client的
责任越轻。

Diagramming specifications

例如
在这里插入图片描述

  • 每一个点代表方法的一种实现
  • 规约定义了空间中所有可能的实现所占的区域
  • 某个具体实现,若满足规约,则落在其范围内;否则,在其之外。
  • 程序员可以在规约的范围内自由选择实现方式
  • 客户端无需了解具体使用了哪个实现
  • 更强的规约,表达为更小的区域
    更强的后置条件意味着实现的自由度更低了 → \rightarrow 在图中的面积更小
    更弱的前置条件意味着实现时要处理更多的可能输入,实现的自由度低了 → \rightarrow 面积更小
    在这里插入图片描述
//Original spec:
static int findExactlyOne(int[] a, int val)
	requires:	val occurs exactly once in a
	effects:	returns index i such that a[i] = val
//A stronger spec:
static int findOneOrMoreAnyIndex(int[] a, int val)
	requires:	val occurs at least once in a
	effects:	returns index i such that a[i] = val
//A much stronger spec
static int findOneOrMoreFirstIndex(int[] a, int val)
	requires:	val occurs at least once in a
	effects:	returns lowest index i such that a[i] = val
//
static int findCanBeMissing(int[] a, int val)
	requires:	nothing
	effects:	returns index i such that a[i] = val, or -1 if no such i

针对这四个规约,可以得到
ExactlyOne < OneOrMore, AnyIndex < OneOrMore, FirstIndex
ExactlyOne < OneOrMore, AnyIndex < CanBeMissing
而OneOrMore, FirstIndex和CanBeMissing无法比较。故示意图如下
在这里插入图片描述
最外圈为ExactlyOne

Designing good specifications

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

  1. The specification should be coherent (内聚的)
    Spec描述的功能应单一、简单、易理解
  2. The specification should be strong enough(足够强)
    需要考虑特殊情况,太弱的spec,client不放心、不敢用 (因为没有给出足够的承诺)。开发者应尽可能考虑各种特殊情况,在post-condition给出处理措施。
  3. The specification should also be weak enough(不要过于强)
    太强的spec,在很多特殊情况下难以达到,给开发者增加了实现的难
    度(client当然非常高兴)。
  4. The specification should use abstract types(使用抽象)
    在规约里使用抽象类型,可以给方法的实现体与客户端更大的自由度,比如用List代替ArrayList,Map代替HashMap等。
  5. Precondition or postcondition?
    是否应该使用前置条件?在方法正式执行之前,是否要检查前置条件已被满足?
    不写Precondition,就要在代码内部check;若代价太大,在规约里加入precondition,把责任交给client。
    客户端不喜欢太强的precondition,不满足precondition的输入会导致失败。
    惯用做法是:不限定太强的precondition,而是在postcondition中抛出异常:输入不合法。尽可能在错误的根源处fail,避免其大规模扩散
    归纳而言,是否使用前置条件取决于(1) check的代价;(2) 方法的使用范围
    1 如果只在类的内部使用该方法(private),那么可以不使用前置条件,在使用该方法的各个位置进行check——责任交给内部client;
    2 如果在其他地方使用该方法(public),那么必须要使用前置条件,若client端不满足则方法抛出异常。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值