1. 思路分析
1.1 第一次作业
没有了傻瓜电梯的过渡,我们一上来就迎来了可捎带电梯的考验。犹记得那一周的周三周四两天,我苦苦思索多线程破解方案,慢慢领悟到生产者-消费者模式的精髓,遂理出思路如下:
- 整体架构为主线程负责输入以及启动电梯进程,另有一个托盘对象(调度器)负责交互,同时按照指导书的提示,我将请求队列也放入此调度器中;
- 采用
wait
方法,使电梯在请求队列为空的时候处于等待状态,有请求时则根据请求来决定动作,此后每到一层就和调度器进行交互,来决定电梯的状态(是上行,下行,还是等待); - 输入结束时,改变一个全局变量的值,电梯线程随时查看此变量的值,并在需要时结束自己;(主要参考讨论区中某助教的分享,后续作业也是此操作,不再赘述);
- 至于具体的调度算法,我借鉴了 SSTF 的思想,也就是如果电梯外有请求就寻找起始楼层离自己最近的,否则就找电梯内请求目的楼层最近的;
1.2 第二次作业
呼,这个单元终于不需要像第一单元那样次次重构了,当然要感谢自己第一次作业就把整体框架和调度算法定下来,后续的作业无须“开天辟地”。当然第二次作业也是需要一定的思维量的,设计策略如下:
- 构造一个电梯线程数组(准确来说应该叫线程池),根据输入来决定 new 出的电梯线程数量;
- 实行请求和电梯绑定的策略,每来一个请求,选取离得最近(获取电梯楼层为只读操作,无须上锁等特殊处理)以及已承担请求不太多(出于防超载和提高效率的目的)的电梯,加入该电梯的请求队列(在调度器中);
- 同样是逐层交互,不过为了进一步提高性能,我采取了动态分配的策略,就是电梯每到一层时,查看属于此电梯的请求是否有更优的分配方案,并随时调整;
1.3 第三次作业
同样,多线程整体架构依然沿袭上次,不过这次多了限制停靠楼层,以及需要考虑换乘请求。设计策略如下:
- 初始三部电梯先行启动,后加的电梯根据输入初始化相应属性并启动;
- 综合考虑,我的考量是“能不换乘尽量不换乘,必要时只换乘一次”,并根据始终楼层将请求分成 A,B,C,AB,AC,BA,BC,CA,CB 九类(表示电梯依次需要的电梯类型),构造
MyPerReq
继承自PersonRequest
类(上一次作业中是直接加的PersonRequest
属性,但是MyPerReq
应该“是”一个请求,而不是“有”一个请求,采用继承关系更符合规范),增加请求类型、是否需要换乘等属性; - 请求和电梯绑定 + 动态分配的策略依然适用,大体思路与第二次作业无异,只是这次需考虑换乘情况,以及根据请求类型选择换乘楼层,灵活调整始终楼层;
2. 可扩展性分析
OO 进程都过半了,要是在扩展性上还没点长进,岂不是要闹笑话?个人感觉第三次作业的可扩展性还不错,具体见下:
2.1 功能设计
从我这三次作业的迭代情况来看的话,在新加功能这一方面,我的电梯表现尚可:电梯有限制了?电梯线程类里面加相应的属性即可;电梯有删减?线程池操作不太难;请求有变动了?MyPerReq
类也能玩出新花样;只要新功能逃不出“电梯送人”这一基本逻辑,我的代码应该就能以不变应万变;
2.2 性能设计
本人三次作业都采用类似 SSTF 的算法,在性能方面,三次作业都没有太为难本人的算法,但是如果“饿死”现象会有相应惩罚,等待时间极差也考虑在性能分内之类的要求出现的话,我的算法相较 LOOK 算法等就会明显处于下风。因此从性能这个角度来说,在某些情况下可扩展性不佳,甚至需要重构(因为请求队列,电梯方法都是围绕算法展开的);
2.3 SOLID 分析
- SRP:电梯类仅含自身的类型、当前楼层等属性,以及自身上下人、移动的方法,调度器中也聚焦请求队列的操作以便和电梯交互,SRP 体现较好;
- OCP:从以上分析也能看出,一般的电梯扩展足以应付,但是若从性能角度需要上层调度器来提高调度效率时,由于电梯线程池和其有一定耦合关系,重构可能在所难免,OCP 表现不佳;
- LSP:用
MyPerReq
来继承课程组提供的请求类,为便于算法实现增加了一些属性和方法,但未破坏原父类的相关约束,LSP 没啥问题; - ISP:本次作业未涉及接口操作,在此不表;
- DIP:本单元作业需要实现的类不多,且各类之间并无明显层次依赖关系,但是本人为图省事,编写了一个专门计算楼层间距离的类并四处调用,不太符合 DIP 的思想;
3. 结构分析
由于本人在这一单元切实体会到了迭代带来的便利之处,故三次作业整体架构大同小异,在此仅分析第三次作业的结构:
3.1 UML图
电梯线程实现自身必要的方法,在特定时间完成和调度器对象的交互,所有类各司其职,整体架构还算清晰;
3.2 协作图
我得承认,为了实现我“奇怪的”算法,我的调度器的加入队列等一些方法显得过于臃肿,这也是我需要思考的地方:该不该为了一点点的效率而牺牲代码的简洁性?或者说,如何做好高效性和简洁性的 trade-off?
3.3 度量分析
Type Name | Method Name | LOC | CC | PC |
---|---|---|---|---|
CalTheDis | disOfTwoFloor | 8 | 2 | 2 |
Dispatcher | Dispatcher | 5 | 2 | 0 |
Dispatcher | getEleReq | 3 | 1 | 0 |
Dispatcher | getAndAdjust | 11 | 2 | 0 |
Dispatcher | justGet | 3 | 1 | 0 |
Dispatcher | put | 14 | 2 | 1 |
Dispatcher | fromA | 4 | 1 | 1 |
Dispatcher | fromB | 4 | 1 | 1 |
Dispatcher | fromC | 4 | 1 | 1 |
Dispatcher | toA | 4 | 1 | 1 |
Dispatcher | toB | 4 | 1 | 1 |
Dispatcher | toC | 4 | 1 | 1 |
Dispatcher | getType | 32 | 10 | 1 |
Dispatcher | transFloor | 32 | 8 | 1 |
Dispatcher | choose | 28 | 6 | 3 |
Elevator | Elevator | 18 | 3 | 7 |
Elevator | run | 32 | 7 | 0 |
Elevator | hasOut | 13 | 4 | 0 |
Elevator | hasIn | 18 | 4 | 0 |
Elevator | someoneOut | 21 | 4 | 0 |
Elevator | someoneIn | 18 | 4 | 0 |
Elevator | judgeState | 83 | 15 | 0 |
Elevator | move | 25 | 4 | 1 |
Elevator | adjustEle | 43 | 10 | 1 |
Elevator | canIn | 5 | 1 | 1 |
Elevator | canOut | 6 | 1 | 2 |
Elevator | getFloor | 3 | 1 | 0 |
Elevator | getType | 3 | 1 | 0 |
Elevator | getMax | 3 | 1 | 0 |
ElevatorControl | getEle | 3 | 1 | 1 |
ElevatorControl | add | 3 | 1 | 1 |
MainClass | main | 49 | 10 | 1 |
MainClass | getRunning | 3 | 1 | 0 |
MainClass | setRunning | 3 | 1 | 1 |
MainClass | getFloorA | 3 | 1 | 0 |
MainClass | getFloorB | 3 | 1 | 0 |
MainClass | getFloorC | 3 | 1 | 0 |
MainClass | getNumOfReq | 3 | 1 | 0 |
MainClass | setNumOfReq | 3 | 1 | 1 |
MainClass | getNumOfEle | 3 | 1 | 0 |
MainClass | setNumOfEle | 3 | 1 | 1 |
MyPerReq | MyPerReq | 7 | 1 | 5 |
MyPerReq | getType | 3 | 1 | 0 |
MyPerReq | getNeedTrans | 3 | 1 | 0 |
MyPerReq | getTransFloor | 3 | 1 | 0 |
MyPerReq | getChooseEle | 3 | 1 | 0 |
MyPerReq | setChooseEle | 3 | 1 | 1 |
SafeOutput | println | 3 | 1 | 1 |
Type Name | NOF | NOPF | NOM | NOPM | LOC | WMC | NC | DIT | LCOM | FANIN | FANOUT |
---|---|---|---|---|---|---|---|---|---|---|---|
CalTheDis | 0 | 0 | 1 | 1 | 10 | 2 | 0 | 0 | -1 | 0 | 0 |
Dispatcher | 2 | 0 | 14 | 6 | 156 | 38 | 0 | 0 | 0.714285714 | 0 | 0 |
Elevator | 10 | 0 | 14 | 6 | 303 | 60 | 0 | 0 | 0 | 0 | 0 |
ElevatorControl | 1 | 0 | 2 | 2 | 9 | 2 | 0 | 0 | 0 | 0 | 0 |
MainClass | 6 | 0 | 10 | 10 | 84 | 19 | 0 | 0 | 0.7 | 0 | 0 |
MyPerReq | 4 | 0 | 6 | 6 | 28 | 6 | 0 | 0 | 0 | 0 | 0 |
SafeOutput | 0 | 0 | 1 | 1 | 5 | 1 | 0 | 0 | -1 | 0 | 0 |
依然是之前提到的 trade-off 问题,为了性能分,而使得相关代码复杂度飙高(比如动态调整请求分配的adjustEle
方法等),当然大佬们一定能做到二者的兼顾,这也是我接下去需要重点关注和学习的地方;
4. Bug 分析
4.1 自己的 bug
从上面的分析也能看出来,本人并未采用线程安全容器等一些高级做法,就是简简单单用 synchronized、wait、notifyAll 这些方法,需要和调度器对象交互时就直接锁起来即可。我承认这种做法显得很 low,而且自己造一些轮子效率也不太高,但是我可以极为清晰简单地把整体逻辑梳理出来,电梯和调度器的交互情况,调度器当前的锁在哪里等等我都了然于胸,不会有死锁等一些线程安全问题的出现(可能还是怂吧,对高级用法了解不够深入所以不敢轻易使用,在这个方面的 trade-off 中选择了稳妥的做法,但是随着经验的积累应该会逐步提高代码的逼格,期待写出高质量的代码)。另一个原因就是本单元的评测机对我等菜鸡来说着实困难,逛了一天的讨论区和百度也无从下手,别人推荐了个去年学长的评测机今年也无法适用,只好弱弱地用保险的写法了。仅从得分的角度来说令人欣慰,三次强测无 bug 出现,互测方面最后一次作业如果输入的最后一条指令是新加电梯我的代码就会卡死(还是对自己的代码太自信了,手工测试也要考虑周全啊喂!),究其原因是我的全局变量(见思路分析处)的改变未及时通知新加的电梯。debug 也很简单,电梯刚启动时 sleep 一个很短的时间即可;
4.2 别人的 bug
无评测机的低端玩家在互测中只能干瞪眼,尝试着手搓了几个极端数据只是 naive 或者显示不合法。不过我在的屋子三次都十分和平,第一次仅有一位老哥的玄学 bug 被一条指令 hack 到(他是怎么过的中测???),第二次干脆就没有 hack 成功的,第三次我的那个 bug 承担了所有的伤;事实证明,非评测机玩家也能在夹缝中生存
5. 心得与体会
5.1 线程安全
还是要强调一下我之前反反复复提到的 trade-off,在个人能力、时间有限的情况下(嗯,没错就是指我,大佬当然能全面兼顾),在面对一项比较复杂的工程时,只能做出“两害相权取其轻”的取舍。具体到这单元的作业,我放弃了一部分的架构上的优化,而是优先确保线程安全上不会出大问题(一些同学惨痛的翻车经历让我有些后怕),当然这极不值得提倡,正确的做法是往死里学!往死里优化架构!往死里测试保证线程安全!可是谁又能懂菜鸡的痛苦呢?尽可能的在能力范围内做到二者的平衡;
5.2 设计原则
整体的设计无非是生产者-消费者模式的延伸,算法也是 SSTF 打底,不过群里的讨论给了我一些新的想法:何不结合观察者模式等新的设计模式来重新审视这几次作业呢?结合图的最短路算法又会擦出怎样的火花呢?奈何单元结束,引无限遐想供日后摸索;
5.3 小结
OO 课程悄然过半,我对面向对象的思想体会更深了,我也在保证正确性的前提下遵循了一些设计原则。同时,一次次的作业让我切实体会到了“痛并快乐着”,那种苦思冥想终得,提交后紧张祈祷的感觉和计组、OS相比真是有过之而无不及,并终将成为我一段“难忘“的回忆。