OO第二单元总结
程序结构分析
-
第一次作业
Type Name MethodName LOC CC PC Elevator Elevator 7 1 1 Elevator move 10 1 0 Elevator on_off 21 7 0 Elevator hasOff 8 3 0 Elevator open 9 1 0 Elevator close 9 1 0 Elevator getPrior 9 3 0 Elevator init 11 3 0 Elevator run 12 4 0 Input Input 3 1 1 Input run 18 3 0 MainClass main 8 1 1 Queue Queue 4 1 0 Queue isEmpty 3 1 0 Queue isStop 3 1 0 Queue setStop 3 1 1 Queue add 4 1 1 Queue remove 3 1 1 Queue getFirst 11 2 0 Queue hasRequest 8 3 2 Queue needGo 17 6 2 Queue get 20 4 2 Request Request 11 2 3 Request getFrom 3 1 0 Request getTo 3 1 0 Request getId 3 1 0 Request getDirection 3 1 0 Type Name NOF NOPF NOM NOPM LOC WMC NC DIT LCOM FANIN FANOUT Elevator 5 0 9 9 103 24 0 0 0 0 0 Input 1 0 2 2 24 4 0 0 0 0 0 MainClass 0 0 1 1 10 1 0 0 -1 0 0 Queue 2 0 10 10 80 21 0 0 0 0 0 Request 4 0 5 5 29 6 0 0 0 0 0 从设计架构和度量分析中可以看出,
Elevator
类的on_off
方法复杂度较高,Queue
类的needGo
与get
方法复杂度较高,可以看出,对于第一次作业来说,最重要的是处理好电梯的运行功能正确,尤其是电梯上下人处,容易出现问题;而needGo
方法则与我使用的电梯运行策略有关,在处理好电梯运行功能正确后,提高电梯性能。
从时序图可以看出,第一次作业就是一个简单的生产者-消费者模型,
Input
类充当生产者,像共享对象Queue
中投放乘客请求,Elevator
为消费者,不断拿走请求将乘客运送到指定楼层。
整体来说第一次作业比较容易,处理好共享对象Queue
的安全访问,选择实现一个性能不错的运行策略就可以了。 -
第二次作业
Type Name MethodName LOC CC PC Elevator Elevator 10 1 2 Elevator getPlace 3 1 0 Elevator getDir 3 1 0 Elevator getDes 3 1 0 Elevator move 10 1 0 Elevator on_off 24 7 0 Elevator hasOff 8 3 0 Elevator open 9 1 0 Elevator close 9 1 0 Elevator getPrior 9 3 0 Elevator init 16 4 0 Elevator run 14 5 0 MainClass main 7 1 1 Queue Queue 4 1 0 Queue getSize 3 1 0 Queue isEmpty 3 1 0 Queue isStop 3 1 0 Queue setStop 4 1 1 Queue add 4 1 1 Queue getFirst 14 3 0 Queue hasRequest 8 3 2 Queue needGo 17 6 2 Queue get 25 5 4 Request Request 11 2 3 Request getFrom 3 1 0 Request getTo 3 1 0 Request getId 3 1 0 Request getDirection 3 1 0 Schedule Schedule 10 2 2 Schedule calculate 44 10 4 Schedule run 38 9 0 Type Name NOF NOPF NOM NOPM LOC WMC NC DIT LCOM FANIN FANOUT Elevator 8 0 12 12 128 29 0 0 0 0 0 MainClass 0 0 1 1 9 1 0 0 -1 0 0 Queue 2 0 10 10 89 23 0 0 0 0 0 Request 4 0 5 5 29 6 0 0 0 0 0 Schedule 3 0 3 3 97 21 0 0 0.666667 0 0 第二次作业在第一次的基础上增加了多部电梯,因此设计一个调度器是十分必要的。此时调度器的调度和电梯的运行策略同等重要。可以看出,除了电梯运行相关的方法复杂度较高外,
Schedule
类的calculate
与run
的复杂度更高,这个类的代码量比较高。这次作业中,我才用了优先选择距离较近的电梯优先分配请求,相同距离则比较电梯分配的请求数,请求数少的优先。对应的是Schedule
的calculate
方法。在强测中,这一调度方法配合单部电梯的LOOK算法获得了较好的性能。
从时序图中可以看出,第二次作业的设计模型与第一次略有不同。对每部电梯来说依然是生产者-消费者模型,但整体上来说是Worker-Thread模型。
Schedule
获得输入的请求,处理之后发送给对应Elevator_i
的Queue_i
中,Elevator_i
再从中执行请求。在第一次作业的基础上,第二次作业采用这样的设计模式非常容易,只需要增加电梯和对应的队列即可,不用对第一次作业已采用的模式进行大的调整,整个模式非常清楚,不易出错。 -
第三次作业
Type Name MethodName LOC CC PC EleA EleA 3 1 3 EleB EleB 3 1 3 EleC EleC 3 1 3 Elevator Elevator 13 1 6 Elevator getPlace 3 1 0 Elevator getDir 3 1 0 Elevator getDes 3 1 0 Elevator getRuntime 3 1 0 Elevator getname 3 1 0 Elevator inRange 8 3 1 Elevator move 12 2 0 Elevator on_off 34 10 0 Elevator hasOff 8 3 0 Elevator open 9 1 0 Elevator close 9 1 0 Elevator getPrior 9 3 0 Elevator init 16 4 0 Elevator run 16 6 0 MainClass main 7 1 1 Prequest Prequest 12 2 3 Prequest getStatus 3 1 0 Prequest setStatus 3 1 1 Prequest getFrom 3 1 0 Prequest getTo 3 1 0 Prequest getId 3 1 0 Prequest getDirection 3 1 0 Queue Queue 4 1 0 Queue getSize 3 1 0 Queue isEmpty 3 1 0 Queue isStop 3 1 0 Queue setStop 4 1 1 Queue add 4 1 1 Queue getFirst 14 3 0 Queue hasRequest 8 3 2 Queue needGo 17 6 2 Queue get 25 5 4 Schedule Schedule 10 2 2 Schedule construct 12 4 4 Schedule contain 8 3 2 Schedule inRange 12 4 2 Schedule findMid 28 9 4 Schedule makePrequest 30 7 1 Schedule produce 10 3 1 Schedule distribute 19 5 2 Schedule calculate 44 10 4 Schedule finish 12 4 0 Schedule rest 14 5 0 Schedule manage 15 5 0 Schedule run 53 11 0 Type Name NOF NOPF NOM NOPM LOC WMC NC DIT LCOM FANIN FANOUT EleA 1 0 1 1 6 1 0 0 0 0 0 EleB 1 0 1 1 6 1 0 0 0 0 0 EleC 1 0 1 1 6 1 0 0 0 0 0 Elevator 12 0 15 15 163 39 0 0 0 0 0 MainClass 0 0 1 1 9 1 0 0 -1 0 0 Prequest 5 0 7 7 37 8 0 0 0 0 0 Queue 2 0 10 10 89 23 0 0 0 0 0 Schedule 11 0 13 13 280 72 0 0 0.461538 0 0 第三次作业相比第二次增加了不同电梯类型和换乘。相应的设计也更加复杂,尤其是
Scheduler
和Elevator
类,增加了很多成员变量和方法。以至于Scheduler
类的设计十分臃肿,方法复杂度也很高,可维护性并不很好。因此在完成这次作业的过程中,我认真地考虑过tasks
这一容器是否需要单独设计一个类来进行管理,这主要是受课程实验的影响,即设计一个类似于票池的容器,这样能减轻Scheduler
的负担。但最后我并没有采用这样的设计,一方面是基于现有架构进行补充,最方便的还是放在Schedule
类中管理;另一方面,考虑到除了乘客请求,还有新增电梯的请求,而tasks
是只存放乘客请求的容器,分离出去不便管理,可能会有线程安全的隐患。
此外,分别设计EleA
,EleB
和EleC
三个电梯子类实际上没有这个必要,只需要在电梯类中增加type
变量和相应的判断即可。但事实上,最好的设计应当是设计一个子调度器,用来接受请求并指挥电梯运行,电梯只管理上下运行,并不直接和主调度器进行通讯。这样就可以分担电梯和主调度器的工作负担,设计也不会那么臃肿,会更有层次感,可维护性也更好。我这样的设计主要还是受第一次作业的影响,在第一次作业中我并没有设计调度器管理电梯的运行,而是将电梯运行具体实现直接集成在了
Elevator
中。说明第一次的设计结构还是十分关键的。
为了应对换乘情况,我的思路是对于需要换乘的请求进行拆分,按执行顺序加入到一个
task
的容器里,再将task
加入到tasks
容器里,无需拆分的请求则直接加入tasks
容器中,并将请求状态设为0,即未分配状态;接着扫描tasks
,找到未分配且可以分配的请求,逐一按照第二次作业的思路进行分配,然后将请求状态设为1,即已分配;电梯执行完请求后,将对应请求的状态设为2,即已完成。当所有任务的状态均为已完成且输入结束后,通知电梯结束。相比于第二次作业,第三次就是增加了一个主请求队列
tasks
来管理请求,整体结构没有太大的改动,模式设计还算是差强人意。只是如前文所述,能增加子调度器,使得设计更有层次感,任务分工更加明确具体就更好了。 -
总结
相比于第一单元惨不忍睹的架构,这一单元的设计明显容易了不少。得益于第一次第二次作业的良好的设计,没有出现第一单元次次重构的惨状。总体来说写得也比第一单元更轻松。除了了解了很多优秀的设计模式外,这也让我深刻认识了架构的重要性。下一单元继续努力。
bug分析
-
第一次作业
第一次作业在完成过程中主要遇到的是电梯结束的问题,电梯在执行完请求后处在等待状态而没有被唤醒,在停止信号设置后增加一个
notifyall
就可以解决问题。在互测过程中我并没有测出其他人的bug,第一次作业整体设计也还算是不错的。
-
第二次作业
第二次作业在第一次的基础上没有做太多改动,无论是线程安全还是电梯功能都没有出问题。
互测中也没有发现其他人的bug,整体都是很好的。
-
第三次作业
第三次作业出现了ctle的情况,在
Scheduler
分配请求的过程中出现了轮询,在没有可分配的请求时没有让出cpu进入等待状态,增加了rest
函数判断后解决了这一问题。值得一提的是,在解决这一问题的过程中,经dalao推荐,我使用了开源工具visualVM来监视cpu的运行状况,成功解决了轮询的问题,在这里推荐这一工具。在互测过程中,我发现了一位同学的代码仅能支持新增电梯id为X1X2X3的bug。后来发现此类设计并不仅是个案,可能是对指导书的理解有误差。此外并无其他问题。
心得体会
整体来说三次作业完成状况都是不错的,强测全通过互测也没有被hack,这得益于简单有效的设计模式,当然电梯作业的整体难度也并没有第一单元高。采取一个合适的模式,对于保证线程安全和电梯功能正确性能良好都有着重要作用。
在这一单元,我了解了java的多线程设计和线程安全问题,还认识了生产者-消费者、Worker-Thread、订阅-发布等设计模式,让我明白了优秀的设计架构带来的高效的编码等诸多好处。下一单元继续努力,学习体会更多有用的设计。