OO第三次作业 Care for a ride?
OO第三次作业 Care for a ride?
Care for a ride?是《海上钢琴师》中我最喜欢的一句台词,在一个风雨交加的夜晚,1900和他的小号手朋友一起,松开钢琴的轮锁,让钢琴在甲板上随意滑行,所以这四次出租车作业,我一直将“Care for a ride?”作为我的README题目。
规格设计发展史
为什么要写规格?
程序,是给机器看的,规格,是给人看的。为什么要在给机器看的东西上写给人看的东西呢,是因为工程的需要。工程是复杂的,长期的,多人合作的。因此,不仅仅你的代码需要实现功能,你也需要维护代码的可读性,你也需要辅助自己和他人,了解每个类每个方法的功能,这样他人才能维护你的代码,你的代码才不是“日抛型”代码,而可以持续的应用于生产活动当中。
并且,规格化设计也将辅助你从“意识流”设计,想什么写什么,变到标准化的设计。“To err is human.”不为自己定些游戏规则,人肯定会犯错的(虽然定了法律也有人犯罪)。
最初的程序设计是直接面向机器、阅读困难,后来出现了面向过程的设计思想。对于面向过程设计思想的变革,goto语句成为讨论的热点。Dijkstra于1968年发表著名的《GOTO有害论》,引起了广泛的关注,结构化的设计思想也在此时应运而生。结构化设计思想,是指的是通过“自顶向下、逐步细化、模块化”的方法进行程序设计。这种设计思想旨在控制各个模块的程序复杂度,从而保证整体工程的正确性。面向过程的设计存在着扩展性不好的弊病,由此,产生了面向对象的设计模式。程序设计的模块化体现的更为明显,各个部分更加独立,相应地,各个模块之间的交互也更加重要。
无论是面向过程还是面向对象,只要存在模块之间的交互,就必然存在着规格规范的问题。就个人感觉来说,规格是说明了模块的接口,说明了模块的IO要求。所以,规格化是模块化的发展的必然需求。
规格bug及其分析
主要有两个bug,一个是JSF规格书写,一个是repOK方法书写:
JSF 规格书写bug
Guideline1:
前置条件必须是布尔表达式。
在后置条件描述中,每个@EFFECTS后跟着的都应该是一个可判定的布尔表达式,且应该只使用集合论和一阶逻辑谓词来表示的布尔表达式,从而有效描述方法的设计约束。虽然也可以采用自然语言描述规格,但会不可避免的引入二义性。
备注:被 JSFTool 测试除了了问题。
C:\oo\testee\src\oo11\InputHandler.java:47: 警告 - oo11.InputHandler.run() : "System.in"
Warning 95 : @requires (pre-condition) must be a boolean expression!
(Maybe this is a natrual language boolean expression...)
这个地方使用了成员变量描述 requires ,不是逻辑表达式、也不是自然语言,java 也没有 c++ 一样的对对象的非 null 判断,因而错误。
C:\oo\testee\src\oo11\TrafficLight.java:36: 警告 -
oo11.TrafficLight() : "total_num = 0"
Warning 4 : @effects (post-condition) must be a boolean expression!
(Maybe this is a natrual language boolean expression...)
C:\oo\testee\src\oo11\TrafficLight.java:36: 警告 -
oo11.TrafficLight() : "
pos = new Position[MAXN]"
Warning 5 : @effects (post-condition) must be a boolean expression!
(Maybe this is a natrual language boolean expression...)
以及很多构造函数、setter 的 effects,直接使用了赋值,没有使用逻辑表达式、也不是自然语言,仅仅只描述了算法。
repOK 方法书写bug
针对构造方法,初始状态repOK为真。
备注
package oo11;
public class TestMain {
public static void main(String[] args) {
Car car = new Car(40);
try {
System.out.println(car.repOK());
} catch(Throwable e) {
e.printStackTrace();
}
TraceableCar carB = new TraceableCar(5);
try {
System.out.println(carB.repOK());
} catch(Throwable e) {
e.printStackTrace();
}
Request req = new Request();
try {
System.out.println(req.repOK());
} catch(Throwable e) {
e.printStackTrace();
}
}
}
这是测试两个 Car 的 repOK 和 Request 无参数构造的 repOK 的主程序,repOK 没有返回 true,原因是 from 数组构造后新建了数组,但是里面的值基本都是 null,但是 repOK 中调用了 fromi.isValid(),导致 nullpointer。
再有是 Position 的带参构造,没有检查坐标的范围。
测试用代码:
package oo11;
public class TestMain {
public static void main(String[] args) {
Position pA = new Position(80, 80);
System.out.println(pA.repOK());
Position pB = new Position(-1, -1);
System.out.println(pB.repOK());
Request reqA = new Request("CR", pA, pB);
System.out.println(reqA.repOK());
}
}
还有很多像是,Information 和 Main 的 repOK 没写(return true;)之类的问题。
不好的条件写法及其改进
修改前:
public void addNearby(Long now_time, Request now_req) {
/**
* @REQUIRES: \exists cars[i].getStatus == Car.CarStatus.waiting
* @MODIFIES: now_req
* @EFFECTS: 将符合抢单条件的车辆加入到now_req的备选集合中
*/
修改后:
public void addNearby(Long now_time, Request now_req) {
/**
* @REQUIRES: \exists cars[i].getStatus == Car.CarStatus.waiting
* @MODIFIES: now_req
* @EFFECTS: \all Integer i, req.set.cotains(cars[i]) ==> (cars[i] can answer req)
*/
修改前:
public ArrayList<Integer> getCarAtStatus(Car.CarStatus status) {
/**
* @EFFECTS: 返回处于status状态的Car的下标集合
*/
修改后:
public ArrayList<Integer> getCarAtStatus(Car.CarStatus status) {
/**
* @EFFECTS: \result = ArrayList<Integer> set, \all Integer i, set.contains(cars[i]) ==> (cars[i].status == status)
*/
修改前:
public void loadRequest(String file_path) {
/**
* @REQUEIRES: file_path!=null
* @MODIFIES: Q
* @EFFECTS: 读取Loadfile中的Request,并加入请求队列中。
*/
修改后:
public void loadRequest(String file_path) {
/**
* @REQUEIRES: file_path!=null
* @MODIFIES: Q
* @EFFECTS: \all Request req, Q.contains(req) ==> (Loadfile.contains(req) )
*/
修改前:
public void openRoad(Position now_src, Position now_dst) {
/**
* @REQUEIRES: (now_src.getX() >= 0 && now_src.getX() <80) &&
* (now_dst.getX() >= 0 && now_dst.getY() <80);
* @MODIFIES: dig;
* @EFFECTS: 打开(now_src,now_dst)对应的路;
*/
修改后:
public void openRoad(Position now_src, Position now_dst) {
/**
* @REQUEIRES: (now_src.getX() >= 0 && now_src.getX() <80) &&
* (now_dst.getX() >= 0 && now_dst.getY() <80);
* @MODIFIES: dig;
* @EFFECTS: \all Integer i,j; (dig[i][j] == 1) ==> (dig[i][j] in (now_src, now_dst))
*/
修改前:
public synchronized void remove(Request now) {
/**
* @REQUEIRES: (queue != null);
* @MODIFIES: None;
* @EFFECTS: 从queue中删除当前请求。
* @THREAD_REQUIRES: \locked(this);
* @THREAD_EFFECTS: \locked();
*/
修改后:
public synchronized void remove(Request now) {
/**
* @REQUEIRES: (queue != null);
* @MODIFIES: None;
* @EFFECTS: (\forall Request req; req != now; queue.contains(req) == \old(queue).contains(req));
* @THREAD_REQUIRES: \locked(this);
* @THREAD_EFFECTS: \locked();
*/
功能bug及其与规格的联系
第11次指导书中:
b) 可追踪出租车扩展普通出租车的路径选择方法,使得可追踪出租车能够行走关闭的道路(普通出租车则不可以)。注:关闭的道路的车流为0,且保持不变。
测试检查对于关闭的道路,可追踪出租车是否会将被关闭的道路的流量记为0。
结果输出算得的最小流量为 2,并下一步走到流量更大的边 (0,0) -> (1,0)。但根据指导书,被关闭的边 (0,0) -> (0,1) 流量更小、为0。bug 源于我使用了GUI中的流量,但是我忘记修改了本次新GUI的代码,导致对于被关闭的道路 getFlow 时,得到的流量不一定为 0:
public static int GetFlow(int x1, int y1, int x2, int y2) {// 查询流量信息
synchronized (guigv.memflowmap) {
return guigv.memflowmap.get(Key(x1, y1, x2, y2)) == null ? 0 : guigv.memflowmap.get(Key(x1, y1, x2, y2));
}
}
其与规格之间的联系,在于我没有明确规格中的流量是怎么来的,而忘记了使用新GUI时造成的流量维护错误。
设计规格和撰写规格的基本思路和体会
最初我真的觉得写规格一点用也没有,因为据我所知,大家都是在写完代码之后写规格·····
这样的规格意义就不大了,因为规格是用来设计的,而不是用来debug或者完成任务的,否则那还不如直接写两句注释。
直到在OO第六次上机当中,我们的任务是完成一个银行账户管理系统,而在这个系统中,有一个类的方法全部都是空的,只有规格,但是规格很完备(老师写的和同学写的是不一样)。
规格用布尔表达式式写成,我发现基本上根据规格写出代码是一件非常容易的事情,而且,写明前置和后置条件,应该可以避免很多由于疏忽造成的bug,想起了高中时写函数,老师逼着你写定义域一样,不在定义域内的计算都是不合理的,不明确前置后置条件的方法,都是耍流氓的方法。