OO第二单元总结
设计结构
第五次作业
这次作业是单个电梯的调度,较为简单。我除主类外设置了三个类:电梯类、控制器类、请求(读取)类,其中后两个类继承Thread,Control负责以look算法控制电梯运行,Request读取请求并保存请求队列。
第六次作业
本次作业增加了电梯数量的指定,和电梯楼层的范围(地下)。
我的代码是在上次作业的基础上扩展而得,依旧是Control和Request类继承Thread。为将输入交由一个类来处理,Request也负责在收到第一条数量指令时创建电梯,每一个电梯在创建时会自己创建一个调度器即Control。此外我将请求队列移至了电梯内部,因为我采取了在收到请求时立刻分配到具体电梯的写法。Request类在addPeople时会根据此刻每个电梯的状态创建ElevatorSimulator,用以计算加入该请求后电梯结束运行的用时详见下文中电梯调度策略。
第七次作业
本次作业新增了动态增加电梯的请求,同时给出了ABC三种不同类别的电梯,对应了不同的移动速度、载客量、停靠层,且涉及到了换乘。
这次的程序架构变得复杂许多,不过大体上是只增不删。本次作业我将原本的Request类拆为Window和Dispatch两者,Window继承了Thread全面负责了不断接收输入,而addPeople放入Dispatch分配请求到电梯,同时Dispatch也负责电梯的创建。每个电梯依旧保存了想要在某层登上电梯以及已在电梯内想要在某层离开的人,且有自己的调度器Control继承Thread以look算法控制电梯运行。由于换乘的存在,这次我自己写了People类,是对PersonRequest的拓展,既记录了当前所在层(电梯外)、目的地,也记录了本次乘坐电梯的短期toFloor。Type是为了便于电梯创建而写的保存三种电梯属性的类。讨论区有关于输出线程安全的内容,于是加入了OutputSafe类,给输出方法加锁。仍然在Dispatch中利用ElevatorSimulator做乘梯策略选择,详见后文。值得一说的是,如有换乘需求,从Elevator中OUT的乘客是要重新进入Dispatch分配器的,此时仍会第二次进行评估,也就是说如果第一次计算出的最优换乘梯不够优了就会被抛弃。而这里我处理写法的不优美导致了唯一被hack到的死锁。
本次作业的电梯关闭条件必然性的与前两次不同,因为在输入结束后仍有可能由换乘导致某空闲电梯再次被唤醒。此前我的Control线程结束条件是该电梯内无人且输入结束,而这次我是由Dispatch来记录输入结束的信号,且在输入结束的情况下判断若所有电梯内都无人则将所有电梯结束标志置为true。
Summary
以下表格分别对应三次作业,on average,三次作业的CBO(耦合度)分别为2.00、2.00、2.89,LCOM(内聚缺乏度)分别为1.00、1.40、1.44。随着程序结构的复杂化两指标呈现上升走势合理,但不得不说,就耦合度而言,尤其是第七次作业,某些类的耦合高得较为突出,一些类的分工边界仍有些模糊。
第六次作业的Request类为了包揽输入处理,SRP(Single Responsibility Principle)完成的不是很好。
而第六、七次作业基本上都能很好的利用此前代码,因而程序的可扩展性得以印证,增多改少,基本符合OCP(Open Close Principle)。不过就第六、七次作业的性能设计而言,可扩展性并不完备,addPeople方法枚举计算得到当前状态下加入后结果最优的电梯,这里的写法较有针对性,在需求变动较大时必然需要彻底改动,毕竟这样的暴力计算在某些新需求下或许不能承受(如换乘多次),又或者需要根据性能计算指标改变计算最小化的对象与方式。
强测及电梯调度策略
三次作业单个电梯调度均使用look算法,在强测中均未出现错误,在随机数据下性能表现良好,强测分依次是97.7403、99.9555、99.9904。
look算法即持续向某一方向移动,直至前方无操作时折返。
三次作业中我都是一接收请求立即对应给单个电梯的(即分配式,有些同学采用了电梯竞争请求的方式),以ArrayList<Stack<PersonRequest>>
的形式分别保存了在每一楼层准备进入/(已在电梯上)准备离开的乘客。故此处look算法的无操作定义为在某楼层无上下电梯请求。
第六次作业开始请求的分配方式才真正关系到性能,这次作业中许多同学采用了平均的分配策略,但一些情况下并不能全面的照顾到性能。用什么标准来衡量一个请求是否适合被放入某指定电梯呢?请求分配时不同电梯距离fromFloor的间隔不同,不同电梯在某层原本是否开关门不同,这些都会关系到加入请求后电梯运行的时长。要全面考虑电梯状态还是挺不容易的,但我决定全面考虑电梯状态,那就是“暴力出奇迹”“莽、都可以莽”。我用ElevatorSimulator类克隆了电梯的状态,加入新请求然后虚假运行电梯(即将sleep(400)
换成timeUsed+=0.4
),评估是否应该加入。这次作业的性能指标是电梯运行的总时长,故我贪心地将请求加入加入请求后电梯结束运行时间最短的电梯,这变相实现了一种结束运行时间的平均。了解一些时间复杂度相关知识就会发现这样做对CPU time和real time来说都微不足道
第七次作业的性能评价标准有变动,变成了所有请求总等待时长+电梯运行时间,打印一些统计数据就会发现请求总等待时长往往(在数据量较大情况下)比电梯运行时间大很多,于是我决定专注减小前者。依旧选择了ElevatorSimulator暴力计算(学过的数据结构和算法都在哭泣)!但这次涉及到换乘怎么办?我是这样,对每一个请求仅考虑换乘一次或不换乘,这次以该请求达到满足的时间为估计指标,选出时间最小的方案。不换乘直接塞乘客进去就好;如需换乘,枚举换乘层与两电梯,先得到电梯1的离梯时间,让电梯2模拟运行该时长,再将乘客加入电梯2模拟运行直至请求满足。仍然,了解一些时间复杂度相关知识就会发现这个计算量对CPU time和real time来说微不足道
第五次作业的性能分计算公式和后两次不太一样,较小的时间差可能造成较大的分数差别。而后两次作业,我在测评时发现其实一些情况下我程序的性能表现在房间内并不算非常好的。毕竟分配请求这一行为是一次性的并非动态的,没有预见性,未来输入的请求很可能会使得最初的分配不是一种好的选择,毕竟局部最优不意味着全局最优。但是由于性能分计算公式非常和善,强测数据较为随机,这种写法在正式测评中的下限表现得还是非常高的,可以说还是值得推荐。
互测
第五、六次作业中未出现bug。第七次作业出现特定情况下的死锁,惨遭一位同学hack5次,现已一次修复。
第五次作业中hack到一位房友"Elevator arrives from floor 1 to 2 too fast",在第一次接收到请求时瞬移了。
第六次作业在hack结束前一小时还没有提交过数据(因为本地没有发现大家错误),决定随便交一组,竟然中了两个死锁。
第七次作业hack到一位同学当仅一条请求且涉及到3层换乘时会达不到目的地。
!讲到发现其他人bug的策略,本单元作业其实我是有用测评机的。只不过测评机的形式比较非主流,是shell脚本(据我所知大家似乎都写了python)。实现方式略显曲折有舍近求远之嫌(另辟蹊径x),可以给大家一种在知识树不完备情况下达到同样既定目的的思路与启示。应该说关键点是shell中的expect,详情请百度。另外,用程序写程序还挺…。
我在Ubuntu系统上测评目录如下。最终效果是命令行键入./machine.sh <随机样例组数> <jar包名>
可对该目录下某一jar包随机生成若干组数据测评,键入./allkill.sh
即可以某一数据测评房内所有玩家。其中test.sh得到的运行结果将会是输入与输出的交替,judge时将以有时间戳否分辨。
.
├── allkill.sh
├── data
│ ├── data1.txt
│ ├── output1.txt
│ ├── *.txt
├── datacheck
│ └── datacheck_linux
├── data_maker
├── data_maker.cpp
├── judge
├── judge.cpp
├── machine.sh
├── test.sh
├── write_sh
└── write_sh.cpp
心得体会
多线程是此前完全没有接触过的东西,感觉还是非常有趣。
非常需要自我批评的一点是,我感觉很多时候在我顺利写出代码后,就没有再去了解更多。祸兮福所倚,因为任务完成的顺利,选择一条道路的同时错失了千千万万条道路,学习过程有点囫囵吞枣。比如用了synchronized之后就没有继续深入研究其他锁机制、自己模糊摸索到了设计模式就没有深入再想经典模型的套用。
今后还要好好学习勤于思考勤于表达!从设计角度考虑问题!做一个很有方法论的人!而不是一个莽夫