设计策略
对于这一单元,我三次作业的设计基本一致,均为主类-输入类-调度器类-电梯类组合。其中输入线程将受到的请求交给调度器,调度器将请求指定给某个电梯,而电梯线程则专注于处理自身的等待队列中的请求。
第一次作业由于只有一部电梯,因此调度器直接将所有请求交给唯一的一部电梯,电梯使用look算法上下跑。
第二次作业由于有多部电梯并且加入了载客量限制,因此调度器需要决定将请求派给哪部电梯。我采用的方法是根据电梯内的人数和电梯的等待队列中的人数之和(也就是我定义的负载)来决策,当存在负载小于7的电梯时,从这些电梯中选择离请求最近的一部;当不存在负载小于7的电梯时,从所有电梯中选择负载最小的一部电梯。
第三次作业增加了不同类型电梯的楼层限制,因此先计算出换乘的方案,为了避免写错,我选择了好写的静态方案,即对于相同的起点和终点,换乘方案相同,并将换乘路线上的各个换乘点依次以ArrayList的形式保存在Person中。除此之外电梯的调度、调度器的指派原则均与第二次作业相同,只是指派时只在可以完成当前段请求的电梯中查找。
我的三次作业强测均在96分以上,尽管使用了并不出众的look算法,但是由于look算法极佳的均衡性,稳扎稳打拿下了令自己满意的性能分。
可扩展性
功能设计
由于我的电梯的换乘采用静态方案,固定以1,5,15层作为换乘层,因此当电梯可达楼层改变时,原有的计算换乘方案的方法就不可用了。
除此之外,其他部分,包括调度原则、电梯运行算法等,可以继续沿用。
性能设计
我的电梯中,检验是否有请求等操作采用遍历的方法,性能不够高,另外查询别的电梯中是否有请求时也是遍历所有电梯,而加锁就会导致这样的做法性能不够高。不过由于电梯速度、容量限制了电梯单位时间的吞吐量,因此程序自身的性能影响不大。
SOLID分析
SRP
总体上各个类的分工较明确,但是有少量方法的工作有重叠,比如电梯类和Person类均有计算方向、距离的方法,这样的计算并不属于电梯和人的工作,应该单独开一个计算类来处理请求相关的计算。
OCP
Elevator类做的较好,每次基本都是直接用上一次的,很少修改。
Scheduler类做的不够好,这和我的设计想法有关。我做第一次电梯的时候其实无需设计一个调度器类,但是为了留出扩展性就设计了一个没有什么实际用处的调度器类,想着当新加需求的时候就修改这个类来满足需求。
LSP
没有使用继承关系,实际上第三次作业很适合使用继承关系来创建有不同属性的电梯。
ISP
没有建立接口,主要是基于电梯有大量字段的考虑。使用接口并不能方便的处理不同类型电梯中共有的字段。
DIP
这一点在我的作业中处理的不好。我的各个实现类之间都是直接依赖于实现类,而没有依赖于抽象类。
程序结构
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.run() | 1.0 | 12.0 | 13.0 |
Person.getRoute() | 3.0 | 10.0 | 13.0 |
Input.run() | 3.0 | 6.0 | 6.0 |
ElevatorFactory.Init(EndStatus,Scheduler) | 5.0 | 5.0 | 8.0 |
Elevator.getOut() | 2.0 | 4.0 | 5.0 |
Elevator.keepDir() | 1.0 | 3.0 | 4.0 |
RequestList.get(int,int,int) | 4.0 | 3.0 | 4.0 |
Elevator.stopAt() | 1.0 | 3.0 | 3.0 |
Elevator.getIn() | 3.0 | 3.0 | 5.0 |
Scheduler.allNoRequest() | 3.0 | 2.0 | 3.0 |
Elevator.hasToSameDir() | 4.0 | 2.0 | 5.0 |
Elevator.canArrive(int,int) | 1.0 | 2.0 | 2.0 |
Scheduler.allEmpty() | 3.0 | 2.0 | 3.0 |
Elevator.stay() | 1.0 | 2.0 | 2.0 |
Person.getTo() | 1.0 | 2.0 | 2.0 |
RequestList.noRequest() | 3.0 | 2.0 | 3.0 |
RequestList.sameDirReq(int,int) | 5.0 | 2.0 | 5.0 |
ElevatorFactory.getElevator(String,String) | 3.0 | 2.0 | 3.0 |
Person.pop() | 1.0 | 2.0 | 2.0 |
RequestList.put(Person) | 3.0 | 2.0 | 3.0 |
RequestList.hasToSameDir(int,int) | 2.0 | 1.0 | 2.0 |
Person.hasNext() | 1.0 | 1.0 | 1.0 |
Elevator.getCurFloor() | 1.0 | 1.0 | 1.0 |
Person.Person(PersonRequest) | 1.0 | 1.0 | 1.0 |
ElevatorFactory.getReachSetB() | 1.0 | 1.0 | 1.0 |
Elevator.getDirection() | 1.0 | 1.0 | 1.0 |
Elevator.getCapacity() | 1.0 | 1.0 | 1.0 |
Elevator.hasArrived() | 1.0 | 1.0 | 1.0 |
Elevator.Elevator(EndStatus,String,int,long,HashSet,int,int,Scheduler) | 1.0 | 1.0 | 1.0 |
Elevator.calDir(int) | 3.0 | 1.0 | 3.0 |
Elevator.notFull() | 1.0 | 1.0 | 1.0 |
RequestList.RequestList() | 1.0 | 1.0 | 1.0 |
Elevator.getDistance(Person) | 1.0 | 1.0 | 1.0 |
Scheduler.Scheduler() | 1.0 | 1.0 | 1.0 |
Person.getDistance() | 1.0 | 1.0 | 2.0 |
MainClass.main(String[]) | 1.0 | 1.0 | 1.0 |
SafetyOutput.println(String) | 1.0 | 1.0 | 1.0 |
Person.setCurFloor(int) | 1.0 | 1.0 | 1.0 |
Elevator.put(Person) | 1.0 | 1.0 | 1.0 |
Elevator.noRequest() | 1.0 | 1.0 | 1.0 |
ElevatorFactory.getReachSetA() | 1.0 | 1.0 | 1.0 |
Person.getCurrent() | 1.0 | 1.0 | 1.0 |
ElevatorFactory.getReachSetC() | 1.0 | 1.0 | 1.0 |
Scheduler.addElevator(String,String) | 1.0 | 1.0 | 1.0 |
Person.getId() | 1.0 | 1.0 | 1.0 |
RequestList.getReqNum() | 1.0 | 1.0 | 1.0 |
Elevator.getWorkload() | 1.0 | 1.0 | 1.0 |
EndStatus.markEnd() | 1.0 | 1.0 | 1.0 |
EndStatus.getStatus() | 1.0 | 1.0 | 1.0 |
Elevator.isEmpty() | 1.0 | 1.0 | 1.0 |
Elevator.getDistance(int,int) | 1.0 | 1.0 | 2.0 |
Input.Input(Scheduler,EndStatus) | 1.0 | 1.0 | 1.0 |
Person.getDir() | 3.0 | 1.0 | 3.0 |
Total | 92.0 | 122.0 | 158.0 |
Average | 1.7037037037037037 | 2.259259259259259 | 2.925925925925926 |
Class | OCavg | WMC |
---|---|---|
Elevator | 2.0952380952380953 | 44.0 |
ElevatorFactory | 2.6 | 13.0 |
ElevatorType | 0.0 | |
EndStatus | 1.0 | 2.0 |
Input | 3.0 | 6.0 |
MainClass | 1.0 | 1.0 |
Person | 2.2 | 22.0 |
RequestList | 2.5 | 20.0 |
SafetyOutput | 1.0 | 1.0 |
Scheduler | 4.8 | 24.0 |
Total | 133.0 | |
Average | 2.418181818181818 | 13.3 |
第三次作业类图
调度器和电梯类之间的耦合度还是比较高的。
程序BUG
这三次作业在强测阶段均无bug,前两次作业在互测阶段也没有bug,第三次作业在互测中被一个一瞬间几十条输入的的数据hack了,但是本地测试了八百多遍没能复现此bug,并且也没能想出代码中有什么线程安全问题。不过没能复现可能是因为自己写的评测机和课程组的评测机实现方式不同。与助教讨论后助教表示可能是程序鲁棒性不够导致的,尽管我在本单元吸取上一单元的教训,已经在尽力加强鲁棒性了,但是可能还是有考虑不周的地方,在后面的单元要继续注重提高鲁棒性。
发现别人BUG采用的策略
由于时间有限,本单元全部采用黑盒测试,即使用自己写的评测机生成数据并评测,如果发现BUG了会通过看源代码这种白盒测试方式找代码中的问题,从而确定hack的是否为同质bug。虽然随机生成的数据不够有针对性,但是通过评测量的积累,依然发现了不少别人的bug,主要都在线程安全方面。这也启示我,对待线程安全问题一定要小心谨慎,多想想哪些方法、哪些对象需要加锁,如何避免死锁,如何避免睡死。
本单元与上一单元测试的不同在于上一单元的测试结果是确定的,只要出错就一定能得到复现,而本单元由于多线程的不确定性,一些本地测试出的bug再次运行时不一定会出错,交上去后也未必能成功hack,因此我成功hack的bug数比本地测出的bug数少很多。
在本单元,如何写出线程安全的代码是一种难题,并且由于多线程的不确定性,调试变得更加困难。幸运的是,我没有因为线程安全出现问题。想要写出安全的代码,还是应该做出周全的设计,磨刀不误砍柴工,精良的设计将让编码变得更加容易,并且大大减少debug的时间,不论从时间上讲还是从分数上讲都是非常划算的一件事。