前言
在最近的一个月的课程中,笔者对于规格化编程进行了深入的学习。运用面向对象抽象思想对编写的程序进行过程抽象、异常处理、数据抽象、类的层次规格与迭代等等规格设计,使得程序结构化程度提高,具有更好的可维护性和复用性。本文通过分析并总结近三次作业规格设计情况,分享我在规格化程序设计上的见解与体会。
作业规格错误汇总
- 规格错误详细信息:
编号 | 类型 | 所在类 | 方法名称 | 代码行数 | 详细 |
---|---|---|---|---|---|
1 | 前置条件不规范 | InputHandler | parseOrderReq | 5 | 未使用形式语言 |
2 | 前置条件不规范 | InputHandler | parseRoadChangeReq | 5 | 未使用形式语言 |
3 | 前置条件不规范 | InputHandler | parseSearchTaxiReq | 4 | 未使用形式语言 |
4 | 前置条件不规范 | InputHandler | parseSearchStateReq | 7 | 未使用形式语言 |
5 | 后置条件为实现算法 | LoadFileUnit | setFlow | 23 | 未使用形式语言表示调用者看到的变化 |
6 | 后置条件为实现算法 | LoadFileUnit | setTaxi | 34 | 未使用形式语言表示调用者看到的变化 |
7 | 后置条件为实现算法 | LoadFileUnit | setReq | 31 | 未使用形式语言表示调用者看到的变化 |
8 | 后置条件为实现算法 | Map | setTaxi | 1 | 未使用形式语言表示调用者看到的变化 |
9 | 后置条件为实现算法 | ReqHandler | markServableTaxi | 4 | 未使用形式语言表示调用者看到的变化 |
10 | 后置条件为实现算法 | ReqHandler | assignTaxi | 18 | 未使用形式语言表示调用者看到的变化 |
11 | 后置条件为实现算法 | ReqHandler | run | 12 | 未使用形式语言表示调用者看到的变化 |
12 | 后置条件逻辑错误 | TaxiAction | run | 85,91,931 | 后置条件未描述方法所有的影响 |
- 数据汇总:
类型 | 总计 | 平均代码行数 | 最大代码行数 |
---|---|---|---|
前置条件不规范 | 4 | 5 | 7 |
后置条件为实现算法 | 6 | 18.5 | 34 |
后置条件逻辑错误 | 1 | 89.7 | 93 |
总计 | 11 | 33.6 | 93 |
规格错误分析
第九次作业
在第九次作业中,根据需求需要加入道路开关功能以及增加用于初始化系统的文件读取指令,并且为所有方法补充过程规格。在这次作业中由于绝大部分代码都是来自上一次作业,许多方法在实现前仅考虑了SOLID原则2而未考虑规格设计,每个方法的规格都是在实现后补充上去的(这在顺序上是倒置的)。有一小部分的方法在功能与产生的作用比较繁杂,难以用形式语言进行描述,最后只能用自然语言作为替换。但对于这些方法使用自然语言也不太好说清楚其后置条件,最后导致了后置条件为算法是实现的过程的错误。此外,在使用形式语言描述的时候,对于一些方法的处理边界的描述上存在一些缺陷。
在完成这次作业前,笔者学习了使用异常抛出来区分正常情况与异常情况。但由于在此之前设计代码时未考虑使用反射机制来处理异常情况,而使用形如null
等变量来当作异常情况的返回。如果要加入这一功能需要重构大部分方法中的讨论情况。由于时间关系,在这次作业中只在新加入的部分使用了异常处理机制。因而在旧代码的部分的规格设计中对于异常情况没有显示表示,而是返回一些无意义的数据(从调用者的角度来看是不友好的)。
在测试别人的程序时发现的规格问题基本与我的相似,基本是方法的冗杂致使规格的后置条件为实现过程或者后置条件有遗漏。
第十次作业
在这次作业中,根据需求仅需增加路口的红绿灯功能,工作量较少(虽然计算时间和流量比较困难),因此笔者将之前写的代码根据过程抽象原则进行了优化。对功能较多的方法进行了重构,将其功能进行了分割,分散至不同类或者方法当中。在此之后程序中绝大多数的方法都能够用较为简洁的形式语言描述。因此在这次作业的测试阶段,对方未报告规格错误。
此外,需求要求补充每个类的类规格、抽象函数以及对象有效性验证方法3。由于最初设计时着重考虑了SOLID原则,程序中每个类的功能是比较明确的。因而增加类规格的困难不大,在互测阶段也没被报告错误。
第十一次作业
在这次作业中,根据需求需要对出租车种类进行扩充,增加一种能满足新需求(不赘述)的出租车,以及实现迭代输出服务记录的功能。在继承前一种出租车的同时要着重考虑里氏替换原则,实现有效的子类设计,并且还需附有有效性论证。具体体现在子类方法与父类方法的前置条件与后置条件的空间上。在我的程序中,子类重写父类方法过程时仅对后置条件进行了扩充,使其能满足里氏替换原则。因而在互测阶段未被报告错误。在我测试的程序中,设计者将所有父类的方法复制到类子类中,并对细节进行了改动。虽然这种做法有很多赘余,但通过论证也未发现问题。
规格优化
- 后置条件为实现过程。
优化前:
public synchronized void setTaxi(int index, int locaX, int locaY, TaxiState state)
/**
* @REQUIRES: 0<=index<100;0<=locaX<=79;0<=locaY<=79;
* @MODIFIES: gui
* @EFFECTS: (在GUI中将编号为index的出租车设置为state状态,移至(locaX,locaY)位置);
* @THREAD_REQUIRES:\locked(this);
*/
优化后:
public synchronized void setTaxi(int index, int locaX, int locaY, TaxiState state)
/**
* @REQUIRES: (0<=index<100);(0<=locaX<=79);(0<=locaY<=79);
* @MODIFIES: gui
* @EFFECTS: (gui.taxi[index].locaX==locaX)&&(gui.taxi[index].locaY==locaY)&&(gui.taxi[index].state==state);
* @THREAD_REQUIRES: \locked(this);
*/
- 前置条件可以扩展,后置条件可以对异常进行处理。
优化前:
public RoadChangeReq parseRoadChangeReq(String input, long time){
/**
* @REQUIRES: input符合道路更改请求格式;
* @EFFECTS: \result==解析后的道路更改请求对象;
*/
Matcher roadReqMatcher = this.roadReqPattern.matcher(input);
if(roadReqMatcher.matches()){
return new RoadChangeReq(roadReqMatcher.group(1),
roadReqMatcher.group(2),
roadReqMatcher.group(3),
roadReqMatcher.group(4),
roadReqMatcher.group(5),
time);
}
else
return null;
}
优化后:
public RoadChangeReq parseRoadChangeReq(String input, long time) throws Exception{
/**
* @REQUIRES: input!=null;
* @EFFECTS: (!roadReqPattern.match(input))==>(\result==解析后的道路更改请求对象);
* (!roadReqPattern.match(input))==>exception_behavior(Exception);
*/
Matcher roadReqMatcher = this.roadReqPattern.matcher(input);
if(roadReqMatcher.matches()){
return new RoadChangeReq(roadReqMatcher.group(1),
roadReqMatcher.group(2),
roadReqMatcher.group(3),
roadReqMatcher.group(4),
roadReqMatcher.group(5),
time);
}
throw new Exception("不符合道路更改请求格式");// 可以自定义异常。
}
- 后置条件应为具体现象。
优化前:
public void setTaxi(int taxiNo, TaxiState state, int credit, int locaX, int locaY){
/**
* @REQUIRES: 0<=taxiNo<100;credit>=0;0<=locaX<79;0<=locaY<79;
* @MODIFIES: this.set,gui
* @EFFECTS: 更新出租车位置。
*/
this.set[taxiNo].setTaxiInfo(state, credit, locaX, locaY);
}
优化后:
public void setTaxi(int taxiNo, TaxiState state, int credit, int locaX, int locaY){
/**
* @REQUIRES: 0<=taxiNo<100;credit>=0;0<=locaX<79;0<=locaY<79;
* @MODIFIES: this.set,gui
* @EFFECTS: this.set[taxiNo].locaX==locaX;
* this.set[taxiNo].locaY==locaY;
* this.set[taxiNo].credit==credit;
* this.set[taxiNo].state==state;
* gui.taxi[taxiNo].locaX==locaX;
* gui.taxi[taxiNo].locaY==locaY;
*/
this.set[taxiNo].setTaxiInfo(state, credit, locaX, locaY);
}
- 后置条件应为具体现象。
优化前:
class InputListener implements Runnable {
private ReqBuffer reqBuffer;
private TaxiInfoSet taxis;
private Map map;
......
@Override
public void run() {
/**
* @MODIFIES: this.map,this.reqBuffer,this.taxis,System.out
* @EFFECTS: (监控到乘客请求)==>(解析并将请求对象加入reqBuffer);
* (监控到道路更改请求)==>(解析并更改map中的道路);
* (监控到出租车搜索请求)==>(解析并System.out相应信息);
* (监控到出租车状态搜索请求)==>(解析并System.out相应信息);
*/
优化后:
class InputListener implements Runnable {
private ReqBuffer reqBuffer;
private TaxiInfoSet taxis;
private Map map;
/**
(省略正则表达式)
public static String REQREGEX;
public static String ROADREQREGEX;
public static String SEARCHTAXIREGEX;
public static String SEARCHSTATEREGEX;
*/
......
@Override
public void run() {
/**
* @MODIFIES: this.map,this.reqBuffer,System.out
* @EFFECTS: (System.in.match(REQREGEX))
* ==>(this.reqBuffer.contains(new Request(System.in)));
* (System.in.match(ROADREQREGEX))
* ==>(this.map.road.status==System.in.status);
* (System.in.match(SEARCHTAXIREGEX))
* ==>(System.out==this.taxis[System.in.taxiNo].info);
* (System.in.match(SEARCHSTATEREGEX))
* ==>(\all Taxi taxi;
* this.taxis.contains(taxi)
* &&taxi.state==System.in.taxiState;
* System.out.contains(taxi.info));
*/
- 后置条件可以写为形式语言。
优化前:
private void markServableTaxi(ReqWin reqWin)
/**
* @ REQUIRES: reqWin!=null;
* @ MODIFIES: rewWin;
* @ EFFECTS: 将符合抢单条件的出租车加入至reqWin的taxiSet中;
*/
优化后:
private void markServableTaxi(ReqWin reqWin)
/**
* @ REQUIRES: reqWin!=null;
* @ MODIFIES: rewWin;
* @ EFFECTS: (\all TaxiInfo taxi;
* this.taxiSet.contains(taxi)
* &&taxi.isin(reqWin.district);
* reqWin.taxiSet.contains(taxi));
*/
作业功能错误汇总
第九次作业
在这次作业中笔者被报告了以下三个错误:
- 错误现象:当读取的文件中有多条乘客请求时程序会死锁。
- 错误分析:在程序的设计中,初始化乘客请求是通过系统启动前就将文件中的请求解析并加入至请求缓存区中;在此之后调度器将缓存区中的请求取出再按调度策略分配服务的出租车。在最初的程序中,这部分的代码如下:
public class SysMain {
public static void main(String[] argv) {
......
LoadFileUnit loadFileUnit = new LoadFileUnit(); // 构造文件读取器
loadFileUnit.checkLoad(); // 检查指令合法性
......
ReqBuffer reqBuffer = new ReqBuffer(); // 构造请求缓存区
......
......
// 构造调度器
ReqHandler reqHandler = new ReqHandler(reqBuffer, taxiSet, map);
loadFileUnit.setReq(reqBuffer, map); // 逐个加入请求
new Thread(reqHandler).start(); // 启动调度线程
......
}
在此之中请求缓存区的容量为1。进而很明显当请求数大于1时由于此时调度线程还未启动,没有线程能够消耗缓存区的请求,导致了主线程一直等待缓存区为空。而这种情况不会发生,最后导致了死锁。
- 错误改正:将调度线程的启动时机提前即可。
----------分割线----------
- 错误现象:多辆出租车同时计算路径时有一定几率报出
地图不连通,程序退出
并结束程序。 - 错误分析:在出租车计算路径是需要使用公用的矩阵存储广度遍历的结果,程序中共享资源的互斥存在缺陷导致计算进入了错误的步骤,得出了地图不连通的结果。笔者在原程序中通过对课程组提供的路径计算方法增加synchronized关键词加锁实现资源的互斥,但由于课程组提供的GUI包中大多数类的封装问题,导致该互斥操作不完善,最终导致该错误发生(
虽然我测了好久也没出现这种情况)。 - 错误改正:构造Map类(线程安全类)包装地图操作。
----------分割线----------
- 错误现象:未排除相同请求。
- 错误分析:相关代码如下:
// 请求窗口集合类的插入方法
public void append(Object req) {
Request newReq = (Request)req;
System.out.println(newReq);
for(int i= 0; i < this.length; i++) {
int[] loca = this.list[i].getReq().getLoca();
int[] aim = this.list[i].getReq().getAim();
long time = this.list[i].getReq().getMakeTime();
if(newReq.getLoca()[0] == loca[0]
&& newReq.getLoca()[1] == loca[1]
&& newReq.getAim()[0] == aim[0]
&& newReq.getAim()[1] == aim[1]
&& newReq.getMakeTime() == time) {
System.out.println("相同请求 : " + newReq);
// 标记 //
}
}
// 在末尾插入。
this.list = Arrays.copyOf(this.list, ++this.length);
this.list[this.length - 1] = new ReqWin(newReq);
}
在对集合中已有的请求进行遍历并判断为相同请求后为中止方法,使相同请求也能被插入至请求队列。
- 错误更改:在标记处增加
return;
。
----------分割线----------
在第十与十一次作业时笔者未被报告程序错误。
在测试别人程序的过程中,笔者测试的这三位同学的程序都无法正常地运行多条乘客请求。在第九次作业时,被测试的程序在运行多条请求时会出现请求间信息错位的现象,初步认定是请求共享部分的互斥工作存在缺陷。在第十次作业时,被测试的程序在运行3至6条程序时会异常地停止运行,无任何反应,但不会崩溃退出。在阅读代码后大致由于路径计算时耗时过多,以致多辆出租车同时计算长距离请求时延迟较大。当运行超过7条请求时,程序会有崩溃的可能,几率随请求数的增加而增加,崩溃的原因是堆栈溢出。在第十一次作业的时候,被测试的程序在运行多条请求时会出现严重的延迟现象。由于这位同学的代码的可读性实在太差,笔者没能找出导致错误的代码。
思考与体会
在经过这三次作业之后,我对于规格化程序设计的重要性有了亲身体会。在编写面向对象程序前就应当对程序中的数据和处理过程进行抽象,定义出对数据的操作以及数据管理的方式,归纳出程序中需要进行的行为,限定各个操作的边界以及用户可见的内容。再结合上个阶段学的面向对象程序设计原则,对于程序中的类设计做出相应限定,使得编写的类具有更好的延展性、可维护性与鲁棒性。
经过总结,对于程序中方法的设计过程大致分为以下几步:
1. 明确方法存在的意义。
2. 明确方法结果正确的判定条件。
3. 明确方法对调用者提出的条件,以保证结果正确。
4. 明确方法执行期间修改的数据。
5. 按照要求的方式整理前置条件、修改数据、后置条件。
经过了短暂的一个月的实践,笔者虽对这些思想有了不少的体会,但还有待更多的实践深化。