OO_UNIT2_SUMMARY
18375166 程佳伟
写在开头的话
时间过得真快,OO第二单元的三周就这么快的过去了,我也成功地挺过了传说中的“魔鬼”电梯这关。
总体而言,我认为相较于第一单元的作业,第二单元无论是在完成的时间和完成的质量都有明显的提高,而且hack到的bug也数倍于第一单元的,不过作为一个正常的人,还是难逃真香定律:自己在第一单元的立下的搭自动测评机的flag倒了!!!(虽然明知道测评机对自己的检查和hack别人的bug都很香),所以在第二单元总结的开头再次立下flag,第三单元一定一定一定(重说三)要搭自动测评机
设计策略
HW1
第一次作业是可稍带电梯,我采取的主要架构是生产者-消费者设计模式,inputService是生产者线程,Lift是消费者线程,共享对象是Controller,Controller里维护请求队列queue,inputService通过addRequest向Controller的队列中添加请求;Lift在没有设置主请求时在通过takeRequest取走主请求,在有主请求时每经过一层楼,都会通过主动调用checkIn从Controller取走能够捎带的Request。
针对可稍带电梯我的做法是,lift从controller中取出request后会将其保存在自身的一个list,每次takeRequest或者经过一层楼checkIn后,会遍历新取得的请求,更新自己的运行的目标楼层和运行方向,同时自身还维护一个inf类,用来表示lift的当前楼层,目标楼层,运行方向,已有乘客数,每次经过一层楼checkIn或者takeRequest后,会自动更新Inf,每次checkIn时会将作为参数Inf传入,用作判断可稍带的条件。
当inputService输入结束时会通过setIsInputWork告知Controller,当Controller中queue中无请求且已经被通知生产者线程已经结束了时,会通知lift:当lift的所有请求处理完时自行结束线程。
以上addRequest、takeRequest、setIsInputWork、checkIn都是synchronized的同步控制方法,如果takeRequest时queue为空,会wait,addRequest和setIsInputWork会notifyAll。
HW2
第二次作业是多部可稍带电梯,于是我的第二次作业在第一次的基础上增加了Dispatcher类,dispatcher时是线程类,含有请求队列queue,inputService通过addRequest向dispatcher添加请求,由于第二次作业的电梯数不为1,因此,每个lift都有一个自己的controller,当dispatcher的queue不为空并且inputService没有结束时,调用dispatch方法向各个电梯的controller添加请求。
针对第二次电梯的性能,我的做法是保存lift向controller传递的inf,这个inf即为上一次lift与controller交互的运行信息,以此为判断标准,当dispatcher向某个lift的controller添加请求时,controller会筛选与自身运行条件相符的请求(例如方向一致,保证不超载等)加入controller的queue,然后将不符合的请求反馈给dispatcher,让这些请求继续分配个下一个lift的controller,如果所有的controller都筛选过后仍有剩余,则随机强制分配给某个controller,达到平均分配的效果。
当inputService输入结束时会通过setIsInputWork告知dispathcer,通过dispatcher为跳板转而用setIsInputWork通知所有controller,然后当dispatcherqueue为空且输入结束时会自行结束线程,当Controller中queue中无请求且已经被通知生产者线程已经结束了时,会通知lift:当lift的所有请求处理完时自行结束线程。
以上addRequest、takeRequest、setIsInputWork、checkIn、dispatch都是synchronized的同步控制方法,如果takeRequest时queue为空,会wait,如果dispatch时queue为空且输入线程未结束时,会wait。addRequest和setIsInputWork会notifyAll。
HW3
第三次作业对基于电梯可到达的楼层和负载能力分了三类,有些电梯只能去特定的楼层,我的第三次作业没有添加新的类,在dispatcher类和Person类即请求中做了改动,Person类增加了transfer、tempdst、tempdirec属性,用于表示是否需要换乘,换乘时的临时目的地和临时方向,dispatch的queue增加到了三个,分别对应三种电梯,然后inputService输入时根据请求的种类添加到不同的queue里去,dispatch分配时也是每个queue分配对应类的controller,交互上的改动是电梯可以将换乘的请求也通过addReuquestFromLift添加给dispatcher。其余的与第二次作业相同。
针对换乘,我的策略时保证每个请求一次换乘即可到达其目的地,首先dispatch会基于请求的出发地和目的地分配到不同的queue里即分配给不同的电梯去完成请求,之后当电梯取走了某个换乘请求时会自动根据电梯的类别、运行方向、当前楼层,此时其transfer为true,这个请求的会将tempdst更新到离自己目的地最近的且当前所搭载的电梯可到达的换乘站,并随之更新tempdirec,待其请求到达换乘站后,将请求的出发地、目的地、方向、transfer相应的更改后又放回dispatcher。
以上新增的addRequestFromLift是synchronized的同步方法。
可扩展性分析
SOLID
S:首先我觉得第三次作业的各个类分工职责还是很明确的,职责也比较单一,dispatcher负责分派任务,充当了托盘与生产者之间的传递者,lift-controller-inputService是生产者-托盘-消费者。(除了让Main类管理了电梯的分类数据)
O:其次扩展功能应该也可以不经过修改已有的函数来实现,例如删除电梯、乘客更改目的地,这两者都可以增加一个EmergencyHandler来实现,前者如果获得到删除电梯的请求,这个类就向对应的controller发出信号,controller再向电梯发出信号,然后电梯设置一个最终疏散层,接着将遗留的乘客疏散到这层后,将请求信息跟新后返回给dispatcher就可以了。后者则分三种情况考虑,EmergencyHandler通过向所有lift发信号,如果此人在电梯内,则更新电梯的运行信息,如果还不在电梯,则如果还未上电梯,则跟新这个请求信息即可,否则将一个新的请求加入到dispatcher即可。
L和I:第三,但是这次作业既没有实现接口,也没有继承关系,这点值得改进,比如三次作业电梯之间就可以实现继承关系。
总而言之,这次作业的可扩展性还是有待提高。
程序机构分析
HW1
代码量
类图
复杂度
这次作业没有超过预警的方法,所以就把平均值放上来吧。
方法和属性数
HW2
代码量
类图
第二次和第三次作业的类图中的方法太多了,一个图片放不下,所以就只保留了类和彼此之间的关系。
复杂度
第二次作业的复杂度有四个超过了我也是没想到
renewUserInf是更新电梯的最新信息,addRequest方法复杂确实是用更新的信息来判断捎带的所有情况,所以可能有些复杂,其实应该可以再多创建一些函数用于分工合作,降低耦合。
selectMainUser是在已有的controller队列里找到最好的主请求,可能也是涉及的情况有些多。
方法和属性数
HW3
代码量
类图
类图的优缺点在第三次作业统一给出:
优点:
分工明确,逻辑清晰,而且线程信息交流上没有出现死锁的问题
缺点:
person类和route类管理的数据有一部分是重合的
复杂度
哇!第三次作业的复杂性真是螺旋爆炸。
这里的有几个函数如addRequest、renewUserIn和selectMainUser在之前已经提到,故不再赘述。
findBestQueue和findBestTransPos是用于判断某个请求最适合哪个类型的电梯来运送和寻找最适合的中转站,考虑的情况可能太多了。
dispatch是用了6个循环来对队列中的请求进行分配,因为有三个队列,所以我之前提到的那种分配模式就要三次。。。,所以稍微复杂了些。
inAndOff是集合了所有的getIn,getOff,open,close为一体,以及由于有些楼层可能无人上无人下,无须开关门,所以还得有这些判断,因此十分复杂。
方法和属性数
sequence diagram
鉴于三次作业都是迭代开发,所以方法几乎没有什么修改,所以所有方法的代码行数、控制分支数在最后统一给出。
My Bugs
这三次作业,在所有强测和互测时仅在第六次作业的强测里有一次RTLE,而且后来我没有做任何的修改,直接预览这个点又ac了,后来经过多次预览发现并不是每次都能出现rtle,大概十几次里出现了两次,并且第七次作业我的架构未做任何更改强测全ac,之后bug修复阶段我又预览了大概10次左右,所有点都是ac没有出现rtle。
针对这个bug,我首先分析了stdout,发现输出非常奇怪,有两段输出之间竟然隔了60s左右,
然后我分析了自己的会不会有死锁问题,我的几个类的线程程通信情况如下:inputservice读到请求就将其加入dispatcher的queue中,dispatcher不断地dispatch把queue中地请求分配给controller的queue,如果dispatcher的queue为空且输入未结束就wait,lift不断运行从controller中取走queue中的请求,如果queue为空且输入不为空则wait,所以controller的锁只有lift和dispatcher能拿到,dispatcher的锁只有其自身和inputService能拿到,所以可能死锁住的位置可能在controller和dispatcher。
之后我在自己的代码通过加入System.err.println来获得一些debug信息,再提交给评测机,更奇怪的现象发生了:仿佛我的inputService并没有获得评测机的输入!
如图898是倒数第二个请求,如果我们得到了倒数一个请求,肯定会有一条输出“inputService receive XXX‘s的请求,但是并没有,然后所有线程都在等待中直到rRTLE,因为inputService没有得到最后的ctrl+D来发出结束输出的信号,所以所有线程都不会结束。
然后这个问题我和我的两个出现同样问题的室友讨论了很久也没有得出结论,所以至今我都没有找出这个bug的原因,只好在此分享出来求大佬指教。
隔了n久才输出下一条
有关的代码
System.err.println的信息
Hack Bugs
这次我没有稳住上一次立的flag:搭测评机,但是我这次hack bug的数量也不少。
我每次会将弱测给的测试样例保存下来拼接在一起,将重复的id改一下,三次作业结束我构造了一个长达200多条请求的数据,每次hack的时候就从这常常的数据中截取50条去测试,效果很好,但是这样的随机测试很容易hack到的是同一个bug,不过这样的超长数据的bug有一定概率hack到死锁的bug。
所以我有根据指导书构造了一些极端测试点,比如第七次作业,根据3楼只有C类电梯能到到的情况,构造了从三楼到所有楼层和所有楼层到三楼的测试点,这样的极端测试点也成功地hack到了不少bug。
针对线程安全性问题,而且在第六次作业中,我构造了一个测试点:设初始有5个电梯,但是我只输入一个请求,这样有可能别人未处理好,会出现多个电梯未关闭现成的问题。
这单元的测试策略和第一单元很不相同,由于线程调度的随机性和每个人的设计不同,并且这是一个动态的过程,因此输出的答案不唯一,并且即使找到了bug也很难复现,对我们的debug过程也构成了挑战,除此之外线程安全也是这次作业的一个大问题。所以我们的测试策略除了随机性测试和针对边缘数据的数据外,还应额外注意线程安全的测试。
总而言之,这次互测阶段收获满满,不过下一单元作业自己能够搭一个测评机就更好了。
心得体会
这次作业我总的设计模式是基于生产者-消费者模式,线程安全问题在一开始上手时感觉很困难,尤其是第5次作业自己测试时因为死锁困扰了很久,后来尝试jprofiler也效果不太好,后来经过在一些关键地方加入了一些println语句,还是顺利过了这关,设计的类也基本满足了高内聚低耦合的原则,总的来说,这次作业我对自己的进步很满意。
此外还有几点我说说:
1、自己每次作业动手前经过充分的构思,所以这三次迭代作业没有进行重构,完成的也很顺利,质量较上次作业进步了很多,这个好习惯希望继续保持下去!。
2、关于这次作业的正确性检查我还有一点小心得,就是在inputService中用一个arraylist存下每个请求,每个请求都有一个status用于判断此请求是否登上电梯或者是否到达目的地,lift每次getIn和getOff就对请求的status进行设置,最后结束时,遍历arraylist里的请求就可以检查请求是否都到达其目的地,这样可以检测自己是否将客人都送到了,而且效果还不错。
3、这次作业的死锁也是一个重点,而以往的调试方法并不适用于这次作业,讨论区的同学们也分享了很多方法,而对于我来说,我的好方法是在一些关键位置输出一些System.err.println(debug信息),比如wait前后,这样就可以判断这个线程是否在wait,用来检查死锁很有帮助,同时stderr信息评测机会过滤掉,并不会影响测试。
写在最后的话
OO这门课程虽然工程量大难度也不小,但是经过这几次作业的训练,值得庆幸的是,自己终于感觉到有几分开始上手的感觉了,下个单元继续加油吧!(别忘了搭评测机!!!)