面向对象编程第2次总结(电梯作业)
第5次作业
设计策略
ElevatorConfig:存放电梯的信息
Elevator:存储电梯状态
ElevatorControl:控制电梯
ElevatorRunnable:控制器线程
InvalidRequest:控制器接收到该类请求,停止电梯运行
Main:主函数
RequestHandler:处理乘客请求
WorkRunnable:工作线程,即调度器
本次作业电梯的调度策略为先来先服务。
最开始笔者使用的控制逻辑是:电梯调度器为每个乘客创建一个新线程RequestHandler,同一时刻,只有一个乘客能使用电梯,由控制器模拟完成请求。这样设计是参考了WEB服务器的多线程模式:每个请求创建一个新线程进行处理,代码实现简单。不过这样设计的最大不足之处是,请求之间互相独立,无法实现合并。
之后的版本,笔者使用生产者-消费者模型,维护一个请求队列(使用BlockingQueue),主函数读入输入,将请求加入到请求队列,工作线程(调度器)从请求队列获取请求,并使用ElevatorControl类控制电梯运行。输入结束后,将InvalidRequest加入请求队列,如果请求队列只有InvalidRequest,调度器关闭电梯并退出。
SOLID设计原则
单一功能原则
基本遵循。除了Elevator类有输出电梯操作的代码(这部分代码应该属于控制器)
开闭原则
Elevator遵循了此原则,其他的类不符合此原则,在加入需要合并的功能时需要重构。
里氏替换原则
本次设计未使用继承,暂不考虑。
接口隔离原则
本次设计未使用接口,暂不考虑。
依赖反转原则
本次设计未使用抽象类或接口,部分违反了此原则。
代码复杂度
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.Elevator() | 1 | 1 | 1 |
Elevator.closeDoor() | 1 | 1 | 1 |
Elevator.getCurrentFloor() | 1 | 1 | 1 |
Elevator.getState() | 1 | 1 | 1 |
Elevator.isReady() | 1 | 1 | 1 |
Elevator.move(int) | 1 | 2 | 2 |
Elevator.openDoor() | 1 | 1 | 1 |
Elevator.tryRequest(int) | 2 | 2 | 3 |
ElevatorControl.ElevatorControl() | 1 | 1 | 1 |
ElevatorControl.free() | 1 | 1 | 1 |
ElevatorControl.getCurrentFloor() | 1 | 1 | 1 |
ElevatorControl.getInstance() | 1 | 1 | 1 |
ElevatorControl.isBusy() | 1 | 1 | 1 |
ElevatorControl.lock() | 1 | 1 | 1 |
ElevatorControl.release() | 1 | 2 | 2 |
ElevatorControl.request(int) | 1 | 3 | 3 |
ElevatorControl.reset() | 1 | 2 | 2 |
ElevatorControl.run(int) | 1 | 1 | 1 |
ElevatorControl.tryLock() | 1 | 1 | 1 |
ElevatorRequest.ElevatorRequest(int,int) | 1 | 1 | 3 |
ElevatorRequest.equals(Object) | 3 | 1 | 4 |
ElevatorRequest.getFromFloor() | 1 | 1 | 1 |
ElevatorRequest.getToFloor() | 1 | 1 | 1 |
ElevatorRequest.hashCode() | 1 | 1 | 1 |
ElevatorRequest.isValid() | 1 | 1 | 1 |
ElevatorRequest.toString() | 1 | 1 | 1 |
ElevatorRunnable.ElevatorRunnable() | 1 | 1 | 1 |
ElevatorRunnable.free() | 1 | 1 | 1 |
ElevatorRunnable.getCurrentFloor() | 1 | 1 | 1 |
ElevatorRunnable.isBusy() | 1 | 1 | 1 |
ElevatorRunnable.move(int) | 1 | 1 | 1 |
ElevatorRunnable.request(int) | 1 | 3 | 3 |
ElevatorRunnable.run() | 1 | 1 | 2 |
Main.handleInput(ElevatorInput) | 1 | 3 | 3 |
Main.handleInputThreaded(ElevatorInput) | 1 | 4 | 4 |
Main.main(String[]) | 1 | 2 | 2 |
RequestHandler.RequestHandler(PersonRequest) | 1 | 1 | 1 |
RequestHandler.run() | 1 | 2 | 2 |
Scheduler.Scheduler() | 1 | 1 | 1 |
Scheduler.getInstance() | 1 | 1 | 3 |
WorkRunnable.WorkRunnable() | 1 | 1 | 1 |
WorkRunnable.getRequest() | 2 | 1 | 2 |
WorkRunnable.isBusy() | 1 | 1 | 1 |
WorkRunnable.putRequest(Object) | 1 | 1 | 1 |
WorkRunnable.run() | 3 | 3 | 4 |
Class | OCavg | WMC |
---|---|---|
Elevator | 1.38 | 11 |
Elevator.State | n/a | 0 |
ElevatorConfig | n/a | 0 |
ElevatorControl | 1.36 | 15 |
ElevatorRequest | 1.43 | 10 |
ElevatorRunnable | 1.43 | 10 |
InvalidRequest | n/a | 0 |
Main | 2 | 6 |
RequestHandler | 1 | 2 |
Scheduler | 2 | 4 |
WorkRunnable | 1.6 | 8 |
这次作业的代码复杂度在可以接受范围内,原因是作业要求十分简单,电梯只要从请求队列不断取请求并执行,不用考虑同方向请求的合并。
类图
实际有2个类未被使用。
UML时序图
互测
由于本次作业要求十分简单,没有发现bug
第6次作业
设计策略
使用生产者-消费者模型。Main读入输入,将请求加入Scheduler的请求队列,Scheduler线程扫描请求队列的请求并合并可以捎带的请求。Scheduler控制电梯每次只移动一个楼层的距离,每到达楼层都检查请求是否有已经到达的乘客,并检查是否可以捎带乘客。
SOLID设计原则
单一功能原则
Elevator:部分遵循此原则,负责模拟电梯运行,但是电梯状态和控制逻辑并存
Main:遵循此原则。读入请求,并加入调度器
Scheduler:遵循此原则,负责调度电梯
Log:遵循此原则。输出调试信息
开闭原则
Scheduler:不遵循此原则,由于下一次电梯作业加入了容量限制,在调度时需要考虑。
Elevator:基本遵循此原则。
Main:不遵循此原则。
Log:遵循此原则。本次通过重载加入了输出TAG的功能,类似于Android的logcat。
里氏替换原则
本次设计未使用继承或接口,暂不考虑。
接口隔离原则
本次设计未使用继承或接口,暂不考虑。
依赖反转原则
调度器依赖于电梯的具体方法,违背了此原则。
代码复杂度
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
singlethread.Elevator.Elevator() | 1 | 1 | 1 |
singlethread.Elevator.close() | 2 | 2 | 2 |
singlethread.Elevator.free() | 2 | 1 | 2 |
singlethread.Elevator.getDirection() | 1 | 1 | 1 |
singlethread.Elevator.getFloor() | 1 | 1 | 1 |
singlethread.Elevator.getPersons() | 1 | 1 | 1 |
singlethread.Elevator.getState() | 1 | 1 | 1 |
singlethread.Elevator.in(PersonRequest) | 2 | 3 | 3 |
singlethread.Elevator.isEmpty() | 1 | 1 | 1 |
singlethread.Elevator.moveTo(int) | 8 | 5 | 8 |
singlethread.Elevator.nextFloor() | 7 | 5 | 7 |
singlethread.Elevator.open() | 2 | 2 | 2 |
singlethread.Elevator.outAll() | 5 | 6 | 9 |
singlethread.Elevator.setDirection(int) | 1 | 1 | 1 |
singlethread.Elevator.setFloor(int) | 1 | 1 | 1 |
singlethread.Elevator.step() | 2 | 3 | 4 |
singlethread.ElevatorConfig.isDisabled(int) | 1 | 1 | 1 |
singlethread.SingleMain.handleRequest() | 1 | 1 | 1 |
singlethread.SingleMain.main(String[]) | 3 | 4 | 4 |
singlethread.SingleScheduler.SingleScheduler() | 1 | 1 | 1 |
singlethread.SingleScheduler.getInstance() | 1 | 1 | 1 |
singlethread.SingleScheduler.hasNext() | 3 | 2 | 3 |
singlethread.SingleScheduler.isUpwards(PersonRequest) | 1 | 1 | 1 |
singlethread.SingleScheduler.merge() | 1 | 10 | 10 |
singlethread.SingleScheduler.pollRequest() | 2 | 2 | 2 |
singlethread.SingleScheduler.put(PersonRequest) | 1 | 1 | 1 |
singlethread.SingleScheduler.run() | 5 | 6 | 8 |
singlethread.SingleScheduler.stop() | 1 | 1 | 1 |
utils.Log.Log() | 1 | 1 | 1 |
utils.Log.d(String) | 1 | 1 | 1 |
utils.Log.e(String) | 1 | 1 | 1 |
utils.Log.getPriorityStr(int) | 8 | 2 | 8 |
utils.Log.i(String) | 1 | 1 | 1 |
utils.Log.println(int,String) | 1 | 2 | 3 |
utils.Log.v(String) | 1 | 1 | 1 |
utils.Log.w(String) | 1 | 1 | 1 |
Class | OCavg | WMC |
---|---|---|
singlethread.Elevator | 2.81 | 45 |
singlethread.Elevator.State | n/a | 0 |
singlethread.ElevatorConfig | 1 | 1 |
singlethread.SingleMain | 2 | 4 |
singlethread.SingleScheduler | 2.4 | 24 |
utils.Log | 2 | 16 |
调度器复杂度较高,这是由于调度器算法实现的时候更多是面向过程。但是电梯类的复杂度最高,这一点确实值得反思,笔者认为是由于将人员进出的逻辑放在电梯类的原因。
类图
UML时序图
互测
我方bug
笔者在测试中未被找出bug
对方bug
笔者找出对方的bug共1个。
对方的bug是会出现电梯不停止,到达规定以外的楼层。
第7次作业
设计策略
依然使用生产者-消费者模型,Scheduler负责拆分请求、分配请求到各电梯。SubScheduler负责各自的电梯调度,合并可捎带请求。SubScheduler如果遇到需要中转的请求,先到达中转楼层(由上一级调度器确定),将后半段加入到上一级调度器等待调度。
由于线程方面的原因,如果将请求重新加入队列,可能会出现子调度器提前退出导致调度异常。笔者将需要中转的请求后半段加入调度器的另一个请求队列,当调度器处理完当前队列后,再处理中转请求的后半段。
SOLID设计原则
单一功能原则
Elevator:部分遵循此原则,负责模拟电梯运行,但是在处理人员进出电梯时需要将未完成的中转请求加入调度器,违反了单一功能。
Main:遵循此原则。读入请求,并加入调度器
Scheduler:遵循此原则,负责拆分、分配请求
SubScheduler:遵循此原则,负责调度具体电梯
Log:遵循此原则。输出调试信息
SafeTimableOutput:遵循此原则。加锁保护非线程安全的TimableOutput,只负责输出信息。
开闭原则
Elevator:不遵循此原则,由于加入了容量限制、运行速度、可停靠楼层。
Main:不遵循此原则。
Scheduler:不遵循此原则,由于加入了容量限制、运行速度、可停靠楼层,在调度时需要考虑。
SubScheduler:不遵循此原则,由于加入了容量限制、运行速度、可停靠楼层,在调度时需要考虑。
Log:遵循此原则。本次通过重载加入了输出TAG的功能,类似于Android的logcat。
SafeTimableOutput:依赖于TimableOutput,遵循此原则。
CombineRequest:遵循此原则。只加入中转楼层的信息及其getter。
里氏替换原则
CombineRequest:继承PersonRequest类,在保留原有方法功能的基础上加入了中转楼层的信息。
InvalidPersonRequest:仅用来标注非法输入,用来结束调度器运行。
接口隔离原则
本次设计未使用接口,暂不考虑。
依赖反转原则
调度器依赖于电梯及其子调度器的具体方法,违背了此原则。
代码复杂度
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
elevator.CombineRequest.CombineRequest(int,int,int,int) | 1 | 1 | 1 |
elevator.CombineRequest.getMiddleFloor() | 1 | 1 | 1 |
elevator.CombineRequest.toString() | 1 | 1 | 1 |
elevator.Elevator.Elevator(String,Set |
1 | 1 | 1 |
elevator.Elevator.ElevatorException.ElevatorException() | 1 | 1 | 1 |
elevator.Elevator.ElevatorException.ElevatorException(String) | 1 | 1 | 1 |
elevator.Elevator.close() | 3 | 2 | 3 |
elevator.Elevator.free() | 2 | 1 | 2 |
elevator.Elevator.getCapacity() | 1 | 1 | 1 |
elevator.Elevator.getDirection() | 1 | 1 | 1 |
elevator.Elevator.getFloor() | 1 | 1 | 1 |
elevator.Elevator.getId() | 1 | 1 | 1 |
elevator.Elevator.getNearFloor() | 1 | 2 | 3 |
elevator.Elevator.getState() | 1 | 1 | 1 |
elevator.Elevator.in(PersonRequest) | 3 | 4 | 4 |
elevator.Elevator.isEmpty() | 1 | 1 | 1 |
elevator.Elevator.isFull() | 1 | 1 | 1 |
elevator.Elevator.isReachable(int) | 1 | 1 | 1 |
elevator.Elevator.moveTo(int) | 8 | 5 | 10 |
elevator.Elevator.nextFloor() | 7 | 1 | 9 |
elevator.Elevator.open() | 3 | 2 | 3 |
elevator.Elevator.outAll() | 8 | 12 | 14 |
elevator.Elevator.printArrive(int) | 1 | 1 | 1 |
elevator.Elevator.remainingCapacity() | 1 | 1 | 1 |
elevator.Elevator.setDirection(int) | 1 | 1 | 1 |
elevator.Elevator.step() | 2 | 3 | 4 |
elevator.InvalidPersonRequest.InvalidPersonRequest() | 1 | 1 | 1 |
elevator.Main.main(String[]) | 3 | 4 | 4 |
elevator.Scheduler.Scheduler() | 1 | 3 | 3 |
elevator.Scheduler.consumeRequest(PersonRequest) | 18 | 15 | 21 |
elevator.Scheduler.getInstance() | 1 | 1 | 1 |
elevator.Scheduler.isEmpty() | 4 | 2 | 4 |
elevator.Scheduler.pollRequest() | 3 | 3 | 3 |
elevator.Scheduler.put(PersonRequest) | 1 | 1 | 1 |
elevator.Scheduler.putMid(PersonRequest) | 1 | 1 | 1 |
elevator.Scheduler.run() | 5 | 7 | 10 |
elevator.Scheduler.splitMiddle(PersonRequest,int) | 4 | 7 | 7 |
elevator.Scheduler.stop() | 1 | 1 | 1 |
elevator.SubScheduler.SubScheduler(Elevator) | 1 | 1 | 1 |
elevator.SubScheduler.isAllInvalid() | 2 | 2 | 5 |
elevator.SubScheduler.isEmpty() | 2 | 1 | 2 |
elevator.SubScheduler.isFull() | 1 | 1 | 1 |
elevator.SubScheduler.isReachable(int) | 1 | 1 | 1 |
elevator.SubScheduler.merge() | 8 | 10 | 12 |
elevator.SubScheduler.pollRequest() | 3 | 3 | 3 |
elevator.SubScheduler.put(PersonRequest) | 1 | 1 | 1 |
elevator.SubScheduler.run() | 4 | 5 | 6 |
elevator.SubScheduler.stop() | 1 | 1 | 1 |
utils.Log.Log() | 1 | 1 | 1 |
utils.Log.d(String,String) | 1 | 1 | 1 |
utils.Log.e(String,String) | 1 | 1 | 1 |
utils.Log.getPriorityStr(int) | 8 | 2 | 8 |
utils.Log.i(String,String) | 1 | 1 | 1 |
utils.Log.println(int,String) | 1 | 2 | 3 |
utils.Log.println(int,String,String) | 1 | 2 | 3 |
utils.Log.v(String,String) | 1 | 1 | 1 |
utils.Log.w(String,String) | 1 | 1 | 1 |
utils.SafeTimableOutput.initStartTimestamp() | 1 | 1 | 1 |
utils.SafeTimableOutput.println(Object) | 1 | 1 | 1 |
utils.SafeTimableOutput.println(boolean) | 1 | 1 | 1 |
utils.SafeTimableOutput.println(char) | 1 | 1 | 1 |
utils.SafeTimableOutput.println(char[]) | 1 | 1 | 1 |
utils.SafeTimableOutput.println(double) | 1 | 1 | 1 |
utils.SafeTimableOutput.println(float) | 1 | 1 | 1 |
utils.SafeTimableOutput.println(int) | 1 | 1 | 1 |
utils.SafeTimableOutput.println(long) | 1 | 1 | 1 |
Class | OCavg | WMC |
---|---|---|
elevator.CombineRequest | 1 | 3 |
elevator.Elevator | 2.95 | 62 |
elevator.Elevator.ElevatorException | 1 | 2 |
elevator.Elevator.State | n/a | 0 |
elevator.InvalidPersonRequest | 1 | 1 |
elevator.Main | 3 | 3 |
elevator.Scheduler | 4.4 | 44 |
elevator.SubScheduler | 2.9 | 29 |
utils.Log | 2 | 18 |
utils.SafeTimableOutput | 1 | 9 |
不幸的是,由于时间有限,笔者并未深入思考如何构建良好的代码结构,导致主要类复杂度过高。
其中,调度器合并请求的函数长度过长,电梯类关于人员进出的方法代码重复较多,需要进一步重构。
类图
UML时序图
互测
我方bug
笔者在测试中被找出多个同质bug,即到达最高楼层后,电梯没有改变运行方向下行,导致无限循环。
例如,对于这个样例
[1.0]21-FROM-3-TO-1
[1.0]22-FROM-3-TO-2
[1.0]23-FROM-3-TO--3
[1.0]24-FROM-3-TO-4
[1.0]25-FROM-3-TO-5
[1.0]26-FROM-3-TO-6
[1.0]27-FROM-3-TO-7
[1.0]28-FROM-3-TO-8
[1.0]10-FROM-3-TO--2
[1.0]11-FROM-3-TO--1
程序有可能进入死循环,部分输出如下:
[ 12.1720]ARRIVE-12-C
[ 12.7740]ARRIVE-13-C
[ 13.3740]ARRIVE-14-C
[ 13.9760]ARRIVE-15-C
[ 14.5780]ARRIVE-16-C
[ 15.1790]ARRIVE-17-C
[ 15.7810]ARRIVE-18-C
[ 16.3820]ARRIVE-19-C
[ 16.9840]ARRIVE-20-C
[ 17.5850]ARRIVE-20-C
[ 18.1870]ARRIVE-20-C
[ 18.7880]ARRIVE-20-C
[ 19.3880]ARRIVE-20-C
[ 19.9900]ARRIVE-20-C
在多次测试中,笔者的代码也出现了随机Wrong answer的问题,推测是线程安全问题导致的问题,但是不幸的是,由于代码复杂度太高,笔者未能找到本质原因。
作为hotfix,笔者在电梯的运行方法加入了在到达最高或最低楼层后自动改变电梯运行方向的代码,暂时修复了这个bug。
对方bug
笔者找出对方的bug共5个。使用的方法很简单,是人工构造边界情况的数据,编写数据输入程序定时输入,例如同时输入超过电梯容量数量的请求,输入多个需要中转的请求,第一批请求结束后再输入请求等。
总的来说,对方的bug是会出现电梯不停止或者提前退出的问题。
心得体会
关于线程,本次电梯作业有两大难点:
- 请求队列、电梯控制器等线程安全问题。
- 在输入结束后,电梯应该处理完剩余的请求,然后关闭,最后退出整个程序。
调试方面,由线程安全导致的错误很难重现,但是可以通过不断重复测试找出问题。
关于设计方面,最大的经验教训是一开始的设计十分重要,必须充分考虑可扩展性,避免每次新作业都要重构代码的情况。同时,减少方法、类的复杂度,对定位问题帮助很大,过度耦合、过高的复杂度会加大问题的复杂程度,往往会拆东墙补西墙,修完BUG后又出现新的BUG,必须避免这种情况的发生。