BUAA_OO_第二单元
一、设计策略分析总结
本单元实验要求对进阶难度的电梯进行模拟,要求模拟乘梯人员发出输入请求,且能够让电梯处理请求并模拟运行、开关门、将乘客送到目的地,在电梯做出相应行为时输出一定的信息。
在本次任务中,我的设计原则是“一个类只做一类事”,同时几乎是按照现实生活中的电梯进行调度的设计。
(一)模块设计
第一次作业中,经过分析并考虑可扩展性,将程序总体分为三个模块:输入请求处理,请求列表,电梯与电梯方向,调度器。其中请求列表为所有模块的共享变量,用于保存请求并供调度器获取;电梯与电梯方向模块中,前者只执行开门关门、上行下行以及进出人的操作,后者只是一个枚举类型,供电梯保存运行状态;输入请求处理使用了一个线程,将读到的请求插入请求列表,在读到NULL时将请求列表记录为结束并结束线程;调度器使用了一个线程,在这个模块中查找合适的请求,并控制电梯的运行。
第二次作业中,在电梯与电梯方向模块里增加了楼层类,主要作用是将可能存在空缺的可用楼层映射到1-n的连续数字,并且记录每个楼层是否有乘客的目的地。
第三次作业中,增加了换乘人员及其列表模块,记录需要换乘的人和换乘路线。
三次作业均只有主线程、输入请求线程及调度器线程三种。
(二)调度算法设计
单电梯调度算法:
初始化:当电梯状态为停止且请求列表中存在该电梯能够运送的请求时,获取第一个可用请求,并将方向设为前往该请求请求楼层的方向,目标楼层设为请求楼层。
目标楼层:当前电梯方向中,据当前楼层最近的楼层。
每到达一层楼,判断该楼层是否有出电梯的请求和同方向进电梯的请求,若有则开门出人,并在能够进入的情况下(目前仅有容量限制导致不能进入)进入所有该层请求的人(包括同方向和异方向),重新计算目标楼层。继续向目标楼层前进。
电梯到达目标楼层时,转换方向,若存在该方向的目标楼层继续前进,否则查看请求列表确定目标楼层。如果列表里也没有合适的,那么电梯状态设为停止,调度器进入wait状态。
多电梯调度算法:
三个电梯调度器分别对满足自己运送条件的请求进行竞争,设置三种换乘:到达15层以上的在15层换乘、上行且需要到3层的在5层换乘,其余需要换乘的在1层换乘。
二、基于度量分析程序结构
指标说明:
-
LOC: Line of Code
-
NCLOC:Non-Commented Line Of Code
-
ev(G) 基本复杂度(Essential Complexity):衡量程序非结构化程度。
-
iv(G) 模块设计复杂度(Module Design Complexity):衡量模块判定结构,即模块和其他模块的调用关系。
-
v(G) 圈复杂度(Cyclomatic Complexity):衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数
(一)第一次作业
1.代码规模
类名 | LOC | NCLOC |
---|---|---|
Main | 13 | 13 |
Dispatcher | 64 | 64 |
Elevator | 77 | 77 |
ElevatorDirection | 13 | 13 |
RequestInput | 30 | 29 |
RequestList | 35 | 35 |
总计 | 232 | 231 |
2.类的属性方法
调度器:
在线程中不断调用lookforReq()方法,若无请求则进入wait状态或者结束线程。
电梯方向:
对电梯的运行方向的枚举,上行下行或者处于暂停状态。reverse函数用于转换方向。
输入请求:
请求列表:
涉及的所有操作均需要锁住,当读取到NULL时设为over。
电梯:
将电梯的一系列操作细分为一个个小方法,如开门关门,进人出人,并能记录电梯内部人员的请求。
3.度量
可看出复杂程度较高的,主要为调度器中的查找请求的方法。这是将对电梯的所有操作都用一个方法来表示造成的。
4.类图
使用单例模式,主线程创建请求输入线程和调度器线程,一个调度器控制一个电梯实例。
(二)第二次作业
1.代码规模
类名 | LOC | NCLOC |
---|---|---|
Main | 12 | 12 |
Dispatcher | 138 | 135 |
Elevator | 96 | 96 |
ElevatorDirection | 13 | 13 |
FloorList | 51 | 51 |
RequestInput | 30 | 29 |
RequestList | 35 | 35 |
总计 | 380 | 376 |
与第一次作业相比,更改只出现在调度器、电梯及楼层类中。
2.类的属性方法
调度器:
调度器中,为了满足更优的调度算法,增加了若干关于请求的计算,其中findUppFloor()和findDownFloor()函数是为了寻找当前方向上的目标楼层。
楼层:
doors数组类似于生活中电梯内部的表盘,记录哪一层有人要下电梯。
getIndex() 用于转换不规则楼层信息与规则的序列。
3.度量
同样也是一些起到控制作用的函数具有较高的复杂度。
4.类图
依然是单例模式,主线程创建输入请求线程和调度器线程,一个调度器对应一个电梯实例及相应的楼层实例。
(三)第三次作业
1.代码规模
类名 | LOC | NCLOC |
---|---|---|
Main | 17 | 17 |
Dispatcher | 194 | 189 |
Elevator | 137 | 137 |
EleDirect | 13 | 13 |
FloorList | 66 | 66 |
RequestInput | 44 | 43 |
RequestList | 33 | 33 |
Person | 75 | 75 |
PersonList | 32 | 32 |
总计 | 611 | 605 |
由于出现换乘的可能性,故增加人员列表导致了大部分类都发生一部分改变,在调度方面区别不大。
2.类的属性方法
人员:
对每一个人的请求创建每一个人的类,当需要换乘时才会对这个人进行下一步操作,不然就不用管了按前两次作业的处理来。
divideReq() 为将该人的请求拆分成两个可达的请求,当第一个请求完成时,向请求列表中插入第二条请求。
换乘人员列表:
如果没有换乘人员,那么一直空着。对该表的操作也是都应该加锁的。
3.度量
感觉复杂度高的都是逻辑比较复杂的,其中牵扯到的操作比较多,看了看也不太好修改。
4.类图
要比前两次的结构复杂,但是其实把RequestList换成PersonList,一直用后者操作也可以,但是那样需要改的比较多,怕改着改着出现问题(懒)。于是只简单增加了换乘人员类,各个类中增加部分语句即可。
(四)UML协作图
本单元程序均只有三个线程,故三次作业UML协作图可统一展示。
(五)分析改进
1.优缺点分析
优点:结构比较明确,电梯运行与控制分离;模块化,增加功能时只需要改变一部分,省时省力。
缺点:存在冗余的情况,如Person类可继承PersonRequest类,但只重新建了个并重复的部分功能。
2.SOLID设计原则改进
-
Single Responsibility Principle(单一职责原则):每个方法基本上都只负责一项功能的执行,避免一个类做太多事情,符合。
-
-
Liscov Substitution Principle(里氏替换原则):不存在子类的设计,符合。
-
Interface Segregation Principle(接口分离原则):不存在接口设计,符合。
-
Dependency Inversion Principle(依赖倒置原则):若增加新的电梯,只需要在FloorList中增加对应电梯的ID的信息,抽象程度较好。
三、错误分析
(一)存在错误
在前两次作业中,处理好输入的结束并且把对共享变量的操作都加上锁之后,应该就没有线程上的bug了。
第三次作业中,出现了CPU超时的情况,原因是判断wait的条件为“请求列表为空且电梯内为空”,但存在“请求列表不为空,但是请求该电梯不能满足”的情况,导致了这个调度器一直在轮询,从而超时。
还有无法中断的情况,在前两次作业中,输入NULL时请求列表会设为状态结束,此时会唤醒一次所有线程,如果电梯wait了,会被唤醒并判断结束,终止线程。但第三次作业中,请求输入结束了,但是如果有换乘的,换乘完会插入请求列表,调度器判断进入wait状态,由于不再有变化,导致线程无法再次被唤醒并且一直wait下去。这需要考虑终止的各种情况。
(二)测试的方法
1. 手工测试
如对NULL的插入位置依次尝试。
2. 自动测试
编写java程序,自动生成测试样例,读取测试并按时间延时输出模仿手工输入,并检查输出结果,记录下错误的测试数据及结果供日后debug。
四、心得体会
做电梯最重要的是理解多线程的思想,理解了之后程序便比较容易编写了,同时也能够知道莫名的bug出现位置的可能性。
另一方面,计组中学到的工程化思想得到了应用,第一次作业时参考去年的电梯作业要求用了一天的时间进行设计,在后两周效果还不错,虽然性能分不算高,但是付出与收获的性价比很高…