OO第二单元总结——电梯系列
一、设计策略
这一单元三次作业都采用生产者——消费者模式。输入线程作为生产者,电梯线程作为消费者,再有一个接受和处理请求的调度器即托盘。
这三者的分工非常明确,在代码实现上也应如此。
输入线程:任务是获取乘客的请求,当然也需要将请求推送给调度器,以便电梯能获取请求。
电梯线程:任务是接送乘客,所以需要实现上下移动、开门、关门,我实现的电梯是自己主动向调度器获取请求的,可能与我实现的算法有关——look算法,在电梯到达每一层时,向调度器询问这一层是否有乘客,若有则获取,当然,不能超出人数限制。第七次作业中,电梯还需要推送乘客请求给调度器。
调度器:相当于一个中转站,接受请求并保存到队列里。调度器也接受从电梯线程发送的询问,再根据情况分配给电梯请求。为了保证线程安全,我使用了LinkedBlockingQueue
来保存乘客请求。
第五次作业:只有一部电梯,我新增了Person
类将PersonRequest
类包装了一下,增加了方向等属性,方便调度器调度。然后就如上所述,简简单单的跑下来没有任何问题。
第六次作业:由一部电梯变为多部电梯,并增加乘客载客量限制。由于我的策略是电梯主动获取请求,所以调度器只判断是否有请求给这个电梯,而不需要判断给哪部电梯,所以实现上非常方便,只需要调度器给电梯分配请求时判断电梯人数即可。
第七次作业:电梯有了可停靠楼层的限制,所以会有一些乘客需要换乘才能到达目的地。由于电梯的改变,我新增了一个Elevator
类来封装电梯的一些信息,如可停靠楼层、类型、Id、运行时间等等,这么做方便调度器来判断请求能否分配给某个类型的电梯,也避免了电梯线程的臃肿。Person
类中也增加了一些属性,如是否需要换乘、是否到达换乘楼层、换乘楼层等等。这次作业中,除了电梯线程需要向调度器发送未到达目的地的乘客请求,以及调度器分配请求时的判断变得复杂一点外,其他实现并无较大改变。
二、可扩展性
功能设计与性能设计
实现面向对象编程,对每一类或者线程,都使他们分工明确,职责单一,但分析了一下SOLID原则,似乎扩展会遇到很大的问题。
第七次作业中,由于换乘增加了Elevator
类,不过换乘采用的是,以1、5、15作为换乘楼层,若是改变了电梯的可停靠楼层,就得重新考虑如何换乘。
第七次作业有多部电梯,为提高性能,尽量避免换乘的策略,如果数据十分有针对性,性能可能就比较低。
SRP
类和方法都做到了职责明确,不过有些电梯类的方法有点多,可以把输出再开一个类。
OCP
当类需要添加属性和方法时,也很容易实现。基本上没有修改已有实现,而是通过扩展来增加新功能。不过结束线程的方法在第七次中改变了,因为乘客不能一次送达,原本是判断输入线程是否结束以及调度器类中的队列是否为空,更改为判断输入线程是否结束以及所有乘客是否已达目的地。
LSP
没有用到继承,所以也没什么好说的。
ISP
没有实现接口,没有什么并列的类需要实现,所以接口的实现似乎是可有可无的?
DIP
没有做到依赖于抽象类,需要改进。
三、程序结构
由于三次作业程序结构的差别并不是很大,所以仅对第三次作业做一下分析。
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.Elevator(ElevatorRequest) | 1 | 1 | 1 |
Elevator.Elevator(String,String) | 1 | 1 | 1 |
Elevator.equals(Object) | 3 | 3 | 5 |
Elevator.getElevatorId() | 1 | 1 | 1 |
Elevator.getPersonMax() | 1 | 1 | 1 |
Elevator.getRunTime() | 1 | 1 | 1 |
Elevator.hashCode() | 1 | 1 | 1 |
Elevator.isFloorArrive(int) | 1 | 1 | 1 |
Elevator.setRunTimeAndPersonMax() | 1 | 4 | 4 |
Elevator.toString() | 1 | 1 | 1 |
ElevatorThread.ElevatorThread() | 1 | 1 | 1 |
ElevatorThread.ElevatorThread(Scheduler,Elevator) | 1 | 1 | 1 |
ElevatorThread.arrive() | 1 | 1 | 1 |
ElevatorThread.close() | 1 | 1 | 1 |
ElevatorThread.downFloor(int) | 3 | 7 | 10 |
ElevatorThread.getDirection() | 1 | 1 | 1 |
ElevatorThread.getToFloor(Person) | 3 | 3 | 3 |
ElevatorThread.getToFloorFirst(Person) | 3 | 3 | 3 |
ElevatorThread.isEmpty() | 1 | 1 | 1 |
ElevatorThread.open() | 1 | 1 | 1 |
ElevatorThread.out() | 1 | 4 | 4 |
ElevatorThread.personIn(Person) | 1 | 1 | 1 |
ElevatorThread.personOut(Person) | 1 | 1 | 1 |
ElevatorThread.put(Person) | 1 | 2 | 2 |
ElevatorThread.run() | 6 | 9 | 14 |
ElevatorThread.upFloor(int) | 3 | 7 | 10 |
Input.Input(Scheduler) | 1 | 1 | 1 |
Input.elevatorMake(ElevatorRequest) | 1 | 1 | 1 |
Input.elevatorMakeFirst() | 1 | 1 | 1 |
Input.run() | 3 | 7 | 7 |
MainClass.main(String[]) | 1 | 1 | 1 |
Person.Person() | 1 | 1 | 1 |
Person.Person(PersonRequest) | 1 | 1 | 2 |
Person.Person(int,int,int,boolean,int) | 1 | 1 | 1 |
Person.equals(Object) | 3 | 12 | 14 |
Person.getDirection() | 1 | 1 | 1 |
Person.getDirection1() | 1 | 1 | 1 |
Person.getDirection2() | 1 | 1 | 1 |
Person.getFromFloor() | 1 | 1 | 1 |
Person.getGetNum() | 1 | 1 | 1 |
Person.getMidFloor() | 1 | 1 | 1 |
Person.getPersonId() | 1 | 1 | 1 |
Person.getToFloor() | 1 | 1 | 1 |
Person.hashCode() | 1 | 1 | 1 |
Person.isLoaded() | 1 | 1 | 1 |
Person.isNeedTransfer() | 1 | 1 | 1 |
Person.setDirection() | 1 | 1 | 3 |
Person.setGetNum() | 1 | 1 | 1 |
Person.setLoaded(boolean) | 1 | 1 | 1 |
Person.setMidFloor() | 1 | 7 | 15 |
Person.toString() | 1 | 1 | 1 |
SafeOutput.println(String) | 1 | 1 | 1 |
Scheduler.Scheduler() | 1 | 1 | 1 |
Scheduler.canGetPerson(Elevator,Person,int) | 6 | 9 | 12 |
Scheduler.canGetPersonFirst(Elevator,Person) | 6 | 6 | 9 |
Scheduler.get() | 1 | 1 | 1 |
Scheduler.get(int,int,int,Elevator) | 4 | 3 | 5 |
Scheduler.getFirst(int,Elevator) | 3 | 6 | 9 |
Scheduler.isEmpty() | 1 | 1 | 1 |
Scheduler.isEnd() | 1 | 1 | 2 |
Scheduler.minusCount() | 1 | 1 | 1 |
Scheduler.put(Person) | 1 | 1 | 2 |
Scheduler.setInputEnd(boolean) | 1 | 1 | 1 |
Class | OCavg | WMC |
---|---|---|
Elevator | 1.5 | 15 |
ElevatorThread | 3.06 | 49 |
Input | 2 | 8 |
MainClass | 1 | 1 |
Person | 1.7 | 34 |
SafeOutput | 1 | 1 |
Scheduler | 2.91 | 32 |
可以看出其中ElevatorThread
和Scheduler
最复杂,而且由于第七次作业中加入了换乘,我在乘客发出请求时预处理判断是否需要换乘以及找出换乘楼层,所以Person
也有点复杂。
优点:生产者-消费者模型,易于实现,简单易懂。
缺点:ElevatorThread
有点臃肿,其实有些不需要的方法可以删掉。
四、分析自己bug
第六次作业互测中出现bug,特征为RTLE
,问题所在类和方法为Elevator.run()
,原因为当多个电梯争抢一个乘客时,这个乘客在电梯到达的一瞬间选择进了一个电梯,因为先前已经判断了是否还有乘客,所以其他电梯此时还在等待乘客的进入,所以会TLE
。更改了Elevator中run方法,让其在循环empty时判断是否还有乘客,若无乘客,就退出运行。
五、他人bug
会将自己错误的例子交上去测一测,也会自己构造一些特殊的例子,也用到了评测机。有效性:自己错的别人确实也有很大概率错,特殊的例子也能刀到人,评测机却刀不到人,是因为随机生成的数据不够特殊,还是评测部分写错了?
本单元的测试策略与第一单元的测试策略的差异就在于写了完整的评测机,虽然hack别人似乎没什么用处,但是又get了新知识,测自己也不错。
六、心得体会
经第一单元的教训,我从第五次作业开始就努力使自己面向对象设计,终于,在这一单元,重构的故事并未发生在我身上,第六次、第七次作业也挺轻松的,没有需要更改很多地方,所以面向对象设计的原则一定要好好遵守。
在第一单元,我就曾遇到线程安全方面的问题,这一单元,作业更是由单线程变为多线程,其实,只要线程之间不要互相调用,以及将共享对象管好,就成功了一大半。