基于度量的程序分析
第一次作业——傻瓜电梯
设计和策略:
傻瓜电梯按照“先来先服务”的原则进行调度,所以调度策略十分简单:调度器中维护一个请求队列,输入线程不断向其中添加指令,电梯线程则不断取出进行执行。在此生产者消费者的模型中,使用了wait和notify机制来避免轮询可能导致的CPU时间超标。
利用枚举变量表示电梯的上下方向和人(请求)的上下方向。
在这次设计中,调度器作为电梯的“大脑”,控制着电梯的行为,并且不断与请求队列进行交互取指令,电梯类只负责开关门和移动动作。
代码度量分析:
UML生成的类图如下:
使用DesigniteJava生成的代码分析报告:
method name | ev | iv | v |
---|---|---|---|
myelevator.Dispatcher.Dispatcher() | 1.0 | 1.0 | 1.0 |
myelevator.Dispatcher.elevatorPick() | 1.0 | 4.0 | 4.0 |
myelevator.Dispatcher.elevatorSend() | 1.0 | 4.0 | 4.0 |
myelevator.Dispatcher.getSingleReq() | 1.0 | 1.0 | 1.0 |
myelevator.Dispatcher.manMove(int,OnOff,int) | 1.0 | 3.0 | 3.0 |
myelevator.Dispatcher.run() | 3.0 | 4.0 | 5.0 |
Total | 8.0 | 17.0 | 18.0 |
Average | 1.3333333333333333 | 2.8333333333333335 | 3.0 |
通过代码分析可以看出,部分类内的方法过于臃肿,比如调度器中的run(), Send(), Pick(),在后来的两次作业中,我对这些较大的方法进行了拆分,分解为较小的方法以降低圈复杂度。
公测:
遇到了很奇怪的问题,交作业的时候中测全过,但是没进互测,之后发现强测分也100;最后助教说是中测有个点没过??
总结:
这是笔者第一次上手多线程设计程序,通过对典型模型“生产者-消费者”模型的理解,将更复杂的需求用该模型来实现。在这次作业中,我体会到并发程序运行的高效性,而且多线程设计是一种更贴近生活的设计模式,设想在现实生活中,向电梯发送请求是源源不断且时间不定的,不可能存在一部这样的电梯:等多有指令输入完成之后才开始运行。因此,这次电梯作业将我们的程序引入了生活之中,突破了以往“用程序解题”的局限,相信也给同学们带来了不错的成就体验。
不足之处:
程序结构依然存在高内聚的问题,再加上这是第一次设计真正的多线程工程代码,太多的时间花在了如何让程序没有bug上,于是代码风格还是较差。
debug的时候,仍然采用传统的IDEA debuger 进行调试,这种调试在单线程开发中确实非常实用,但是到了多线程中,由于JVM调度的不确定性,有时候很难定位bug的位置;所以笔者后来采用了学长封装的DebuggerHelper进行定位输出调试,效率提高了许多。
第二次作业——捎带电梯
设计思路:
电梯处于空闲状态的时候,从队列中取出一条电梯最快到达的指令先运行起来;
电梯处于运行状态时:每到一层楼,判断是否有乘客需要上下;如果有乘客需要下电梯,那么,需要在该层出电梯的乘客先下;如果有乘客需要上电梯,那么在关门sleep的时间之后,从请求队列中取出所有能在这一层上电梯的乘客。
调度器判断电梯是否能捎带的条件为:
direction == passenger.getDirection() && curFloor == passenger.getSrcFloor())
Project Name | Package Name | Type Name | MethodName | LOC | CC | PC |
---|---|---|---|---|---|---|
O5 | alselevator.lift | Elevator | Elevator | 5 | 1 | 0 |
O5 | alselevator.lift | Elevator | Elevator | 9 | 2 | 1 |
O5 | alselevator.lift | Elevator | Elevator | 9 | 2 | 1 |
O5 | alselevator.lift | Elevator | Elevator | 9 | 1 | 0 |
O5 | alselevator.lift | Elevator | Elevator | 9 | 1 | 0 |
O5 | alselevator.lift | Elevator | Elevator | 3 | 1 | 0 |
O5 | alselevator.lift | Elevator | Elevator | 3 | 1 | 0 |
O5 | alselevator.lift | Elevator | Elevator | 3 | 1 | 1 |
O5 | alselevator.lift | ElevatorConst | ElevatorConst | 0 | 1 | 1 |
O5 | alselevator.lift | ElevatorConst | ElevatorConst | 0 | 1 | 1 |
O5 | alselevator.lift | ElevatorConst | ElevatorConst | 0 | 1 | 0 |
O5 | alselevator.lift | ElevatorConst | ElevatorConst | 0 | 1 | 0 |
O5 | alselevator.lift | Floor | Floor | 4 | 1 | 1 |
O5 | alselevator.lift | Floor | Floor | 3 | 1 | 0 |
O5 | alselevator.lift | Floor | Floor | 3 | 1 | 0 |
O5 | alselevator.lift | Floor | Floor | 3 | 1 | 1 |
O5 | alselevator.lift | Floor | Floor | 3 | 1 | 1 |
O5 | alselevator.lift | LiftController | LiftController | 8 | 2 | 2 |
O5 | alselevator.lift | LiftController | LiftController | 6 | 1 | 1 |
O5 | alselevator.lift | LiftController | LiftController | 41 | 8 | 0 |
O5 | alselevator.lift | LiftController | LiftController | 8 | 2 | 0 |
O5 | alselevator.lift | LiftController | LiftController | 17 | 7 | 0 |
O5 | alselevator.lift | LiftController | LiftController | 17 | 7 | 1 |
O5 | alselevator.lift | LiftController | LiftController | 14 | 4 | 0 |
O5 | alselevator.lift | LiftController | LiftController | 8 | 3 | 1 |
O5 | alselevator.lift | LiftController | LiftController | 53 | 10 | 0 |
O5 | alselevator.lift | LiftController | LiftController | 30 | 7 | 1 |
O5 | alselevator.lift | LiftController | LiftController | 19 | 4 | 1 |
O5 | alselevator.lift | Passenger | Passenger | 12 | 2 | 1 |
O5 | alselevator.lift | Passenger | Passenger | 3 | 1 | 0 |
O5 | alselevator.lift | Passenger | Passenger | 3 | 1 | 0 |
O5 | alselevator.lift | Passenger | Passenger | 3 | 1 | 0 |
O5 | alselevator.lift | Passenger | Passenger | 3 | 1 | 0 |
O5 | alselevator.lift | Passenger | Passenger | 3 | 1 | 0 |
O5 | alselevator.lift | Passenger | Passenger | 3 | 1 | 1 |
O5 | alselevator.request | InputHandle | InputHandle | 25 | 3 | 1 |
O5 | alselevator.request | ReqList | ReqList | 4 | 1 | 0 |
O5 | alselevator.request | ReqList | ReqList | 4 | 1 | 1 |
O5 | alselevator.request | ReqList | ReqList | 14 | 3 | 0 |
O5 | alselevator.request | ReqList | ReqList | 3 | 1 | 0 |
O5 | alselevator.request | ReqList | ReqList | 4 | 1 | 1 |
O5 | alselevator.request | ReqList | ReqList | 3 | 1 | 0 |
O5 | alselevator.request | ReqList | ReqList | 3 | 1 | 1 |
O5 | alselevator.request | SingleRequest | SingleRequest | 8 | 2 | 1 |
O5 | alselevator.request | SingleRequest | SingleRequest | 3 | 1 | 0 |
O5 | alselevator.request | SingleRequest | SingleRequest | 3 | 1 | 0 |
O5 | alselevator.request | SingleRequest | SingleRequest | 3 | 1 | 0 |
O5 | alselevator.request | SingleRequest | SingleRequest | 3 | 1 | 0 |
O5 | alselevator.scheduler | Scheduler | Scheduler | 4 | 1 | 1 |
O5 | alselevator.scheduler | Scheduler | Scheduler | 30 | 8 | 2 |
O5 | alselevator.scheduler | Scheduler | Scheduler | 15 | 4 | 2 |
O5 | alselevator.scheduler | Scheduler | Scheduler | 3 | 1 | 0 |
O5 | alselevator.scheduler | Scheduler | Scheduler | 3 | 1 | 0 |
针对复杂度较高的电梯控制器进行了进一步的代码分析发现如下问题:
部分方法耦合度仍然较高,比如图表中标红的方法:getPassenger(),在这个方法中,我进行了大量的if-else判断,所以导致圈复杂度和模块设计复杂度都较高,但是基本复杂度较低;经过反思,我逐渐意识到“高内聚,低耦合”的重要性,我在此方法中进行了多于一件工作的处理,这不符合“开闭原则”,在之后优化代码的工作中,我设想把这个“大方法”拆分为几个较小的方法来协作完成一项任务。
贴出部分代码进一步分析:
在“添加乘客”这个方法中,我不仅完成了将乘客添加到电梯中,而且为电梯设置了方向,还重新调用了方法本身……这一系列的不合理设计,在当时竟然都没有察觉出来,但是听取老师课上对OO多线程设计模式的分析之后,我对方法设计有了新的认识。
public int addPassenger() {
ArrayList<Passenger> pasList = scheduler.getPasList();
if (elevator.getDirection() == Direction.Wait && !pasList.isEmpty()) {
passengers.addAll(pasList);
if (elevator.getCurFloor() == pasList.get(0).getSrcFloor()) {
if (elevator.getCurFloor() < pasList.get(0).getDstFloor()) {
elevator.setDirection(Direction.Up);
} else if (elevator.getCurFloor()
> pasList.get(0).getDstFloor()) {
elevator.setDirection(Direction.Down);
}
floors.add(new Floor(pasList.get(0).getDstFloor()));
floors.add(new Floor(pasList.get(0).getSrcFloor()));
} else if (elevator.getCurFloor() < pasList.get(0).getSrcFloor()) {
elevator.setDirection(Direction.Up);
floors.add(new Floor(pasList.get(0).getSrcFloor()));
floors.add(new Floor(pasList.get(0).getDstFloor()));
} else {
elevator.setDirection(Direction.Down);
floors.add(new Floor(pasList.get(0).getSrcFloor()));
floors.add(new Floor(pasList.get(0).getDstFloor()));
}
pasList.remove(0);
scheduler.update(elevator.getDirection(), elevator.getCurFloor());
addPassenger();
}
公测:
我方:86-87之间,由于没有对性能进行优化,所以只拿到了基础的正确分数,强测没有错误点。
互测:
采用传统的“读代码”方式debug,效率极低,而且几乎无法发现同组成员的bug,所以,提交的几次数据均未成功hack到对方。
第三次作业——多部捎带电梯
设计思路:
在第二次作业的基础之上,限制了电梯的楼层,所以我将1,15楼作为中转楼层,即乘客到达1,15楼时,如果目标楼层不在当前搭乘的电梯中,则需要换乘,换乘时,构造一条新的指令加入到请求队列中。然后限制电梯能捎带指令的楼层问题就转化为第二次电梯作业的模型了.
用UML和代码分析工具生成的类图如下:
method name | ev(G) | iv(G) | v(G) |
---|---|---|---|
elevator.ElevatorBase.arrive() | 1.0 | 3.0 | 3.0 |
elevator.ElevatorBase.checkArrive(Passenger) | 1.0 | 9.0 | 9.0 |
elevator.ElevatorBase.checkDir() | 3.0 | 4.0 | 5.0 |
elevator.ElevatorBase.checkOff() | 3.0 | 2.0 | 3.0 |
elevator.ElevatorBase.checkTrans() | 4.0 | 4.0 | 5.0 |
elevator.ElevatorBase.close(int) | 1.0 | 2.0 | 2.0 |
elevator.ElevatorBase.ElevatorBase(double,int,Scheduler,LiftType) | 1.0 | 1.0 | 1.0 |
elevator.ElevatorBase.getCurFloor() | 1.0 | 1.0 | 1.0 |
elevator.ElevatorBase.getDstFloor() | 1.0 | 1.0 | 1.0 |
elevator.ElevatorBase.getMainPerson() | 1.0 | 1.0 | 1.0 |
elevator.ElevatorBase.getOff() | 1.0 | 8.0 | 9.0 |
elevator.ElevatorBase.getOn(Passenger) | 1.0 | 1.0 | 1.0 |
elevator.ElevatorBase.getScheduler() | 1.0 | 1.0 | 1.0 |
elevator.ElevatorBase.getStatus() | 1.0 | 1.0 | 1.0 |
elevator.ElevatorBase.getTransFloors() | 1.0 | 1.0 | 1.0 |
elevator.ElevatorBase.getWaitList() | 1.0 | 1.0 | 1.0 |
elevator.ElevatorBase.isFull() | 1.0 | 1.0 | 1.0 |
elevator.ElevatorBase.liftMove() | 1.0 | 3.0 | 7.0 |
elevator.ElevatorBase.open(int) | 1.0 | 1.0 | 1.0 |
elevator.ElevatorBase.pickPerson() | 4.0 | 5.0 | 6.0 |
elevator.ElevatorBase.run() | 3.0 | 3.0 | 4.0 |
elevator.ElevatorBase.sendPerson() | 2.0 | 4.0 | 5.0 |
elevator.ElevatorBase.setDirection() | 1.0 | 4.0 | 7.0 |
elevator.ElevatorBase.setDoor(Passenger) | 1.0 | 6.0 | 8.0 |
elevator.ElevatorBase.setDstFloor(int) | 1.0 | 1.0 | 1.0 |
elevator.ElevatorBase.setFloors(List) | 1.0 | 1.0 | 1.0 |
elevator.ElevatorBase.setMainPerson(Passenger) | 1.0 | 1.0 | 1.0 |
elevator.ElevatorBase.setStatus(LiftStatus) | 1.0 | 1.0 | 1.0 |
elevator.ElevatorBase.upDateDst(Passenger) | 1.0 | 3.0 | 3.0 |
elevator.LiftA.LiftA(double,int,Scheduler,LiftType) | 1.0 | 1.0 | 1.0 |
elevator.LiftB.LiftB(double,int,Scheduler,LiftType) | 1.0 | 1.0 | 1.0 |
elevator.LiftB.upDateDst(Passenger) | 1.0 | 4.0 | 4.0 |
elevator.LiftC.arrive() | 1.0 | 7.0 | 7.0 |
elevator.LiftC.LiftC(double,int,Scheduler,LiftType) | 1.0 | 1.0 | 1.0 |
elevator.LiftC.upDateDst(Passenger) | 2.0 | 7.0 | 7.0 |
elevator.Passenger.getDirection() | 1.0 | 1.0 | 1.0 |
elevator.Passenger.getFromFloor() | 1.0 | 1.0 | 1.0 |
elevator.Passenger.getPersonId() | 1.0 | 1.0 | 1.0 |
elevator.Passenger.getToFloor() | 1.0 | 1.0 | 1.0 |
elevator.Passenger.isStraightA() | 1.0 | 1.0 | 1.0 |
elevator.Passenger.isStraightB() | 1.0 | 1.0 | 1.0 |
elevator.Passenger.isStraightC() | 1.0 | 1.0 | 1.0 |
elevator.Passenger.Passenger(int,int,int) | 1.0 | 1.0 | 1.0 |
elevator.Passenger.Passenger(PersonRequest) | 2.0 | 1.0 | 2.0 |
elevator.Passenger.setDirection() | 1.0 | 4.0 | 11.0 |
elevator.Passenger.setStraight() | 3.0 | 4.0 | 7.0 |
elevator.Passenger.toString() | 1.0 | 1.0 | 1.0 |
scheduler.Scheduler.addPassengers() | 1.0 | 1.0 | 1.0 |
scheduler.Scheduler.addPerson(Passenger) | 1.0 | 1.0 | 1.0 |
scheduler.Scheduler.getPassenger(LiftStatus,int,LiftType) | 9.0 | 10.0 | 13.0 |
scheduler.Scheduler.getPassenger(LiftType) | 11.0 | 17.0 | 17.0 |
scheduler.Scheduler.getPassengers() | 1.0 | 1.0 | 1.0 |
scheduler.Scheduler.getPerson(int,LiftStatus,LiftType) | 4.0 | 7.0 | 7.0 |
scheduler.Scheduler.getSize() | 1.0 | 1.0 | 1.0 |
scheduler.Scheduler.isEnd() | 1.0 | 1.0 | 2.0 |
scheduler.Scheduler.isWaitGet(LiftType) | 11.0 | 14.0 | 17.0 |
scheduler.Scheduler.Scheduler() | 1.0 | 1.0 | 1.0 |
scheduler.Scheduler.setInputEnd(boolean) | 1.0 | 1.0 | 1.0 |
scheduler.Scheduler.subPassengers() | 1.0 | 1.0 | 1.0 |
Total | 107.0 | 170.0 | 204.0 |
Average | 1.8135593220338984 | 2.8813559322033897 | 3.457627118644068 |
分析
截取了部分调度器内复杂度较高的方法块,发现圈复杂度和模块复杂度较高的方法都是添加指令方法中的条件判断语句,这种条件判断的方式容错性特别低;在本次作业的第一版本中,笔者就是由于条件判断的逻辑出现了错误,导致最后一个点一直WA。
if (type == LiftType.A) {
if (person.isStraightB() || person.isStraightC()) {
continue;
}
} else if (type == LiftType.B) {
if (person.isStraightC() || person.isStraightA()) {
continue;
}
} else if (type == LiftType.C) {
if (person.isStraightA() || person.isStraightB()) {
continue;
}
}
总结:
通过IDEA的代码分析工具发现,只有方法内部出现了较多的 if - else语句,就会使方法内部的各项复杂度指数飙升,相应的改进方法为:在三部电梯中,重写获取指令的方法,利用子类的特性来避免大量了if-else语句。
关于完成进度问题:在第三次电梯中,我对代码进行了大量的重构,主要是将原来耦合度较高的方法进行拆分,并且取消了调度器作为“电梯大脑”的职能,而电梯能自主获取调度器中的指令并且运行。在之后的作业中,对前一次作业的重构应该放在本次作业开始之前,为本次作业争取到更多的测试时间。
公测:
由于某处添加乘客的时候忘记限制楼层,导致强测WA了好几个点:
本来条件应该是这样,但是后来修改某处bug时,忘记添加if判断条件,导致电梯在不该开门的楼层接送了乘客。
(下次一定好好测试,,,这次时间太紧了,一波重构再一波多电梯,剩余的测试时间不是很多);
添加了此处的if语句之后,就顺利修复了所有bug。
if (floors.contains(curFloor)) {
Passenger p = pickPerson();
checkArrive(p);
}
互测:
采用的策略,通过自动生成数据,python搭建test评测机,自动化的对本房间内所有人进行测试。
总共hack了18次。
Java设计模式的选择
-
生产者消费者模式
在多线程中,生产者消费者模式是最常见的设计模式。此次作业中,输入线程作为生产者,电梯线程作为消费者,共享调度器这一对象(更准确的说法是:调度器中包含的一个指令队列)。为了保证线程安全和避免暴力轮询导致CPU超时,我采用了wait和notify机制:每当有指令加入请求队列时,notifyAll唤醒所有等待中的线程。
-
观察者模式
电梯作为消息的发送者,当电梯状态改变时,通知调度器针对当前电梯的状态做出反应。电梯状态的改变包括:运行方向的改变、所处楼层的变化,运行与等待的变化;而调度器针对不同类型状态的变化,从请求队列中选择电梯允许执行的请求,让电梯捎带或是起步运行。