OO第二阶段作业总结
一. 整体思路
第一次作业
1.设计策略
(1)除主线程外共2个线程。GetRequest线程负责读取请求并传入调度器,Elevator线程负责从调度器中获取请求并输出电梯相应的运行结果。
(2)调度器类起着生产者-消费者模型中的托盘作用。GetRequest线程通过put方法向调度器中维护的队列加入请求,Elevator线程通过get方法从调度器维护的队列中按顺序取出请求(按FIFO原则)。
(3)当GetRequest线程读取null时,将null放入调度器队列,然后结束。当Elevator线程从调度器队列get到null时,结束线程。
2.多线程同步控制
在调度器类(dispatch)中给操作共享对象(即请求队列)的方法get与put加锁,以保护请求队列。在get方法中,如果请求队列为空,就等待;否则取走队头请求。在put方法中,将请求放入请求队列中,然后用notifyAll通知可能正处于阻塞状态的电梯线程。
第二次作业
1.设计策略
(1)在调度器中添加getAddRequest方法,完成返回捎带请求的任务。电梯到每个楼层时,从请求队列中选取以本楼层为起始楼层且与电梯运行方向同向的请求,返回的请求队列即为可捎带请求。
(2)对于电梯类,在每个楼层判断是否有需要出电梯或者可捎带进电梯的请求。若有,则开门,并在开关门之间的400ms时间过后再次寻找可捎带请求(以此提高效率)。
(3)每个主请求被分为从当前楼层到起始楼层,以及从起始楼层到目标楼层两个部分。若两个部分方向不一致,且满足下述3个条件:未换过向,电梯正在或已走过起始楼层,电梯中只有主请求,则换向。
2.多线程同步控制
与第一次作业相比,只在调度器类中多一个加方法锁的getAddRequest方法,使得对请求队列进行操作的get,put,getAddRequest方法互斥。
第三次作业
1.设计策略
第三次作业主要的问题在于请求的拆分、电梯定向运行以及终止条件的判断。
(1)请求的拆分:采用改装版迷宫策略(ty巨狗牛批!),将每个请求拆分为多个单电梯可达的子请求,每个电梯只能获取到起始楼层、目标楼层都在其可达列表中的子请求。
初始化:
设置3行23列的矩阵,每行(i)代表一个电梯,每列(j)代表一个楼层。若电梯i可在楼层j停靠,mystery[i][j]=1;否则mystery[i][j]=0。
搜索算法:
采用广度优先搜索,第一次到达可停靠的终点楼层时程序结束,如此找到的会是相对来较短的路径(不一定最短,因为没有考虑电梯速度,并且同楼层换乘时从电梯1到电梯3会比从电梯1到电梯2多走一步)。对于当前点,若mystery[i][j]==0,可向同一个电梯的上一层或下一层走一格;若mystery[i][j]==1,可向同一电梯的上下一层走一格,也可向隔壁电梯的同层(如果隔壁电梯同层满足mystery[i][j]==1)走一格。
放入队列:
对于每个输入的请求,把拆分后的第一个子请求的state设为1,表示可以立即执行;其余设为0,表示尚不能执行。当每个请求在电梯中执行完毕后,会在调度器内调用setState方法,把请求队列中下一个同id请求的state设为1,以保证执行顺序正确。
(2)电梯定向运行(伪代码如下)
outList:存储还在电梯中的请求。
waitRes:从请求队列中获取的、目前还不在电梯里的一个主请求,用来协助定向。
dir:方向。
flag=false
如果outList中除了本层出去的以外还有别的请求,则dir=request.Tofloor-floor
如果outList中只有本层出去的请求(或没有请求),则
如果waitRes不为null,则
如果waitRes.fromFloor==floor, dir=request.Tofloor-floor
否则,dir=request.Fromfloor-floor
如果waitRes为null,则
如果outList不为空,设置flag,不定方向,执行excute2方法(与execute方法的区别:根据接入的第一个可捎带请求定向,接下来的请求按此方向接入。返回值:若捎带列表为空,返回0;如果不为空,返回方向。)
如果execute2的返回值为0或outList为空,从dispatch中getwaitRes
如果get到waitRes=null,break
如果get到waitRes!=null,则
如果waitRes.fromFloor==floor, dir=request.Tofloor-floor
否则,dir=request.Fromfloor-floor
如果!flag,则
执行execute方法
电梯按方向升降
arrive下一层
(3)终止条件判断:
对于GetRequest线程,读到null时结束;对于每个电梯线程,当请求队列中剩下的请求均是该电梯不可执行的,且队尾元素为null时,结束。
2.多线程同步控制
在调度器类(dispatch)中,给get,put,getAddRequest(对应execute捎带),getAddRequest2(对应execute2捎带),setState这些操作请求队列的方法都加上方法锁,从而保证互斥。另外,由于指导书里提到的TimableOutput输出接口不保证线程安全性的问题,我给每个电梯线程传入了同一个对象monitor,把所有的输出放在monitor对象锁里,确保安全。
二. 自我测试中发现的问题
第二次作业
由于我在设计本次作业的捎带方法时思路比较清奇,因此代码架构较为复杂,bug频发,补丁众多。
(1)为保证换向条件之一(电梯中只有主请求)及结束一个主请求的条件之一(电梯中没有人)的正确性,必须保证电梯在第一次经过主请求的起始楼层时将其接入。但由于第一次经过其起始楼层时,主请求方向很可能与电梯目前的运行方向相反,因此不会被带入电梯。最后用firstFlag特判解决。
(2)为防止电梯在没有接到主请求时就因电梯为空而结束此请求,必须加入mainFlag(标志是否已接到主请求)判定结束条件。
(3)为完成电梯不连续开关门的小优化,每个主请求执行结束时并不关门,等待400ms后,运行下一个主请求,由它为前一个主请求关门。这样,若在前一个主请求开门后400ms内有可捎带进本层电梯的请求,就不会连续开关门。但我忽略了请求队列为空的情况,因此若输入为空,电梯会在一楼直接关门,然后结束程序。(然而因为互测交不了空输入躲过一劫hiahiahia)
第三次作业
(1)遇到了电梯线程无法结束的问题,举例说明如下:
输入:[0.0]1-FROM-1-TO-2
执行:
1. 电梯线程A、C启动,此时请求队列中还没put到null,因此电梯线程A、C进入调度器类get方法中的wait状态。
2. 电梯线程B启动,取到从1楼到2楼的请求,执行后取到null,本线程结束。
结果:
电梯线程A、C无法结束,不能退出wait状态。
解决方法:
当电梯线程调用调度器类的get方法时,若get到null,则补上notifyAll(),使得A、C线程能够收到通知,从阻塞状态转入可执行状态,因此能get到null并成功结束线程。
(2)最初在setState方法结尾没有加notifyAll,使得即使某些请求变为可执行状态,能执行它的电梯线程仍保持在get方法中的wait状态,无法执行。
三、代码分析
程序结构的度量参数如下:
(1)ev(G):基本复杂度,用来衡量程序非结构化程度的,范围在[1,v(G)]之间,值越大则程序的结构越“病态”。非结构成分降低了程序的质量,增加了代码的维护难度。
(2)Iv(G):模块设计复杂度,用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。
(3)v(G):循环复杂度,用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数。
(4)OCavg:类方法的平均循环复杂度。
(5)WMC:类方法的总循环复杂度。
参考博客链接:https://www.cnblogs.com/qianmianyu/p/8698557.html
https://www.cnblogs.com/panxuchen/p/8689287.html
部分设计原则解释如下:
(1)单一功能原则:一个类只承担一种类型的责任,即只有单一功能,以避免同一个类中职责间的影响。
(2)里氏替换原则:子类可在任何父类能够出现的地方替换父类,且经替换后代码还能正常工作。
(3)依赖反转原则:代码应当取决于抽象概念,而不是具体实现,即依赖于抽象而非实例。
(4)接口隔离原则:多个专门的接口优于单一的总接口。
(5)开闭原则:对扩展开放,对修改封闭。
参考博客链接:https://www.cnblogs.com/huangenai/p/6219475.html
第一次作业
(1)类图
(2)方法度量与类度量
(3)分析
1.复杂度分析
本次作业中各类及各方法的复杂度都控制得较好,结构也较为清晰,给后面的作业留下了较好的扩展基础。
2.设计原则分析
从依赖反转原则和开闭原则的角度分析,电梯类的实现有很大的问题。因为在电梯执行请求的过程中,我并没有模拟电梯上下楼层及开关门的行为,只是以输出信息为目的,把电梯对于每个请求的所有操作(起始层开门进人关门,运行,到达层开门出人关门)全部放在了Elevator类的output的方法中,没有做到依赖于抽象而非实例。
第二次作业
(1)类图
(2)方法度量
(3)分析
1.复杂度分析
电梯类的execute方法复杂度非常高,导致电梯类的总循环复杂度也奇高。这样的结果其实在预料之中。由于设计思路的问题,为满足60行的限制,我从这一方法中分出了很多小方法,压缩到无可压缩的地步也还有58行。方法体中有4个flag,多个if/else嵌套以及复杂的判断条件,使得复杂度大大提升。
2.设计原则分析
从单一功能原则角度分析电梯类的execute方法,可以发现,它将太多的功能杂糅在了一起。将主请求分为两部分,每层的运行,换向、结束的判断……其功能过于复杂,导致了设计结构很冗长。因此,在第三次作业中,我着重改正了这个问题,将电梯每层的运行单独作为execute方法,用每层定一次向替代了划分主请求并在开头给全程定向的做法,简化了设计。
第三次作业
(1)类图
(2)方法度量
(3)分析
1.复杂度分析
调度器类中的checkGet、get、getAdd以及setState方法非结构化程度较高,原因是这些方法的for或是while循环中都有大量的判断条件及条件分支,使程序难维护、难理解。
电梯类中的execute方法,其模块设计复杂度和循环复杂度较高。模块设计复杂度高是因为execute方法是电梯在每层的运行方法,需要调用开门、进人、出人、关门方法,以及从调度器中获取捎带请求、改变调度器请求队列中同id请求的状态。循环复杂度高的原因是,当电梯到达waitRes(主请求)所在楼层时,需用特判的方法将其接入电梯,因此多了一些if/else判断。
迷宫类中的findRoad方法,由于bfs本身是个面向过程的算法,为适应电梯要求改版后更是增加了判断条件和循环过程,因此复杂度较高。
2.设计原则分析
不符合单一功能原则,尤其是调度器类,作为联系getRequest类和电梯类的桥梁,取放请求、调用迷宫拆分请求、设置请求状态、获取捎带请求的功能都由其实现。
四、快乐hack(不,你不快乐)
第二次作业
发现了先出人后进人的bug。可能的原因是,对于获取到的请求,当经过其起始楼层时就进人,经过目标楼层时就出人,且在直接从起始楼层出发的情况下未完成进人操作。hack样例:
可以看出,简单但切中要害(基本都是自己de过的bug orz)的样例有奇效。
第三次作业
没有评测机也不想肉眼检查输出,所以就没有动刀了orz……下个单元打算手写评测机,不然以后大概次次都只能放空刀了(发出菜叫)。
五、心得体会
第一次作业比较简单,主要就是定下了一个可复用的基本架构:电梯线程-调度器-输入线程,为之后的两次作业打下基础。第二次作业对我来说可能比第三次作业写的还久一些(捂脸……),因为我在电梯线程的执行方法中没有做到分解问题、简化架构,把一个主请求而非电梯在一个楼层的运作看做一次执行。这种思路无形中把问题复杂化了,使得我为了debug不停地加特判,整个方法冗长繁杂,还无法分割。第三次作业从度量上看不是那么漂亮,但我已经尽力做了很多的优化(……),比如在每层定向且执行,单独造一个迷宫类用bfs实现输入请求地拆分等等(再次感谢ty巨狗!)。最后,感谢研讨课大佬们的分享,我下一个单元也要写一个评测机(立下flag!),and一定要试试除了System.out.println以外的debug方法(一开debug模式bug就消失了是真的难受……),不然真的没办法愉快地狼自己和狼人了(哭泣)。