Fate/Beihang OO——第二单元总结
如果说第一次的OO战场还充满刺激与青春活力,那这一次除了遗憾就是悔意,还有无数无法弥补的失败,获得的大概就只有些许美名其曰“教训”的失败者的挣扎吧。
一、三次作业的设计架构
在这次电梯的作业中,我成功摆脱了第一单元重构三次的窘态,成功实现三次迭代,没有重构,但这也间接导致了第三次作业翻车。先不说结果,能实现迭代确实让我比较有成就感,也偷了不少懒最终酿成悲剧。
三次作业的架构基本没有区别,都是类似生产者-消费者模式的架构,输入线程作为生产者提供请求,而电梯线程作为消费者完成请求。我并没有将调度器作为单独的一个线程,而是将其作为其他线程的功能来实现的。
1.第一次作业
在这一次作业中定下了架构的大趋势。
总体而言,我用了Main, Input, Elevator, Controller四类,但其实Controller是完全可以和Elevator类合在一起的,这个类没有和其他类交互,独立出来只是徒增写法的复杂。
大致运作流程是这样的:Input类获取请求,然后调用Controller类的方法将请求加入等待队列(如之前所说,其实把Controller类整个放进Elevator类会少很多麻烦),同时会对Elevator类的mainLock锁唤醒,而Elevator类的run方法主要就是循环判断是否满足空闲条件,若满足则mainLock锁wait等待唤醒,否则则获取一个主请求并运作。
单电梯调度策略:(后两次作业中单电梯调度策略基本沿用这个)若有主请求则往主请求目标楼层运作,途中若有人下则开门,若有人上则判断其方向是否相同,相同才捎带;当没有主请求时,若等待队列有人,则选取From离电梯最远的请求作为主请求,若等待队列无人、电梯内有人,同样选取To离电梯最远的作为主请求,若均无人则进入wait。
当Input线程停止时,同样会唤醒waitLock,同时将一个static变量end置true,而电梯停止的标志则是等待序列、电梯内均无人且end为true。
在mainLock之外,因为当时对多线程“锁”知识的匮乏,Elevator的好多方法都被我上了一个this的锁,这直接导致第三次作业暴死。
2.第二次作业
这一次增加了人数限制和多电梯。
比起第一次作业,改动主要如下:
将Controller类整个集成到了Elevator类里,即每架电梯自己决定自己如何运作并独享一个等待队列,除了上人的时候增加人数判断,其他调度策略与第一次相同。同时在Input类里增加电梯队列保存所有电梯,同时增加总调度器,采用平均分配调度法(说白了就是没有调度策略,毕竟第一周优化死怕了),也即每一个请求依次加入每架电梯的等待序列中。
另外,值得一提的是,因为楼层涉及到了-1到1不经过0,而我并不想特判,故我自己写了一个Request类来代替原有的PersonRequest类,将楼层简化成了1-19,只在输入输出时进行处理保证正确性。
(结果这个类名在第三次作业被用了,实属汗颜)
3.第三次作业
这一次增加了动态增加电梯功能、电梯楼层限制和电梯种类。
其中电梯种类没什么好叙述的,这里先放下不表。动态增加电梯也只是改造了一下Input,在Input中增加了方法往电梯序列增加电梯并运行。
比较大的改动是关于楼层限制的问题,这让我不得不考虑换乘的问题。秉承舍弃性能原则,我同样采用了一个比较安全简单的调度方式,同时把Input中的总调度方法重写。
首先,我在自己写的Request(被官方逼得改名MyRequest了)中增加了isChange变量标志其是否为需要换乘的请求,同时在里面增加子请求对象,同时保存换乘后将子请求加入电梯的下标。也就是说,我将一个需要换乘(如换乘方法是floor1-ele1-floor2-ele2-floor3)的请求拆分成两个请求:floor1-floor2,floor2-floor3,同时在总调度阶段计算出它的主请求和子请求分别将利用哪个电梯。因为不涉及电梯的删除,故用下标保存电梯是安全合理的。
总调度稍微详细的算法如下:首先遍历电梯(不是从0遍历,而是从上次遍历结束的点开始,以尽量多利用一点电梯),若有能直达的,则直接塞入那个电梯的等待序列,否则依次找到入口电梯ele1和出口电梯ele2,接着在入口电梯和出口电梯中找到一层共同楼层作为floor2,然后将floor2-floor3作为子请求加入主请求floor1-floor2中,将主请求置为换乘,最后加入ele1的等待序列。
同时,改变了结束电梯的策略——Input结束且所有人接送完毕后才结束电梯。
二、第三次作业可扩展性分析
在这个板块中,我将主要按SOLID来分析自己的第三次作业
1.SRP原则
我认为自己的电梯从方法上说,在“各司其职”方面还做得挺不错,每个功能都分得比较开,包括主调度方法都是拆成数个部分进行,稍有不足的是Elevator类的run方法,我直接将电梯的运行全过程写在了里面,其实应该解耦合、拆分成更多方法的。
但是在类的方面,我做得不是很好,但是也算是“各司其职”,毕竟都只负责了自己分内的工作,只是因为将调度器集成所以显得比较冗余。
2.OCP原则
这一点我也做得不错,如之前所述,我成功做到了三次迭代,比如单电梯的调度一直用到了最后。
从这里来看,即使增加要求,至少在大部分功能上大概率也是能继续迭代而不至于重构的。
3.LSP原则
这一次我并没有用到任何继承(Thread类除外),所以不太好评价这一点。按照第一单元的写法,可能大概率无法满足要求。
4.ISP原则
本次完全没有达成这个要求。不过我认为这一次的作业也没有必要使用接口建立抽象层次。
5.DIP原则
本次完全没有考虑这一点,一个原因是根本没有用到任何依赖关系吧。
三、基于度量分析自己的程序结构
考虑到三次迭代之故,这里就只分析第三次作业的架构。
类和方法相关度量
class | OCavg | WMC | CSOA | LOC |
---|---|---|---|---|
Elevator | 125.0 | 274.0 | 2.41 | 53.0 |
Input | 99.0 | 165.0 | 4.375 | 35.0 |
Main | 23.0 | 42.0 | 1.6 | 8.0 |
MyRequest | 29.0 | 52.0 | 1.09 | 12.0 |
Total | 276.0 | 533.0 | 108.0 | |
Average | 69.0 | 133.25 | 2.35 | 27.0 |
method | ev(G) | iv(G) | v(G) | CONTROL | LOC |
---|---|---|---|---|---|
Elevator.arrive() | 2.0 | 1.0 | 2.0 | 11.0 | 2.0 |
Elevator.closeEle() | 1.0 | 1.0 | 1.0 | 6.0 | 1.0 |
Elevator.down() | 1.0 | 1.0 | 2.0 | 6.0 | 2.0 |
Elevator.Elevator(String,String) | 9.0 | 2.0 | 2.0 | 22.0 | 5.0 |
Elevator.getFloor() | 0.0 | 1.0 | 1.0 | 3.0 | 1.0 |
Elevator.getMain() | 3.0 | 2.0 | 3.0 | 17.0 | 4.0 |
Elevator.getMainLock() | 0.0 | 1.0 | 1.0 | 3.0 | 1.0 |
Elevator.getMaxFloor() | 0.0 | 1.0 | 1.0 | 3.0 | 1.0 |
Elevator.getMinFloor() | 0.0 | 1.0 | 1.0 | 3.0 | 1.0 |
Elevator.getTo() | 0.0 | 1.0 | 1.0 | 3.0 | 1.0 |
Elevator.getWaitLock() | 0.0 | 1.0 | 1.0 | 3.0 | 1.0 |
Elevator.ifOut() | 4.0 | 5.0 | 3.0 | 13.0 | 5.0 |
Elevator.inPerson() | 5.0 | 1.0 | 6.0 | 26.0 | 7.0 |
Elevator.justOpen() | 1.0 | 1.0 | 2.0 | 9.0 | 2.0 |
Elevator.openEle() | 1.0 | 1.0 | 1.0 | 6.0 | 1.0 |
Elevator.outMain() | 2.0 | 1.0 | 3.0 | 14.0 | 3.0 |
Elevator.outPerson() | 5.0 | 1.0 | 4.0 | 21.0 | 4.0 |
Elevator.personIn(MyRequest) | 0.0 | 1.0 | 1.0 | 3.0 | 1.0 |
Elevator.run() | 13.0 | 1.0 | 18.0 | 51.0 | 19.0 |
Elevator.toMain(int) | 2.0 | 1.0 | 3.0 | 14.0 | 3.0 |
Elevator.up() | 1.0 | 1.0 | 2.0 | 6.0 | 2.0 |
Elevator.waitIn(MyRequest) | 1.0 | 1.0 | 1.0 | 6.0 | 1.0 |
Input.endWait() | 2.0 | 1.0 | 2.0 | 7.0 | 2.0 |
Input.findChange(int,int,int,int) | 13.0 | 14.0 | 2.0 | 39.0 | 20.0 |
Input.getEnd() | 0.0 | 1.0 | 1.0 | 3.0 | 1.0 |
Input.getN() | 0.0 | 1.0 | 1.0 | 3.0 | 1.0 |
Input.ifNon(int,int) | 2.0 | 3.0 | 3.0 | 11.0 | 4.0 |
Input.Input() | 0.0 | 1.0 | 1.0 | 2.0 | 1.0 |
Input.inWait(MyRequest) | 10.0 | 6.0 | 5.0 | 42.0 | 7.0 |
Input.run() | 9.0 | 3.0 | 9.0 | 48.0 | 9.0 |
Main.Floor(int) | 2.0 | 3.0 | 1.0 | 8.0 | 5.0 |
Main.ifComplete() | 1.0 | 1.0 | 1.0 | 5.0 | 1.0 |
Main.inAdd() | 1.0 | 1.0 | 1.0 | 5.0 | 1.0 |
Main.main(String[]) | 0.0 | 1.0 | 1.0 | 8.0 | 1.0 |
Main.outAdd() | 2.0 | 1.0 | 3.0 | 8.0 | 3.0 |
MyRequest.addChange(MyRequest,Elevator) | 0.0 | 1.0 | 1.0 | 5.0 | 1.0 |
MyRequest.change(int) | 1.0 | 2.0 | 1.0 | 7.0 | 2.0 |
MyRequest.getChange() | 0.0 | 1.0 | 1.0 | 3.0 | 1.0 |
MyRequest.getFromFloor() | 0.0 | 1.0 | 1.0 | 3.0 | 1.0 |
MyRequest.getId() | 0.0 | 1.0 | 1.0 | 3.0 | 1.0 |
MyRequest.getNextTo() | 0.0 | 1.0 | 1.0 | 3.0 | 1.0 |
MyRequest.getToFloor() | 0.0 | 1.0 | 1.0 | 3.0 | 1.0 |
MyRequest.ifChange() | 0.0 | 1.0 | 1.0 | 3.0 | 1.0 |
MyRequest.MyRequest() | 0.0 | 1.0 | 1.0 | 2.0 | 1.0 |
MyRequest.MyRequest(int,int,int) | 0.0 | 1.0 | 1.0 | 6.0 | 1.0 |
MyRequest.MyRequest(PersonRequest) | 0.0 | 1.0 | 1.0 | 6.0 | 1.0 |
Total | 94.0 | 77.0 | 102.0 | 482.0 | 136.0 |
Average | 2.04 | 1.67 | 2.22 | 10.48 | 2.96 |
如之前所述,因为将总调度器和分调度器分别集成在了Input类和Elevator类里,他们承担了太多生命不该承受之重。
同样如之前所述,Elevator的run方法有点冗杂。
UML类图
从这里也可以看出,我的Elevator类和Input类真的很臃肿,但我感觉将调度器独立出去也比较别扭,可现在看来,那样做可能确实要舒服一些。
四、分析自己程序的BUG
本次作业我根本没有测试很多点,因为能力不足、怠惰之故,我没有写投放机,故我只能手打数据,再加上本就忙碌,我基本只测试了依次输入的点,且没有进行抗压测试,而我的BUG是即使不分批次投入,只要一次性投入大量数据就能找到的,这里有态度问题,我反省。
这次的BUG全出在锁上,真是多线程传统艺能。
1.第一次作业
这次作业可以说没有BUG,但是却没有通过中测进阶,这是一个失误引起的——电梯结束条件中括号打偏了。
强测AC91分,中测没过,真是讽刺。
- 本来是过了的,后来改了checkstyle重新提交就RTLE了一个点,我想当然地没有注意,直到周六晚21: 50才发现,当时脑子一热,以为是5minCD,于是回档提交。在21: 55 找到BUG,却发现正在CD,真的无力回天。
2.第二次作业
这次作业在强测中被干掉了一个点,但是在重新提交程序后就靠多线程的玄学性通过了,我也没有细想哪里出了问题,这直接导致第三次大翻车。
因为我在Elevator类乱加this锁的缘故,若在Input类(或其他类)向Elevator投放请求的同时电梯正在试图进行其他操作,则可能造成this锁和mainLock锁死锁错误,这在第二次作业很难出现,但在第三次复杂情况下直接导致强测爆0。
3.第三次作业
如第二次作业所述,乱加锁要不得啊。
除此之外,因为提到了output线程不安全故给output加了个锁,却鬼使神差地在电梯上下行sleep时也加上了这个锁,直接导致DEBUG阶段没有死锁却还是可能RTLE。
五、Hack策略
没有hack策略
本次的圣杯战争没我什么事,三次作业只有第二次进了互测,第二次互测全房加起来就hack了一个点(活跃分给你扬咯),而我因为没有写投放机,即使有针对性的输入数据也没法得出标准输出。手打了两组数据后坚持不下去了,找同学要了两组数据扔上去,没有hack到人。
假设要我hack人呢?我可能主要会从以下几个角度分析:
- 抗压测试,疯狂投入大量数据
- 换乘测试,换乘接换乘
- 满员状态下的加人、换乘测试
- 线程安全问题:比如在电梯空闲很久过后冷不丁加请求
与第一单元相比,这次主要可以针对多线程的不安全性进行hack,在正确性方面则是类似的。
六、心得与体会
- 线程安全方面:
- 锁是个好东西,但绝对不能乱加,加了锁之后最好多想一想、理一理各种方法中的调用关系、用锁的情况,以避免出现意外情况。
- 不能乱加的同时,也不能少加,共用对象的时候一定要看有无可能同时有其他方法或类调度同样的对象,若有则需要加锁。
- 设计原则
- 正确性第一,没有能力不要乱优化就完事了。
- 其他
- 多测试,不要怕麻烦,不会写就去问同学,多多交流。
- 在这次作业中,我也获得了很多,学习了多线程这种东西,同时端正自己的态度,即使在家,也绝不能继续怠惰下去了。