电梯迭代作业总结
电梯迭代作业总结
一.前言
与上学期的c语言的学习不同,这一学期的学习强度和上学期完全不同,java的编程量是c语言的好几倍,基本每个题目都要有上百行代码,编程时间也比上学期翻了好几倍,基本每次小题要一个小时不等,而大型作业都要基本几天几个下午几个晚上,总体难度是相当高的。
电梯迭代主要涉及java中类的设计,Array List、LinkList的使用,其中要把类与类的关系处理好,像是关联、依赖、聚合,但最核心的还是电梯的运行算法。
二.设计与分析
第一次迭代
题目:7-34 NCHU_单部电梯调度程序
设计一个电梯类,具体包含电梯的最大楼层数、最小楼层数(默认为1层)当前楼层、运行方向、运行状态,以及电梯内部乘客的请求队列和电梯外部楼层乘客的请求队列,其中,电梯外部请求队列需要区分上行和下行。
电梯运行规则如下:电梯默认停留在1层,状态为静止,当有乘客对电梯发起请求时(各楼层电梯外部乘客按下上行或者下行按钮或者电梯内部乘客按下想要到达的楼层数字按钮),电梯开始移动,当电梯向某个方向移动时,优先处理同方向的请求,当同方向的请求均被处理完毕然后再处理相反方向的请求。电梯运行过程中的状态包括停止、移动中、开门、关门等状态。当电梯停止时,如果有新的请求,就根据请求的方向或位置决定移动方向。电梯在运行到某一楼层时,检查当前是否有请求(访问电梯内请求队列和电梯外请求队列),然后据此决定移动方向。每次移动一个楼层,检查是否有需要停靠的请求,如果有,则开门,处理该楼层的请求,然后关门继续移动。
使用键盘模拟输入乘客的请求,此时要注意处理无效请求情况,例如无效楼层请求,比如超过大楼的最高或最低楼层。还需要考虑电梯的空闲状态,当没有请求时,电梯停留在当前楼层。
请编写一个Java程序,设计一个电梯类,包含状态管理、请求队列管理以及调度算法,并使用一些测试用例,模拟不同的请求顺序,观察电梯的行为是否符合预期,比如是否优先处理同方向的请求,是否在移动过程中处理顺路的请求等。为了降低编程难度,不考虑同时有多个乘客请求同时发生的情况,即采用串行处理乘客的请求方式(电梯只按照规则响应请求队列中当前的乘客请求,响应结束后再响应下一个请求),具体运行规则详见输入输出样例。
输入格式:
第一行输入最小电梯楼层数。
第二行输入最大电梯楼层数。
从第三行开始每行输入代表一个乘客请求。
电梯内乘客请求格式:<楼层数>
电梯外乘客请求格式:<乘客所在楼层数,乘梯方向>
,其中,乘梯方向用UP
代表上行,
DOWN
代表下行(UP、DOWN必须大写)。
当输入“end”时代表输入结束(end不区分大小写)。
输出格式:
模拟电梯的运行过程,输出方式如下:
运行到某一楼层(不需要停留开门),输出一行文本:
Current Floor: 楼层数 Direction: 方向
运行到某一楼层(需要停留开门)输出两行文本:
Open Door # Floor 楼层数
Close Door
设计思路
要求设计一个电梯类,并对外部请求、内部请求进行处理,我们可以将外部请求和内部请求添加到两个队列,因为题目说明采用串行处理乘客请求,所以每次只需要对外部队列的头部请求与内部队列的头部请求进行比较,每次处理一个请求。
然后就是涉及数据的输入与输出了
输入
第一行是电梯最小楼层,第二行是电梯最大楼层,第三行之后就是电梯的请求,直到读入end或END为止,可以利用循环将所有数据读入,并把他们依次放入队列当中,读取并转换。
minFloor = Integer.parseInt(list.get(0));// 第一行,电梯最低楼层
maxFloor = Integer.parseInt(list.get(1));// 第二行,电梯最高楼层
运用正则表达式去判断是内部请求还是外部请求,内部请求只有目标楼层,外部请求有目标楼层以及目标方向,再将输入数据进行处理,删除<>和,,最后添加两个对列。
matches("<\d+,\s*(UP|DOWN)>)
matches("<\d+>”)
String[] parts = request.replaceAll("[<>]", "").split(",");
输出
每次朝目标楼层移动一层,输出
Current Floor:楼层数 Direction:方向
再进行判断,如果是要到的目标楼层,就输出
Open Door # Floor 楼层数
Close Door
获取下一目标楼层
由于每一次都会完成某一个队列的头部请求(到达某一楼层),所以要在外部队列头部、内部队列头部选择一个请求并执行。
首先是关于其中一个队列为空的情况,默认执行非空的队列
然后是两个队列都在当前楼层上方或下方,如果外部队列方向和电梯方向相同,就比较两队列哪个更离当前楼层近,近的就是下一目标楼层。
如果与电梯方向不同,下一目标楼层就直接是内部队列。
最后是一上一下,优先与电梯同向的请求,如果是外部请求,电梯也要相应的改变方向
分析
SOURCEMONITOR
- 代码规模:192 行,125 条语句,分支语句占 25.6% ,含注释行占 5.7% 。
- 结构特性:4 个类和接口,平均每类 3.25 个方法,每方法平均 7.38 条语句。
- 复杂度:最复杂方法
Elevator.getnextfloor()
,复杂度 20 ,平均复杂度 4.15 ,最深代码块深度 6
POWERDESIGN
- levator 类:表示电梯,有最小楼层
minFloor
、最大楼层maxFloor
等属性 ;含构造方法初始化楼层范围,以及添加内外部请求、设置运行方向、获取请求列表、获取当前楼层、打印运行过程、处理请求等方法。 - Main 类:程序入口,
main
方法启动程序,与Elevator
类通过currentDirection
关联。 - Direction 枚举类:定义电梯运行方向
UP
(向上)和DOWN
(向下) 。 - ExternalRequest 类:代表外部请求,有请求楼层
floor
和方向direction
属性,含构造方法及获取楼层、获取方向的方法。
总结
Elevator
类承担了众多职责,包括管理内外部请求、设置方向、处理请求等。这可能导致类过于复杂,违反单一职责原则,后期维护和扩展时,一个功能的修改可能影响其他功能。
关系不够清晰:类与类之间的关联关系仅用简单线条表示,没有明确标注关联的多重性(如 1 对 1、1 对多等) ,理解类间数据交互和依赖程度时不够直观。
第二次迭代
7-37 NCHU_单部电梯调度程序(类设计)
对之前电梯调度程序进行迭代性设计,目的为解决电梯类职责过多的问题,类设计要求遵循单一职责原则(SRP),要求必须包含但不限于设计电梯类、乘客请求类、队列类以及控制类,具体设计可参考如下类图。
电梯运行规则与前阶段单类设计相同,但要处理如下情况:
乘客请求楼层数有误,具体为高于最高楼层数或低于最低楼层数,处理方法:程序自动忽略此类输入,继续执行
乘客请求不合理,具体为输入时出现连续的相同请求,例如<3><3><3>
或者<5,DOWN><5,DOWN>
,处理方法:程序自动忽略相同的多余输入,继续执行,例如<3><3><3>
过滤为<3>
输入格式:
第一行输入最小电梯楼层数。
第二行输入最大电梯楼层数。
从第三行开始每行输入代表一个乘客请求。
电梯内乘客请求格式:<楼层数>
-电梯外乘客请求格式:<乘客所在楼层数,乘梯方向>
,其中,乘梯方向用UP
代表上行,用DOWN
代表下行(UP、DOWN必须大写)。
当输入“end”时代表输入结束(end不区分大小写)。
输出格式:
模拟电梯的运行过程,输出方式如下:
运行到某一楼层(不需要停留开门),输出一行文本:
Current Floor: 楼层数 Direction: 方向
运行到某一楼层(需要停留开门)输出两行文本:
Open Door # Floor 楼层数
Close Door
设计思路
新增要求:
乘客请求楼层数有误,具体为高于最高楼层数或低于最低楼层数,处理方法:程序自动忽略此类输入,继续执行
乘客请求不合理,具体为输入时出现连续的相同请求,例如<3><3><3>或者<5,DOWN><5,DOWN>,处理方法:程序自动忽略相同的多余输入,继续执行,例如<3><3><3>过滤为<3>
-
楼层有误 在输入前进行判断,如果不合规请求就不入队列
-
乘客请求不合理 在关于队列删除方法时进行处理,比如利用循环删除同一队列相邻请求
类的设计
设计四个类 Elevator(电梯类)、Requestqueue(请求类)、Externalqueue(外部请求类)、Control(控制类)
-
Elevator(电梯类)
属性:有当前楼层currentFloor
、最大楼层maxFloor
、最小楼层minFloor
,状态state
,运行方向direction
方法:包括构造方法初始化楼层范围,获取 / 设置当前楼层、状态、方向等方法 -
Requestqueue(请求类)
属性:内部请求列表internalRequests
、外部请求列表externalRequests
,当前楼层currentFloor
,运行方向direction
。
方法:有构造方法,以及获取 / 设置各类请求列表、添加内外部请求等方法。
-
Externalqueue(外部请求类)
属性:请求楼层floor
、运行方向direction
。
方法:构造方法用于初始化楼层和方向,还有获取楼层和方向的方法。 -
Control(控制类)
属性:关联Elevator
对象和RequestQueue
对象。
方法:构造方法,以及设置电梯、请求队列,判断方向、移动电梯等方法
核心算法getNextFloor() 不变
新增方法determineDirection(),用于在判断下一电梯目标楼层之前简单的判断方向,比如一个请求为空时,方向是朝着非空请求的;如果都为非空时,两个请求一个在上方,一个在下方,不改变方向;如果两个都在上方,电梯方向向上,如果都在下方,电梯方向向下。
judgeDirection()是给出当前楼层和目标楼层来用来确定电梯方向的
关于输出的算法,进行了一点改进
Motion()用来进行每一次从当前楼层到目标楼层的移动
Move()方法每次向下或向上移动一层,并输出 :
Current Floor:楼层数 Direction:方向
openDoor()方法输出 :
Open Doors # Floor 楼层数
Close Door
最后是处理请求的算法
分析
SOURCEMONITOR
- 代码规模:280 行,189 条语句,分支语句占 19.0% ,无注释行,100 条方法调用语句。
- 复杂度:最复杂方法
Controller.getNextFloor()
,复杂度 16 ,最深代码块深度 6 ,平均代码块深度 2.20
第三次迭代
7-40 NCHU_单部电梯调度程序(类设计-迭代)
对之前电梯调度程序再次进行迭代性设计,加入乘客类(Passenger),取消乘客请求类,类设计要求遵循单一职责原则(SRP),要求必须包含但不限于设计电梯类、乘客类、队列类以及控制类,具体设计可参考如下类图。电梯运行规则与前阶段相同,但有如下变动情况:
乘客请求输入变动情况:外部请求由之前的<请求楼层数,请求方向>
修改为<请求源楼层,请求目的楼层>
对于外部请求,当电梯处理该请求之后(该请求出队),要将<请求源楼层,请求目的楼层>
中的请求目的楼层
加入到请求内部队列(加到队尾)
注意:本次作业类设计必须符合如上要求(包含但不限于设计电梯类、乘客类、队列类以及控制类),凡是不符合类设计要求此题不得分,另外,PTA得分代码界定为第一次提交的最高分代码(因此千万不要把第一次及第二次电梯程序提交到本次题目中测试)。
输入格式:
第一行输入最小电梯楼层数。
第二行输入最大电梯楼层数。
从第三行开始每行输入代表一个乘客请求。
电梯内乘客请求格式:<楼层数>
电梯外乘客请求格式:<请求源楼层,请求目的楼层>
,其中,请求源楼层
表示乘客发起请求所在的楼层,请求目的楼层
表示乘客想要到达的楼层。
当输入“end”时代表输入结束(end不区分大小写)。
输出格式:
模拟电梯的运行过程,输出方式如下:
运行到某一楼层(不需要停留开门),输出一行文本:
Current Floor: 楼层数 Direction: 方向
运行到某一楼层(需要停留开门)输出两行文本:
Open Door # Floor 楼层数
Close Door
设计思路
输入
还是用正则表达式将输入数据提取出来,内部请求不变,外部请求第一个数字为源楼层,第二个为目标楼层,新的正则表达式:request.matches("<\d+,\d+>")
修改类
删除外部类,添加passenger类(乘客类),属性有源楼层和目标楼层。内部队列中没有源楼层,所以需要两种构造方法,然后新增方法,判断外部请求的方向。
Queue类中的队列类型改为passenger类型
改进control类
getnextFloor()不需要大改,removeRequests()需要优化,新增方法isAdd()用于判断是否添加外部请求中的目标楼层到内部请求队列最后,其他地方稍微优化即可。
-
isAdd():如果getnextFloor中的返回值是外部请求,则将外部请求中的目标楼层添加至内部队列。
-
优化
明确下一目标楼层,因为原来的motion函数是基于getnextFloor来实现的,如果isAdd中在原本为空的内部队列添加外部请求的目标楼层,那getnextFloor将会是外部请求的两给楼层进行比较,但无论怎样外部请求都是先从源楼层再到目标楼层。所以原本的算法存在一定问题,将getnextFloor返回值固定可以一定解决这一问题
-
removeRequest的改进
在上次迭代其实也有问题,只是当时测试点过了,就是当外部和内部队列头部是相同楼层是,会处理失败,原算法是只要是和移动后的楼层与队列头部请求的楼层相同就删除队列头部,但如果外部队列的方向和电梯方向不同时应当只将内部请求队列的头部删除。
所以要对两队列头部相同的情况进行讨论,如果外部和电梯方向相同就将两个都删除,如果不同就只删除内部队列头部
分析
SOURCEMONITOR
- 语句构成:分支语句占比 21.5% ,方法调用语句有 142 条,注释行占比为 0.0% 。
- 类与方法相关:有 4 个类,平均每个类有 5.57 个方法,每个方法平均 3.95 条语句 。最复杂方法是
Controller.getNextFloor()
,位于 183 行,复杂度为 16 。 - 代码结构复杂度:最深代码块在 197 行,最大块深度为 6 ,平均块深度 2.22 ,平均复杂度 2.59
三.采坑心得
- 第一次迭代时,其实根本没有理解电梯运行逻辑,考虑了几十种情况,一开始的想法其实并不理解为什么外部和内部请求要分开的,一开始我是把他们放在同一个队列当中,在进行排序,一个一个执行,但后面我认为这过于复杂,行不通,知道老师将Main()函数开源,我才明白其实只需要比较两队列头部。但后续处理获取下一目标楼层时,理解出现些许偏差,最后将所有情况总结优化得到最终方法
- 第二次迭代时,由于第一次并未做到职责单一原则,导致将第一次的源码迭代时,很多小细节都要额外处理,关于removeRequest(),一开始运行结果总是报错,无法正确输出,我的想法时当队列中的楼层与下一目标楼层相同时,删除请求,但后续才发现执行removeRequst前先进行了移动,所以移动后的下一目标又有新的楼层,导致出错,这在第一次时是没错的。那是因为所有方法在同一个类中,彼此耦合性太强,导致一个改变则全事变,像这样的细节有很多。
- 第三次迭代时,我犯了个很大的错误就是直接将外部请求中的目标楼层全都直接添加至内部队列末尾,这会导致只有外部队列时,有时外部请求中的目标楼层会先于源楼层执行
如<9><1,5> 运行结果:1->9->5->1,但实际结果1->9->1->5 - 这三次其实一直都有一个问题就是removeRequest时,当两队列头部为同一楼层时,外部请求方向与电梯方向不同时也会将外部请求删除,所以需要将这一情况进行讨论
四.改进建议
-
方法其实设计过于繁杂,设计的其实并不聪明,if-else嵌套层数有点过多,有一种特殊情况我是单独列出来的,不利于后续改进维护,if中判断条件过多,代码稳定性差
-
代码注释少,可读性差,一些方法命名不合理,外人不容易看懂
-
方法尽量做到单一,降低耦合性,类的设计必须遵守单一职责原则,后续的改进可以朝MVC结构改进,即增加View类,将输入输出单独拿出来
五.总结
通过三周的努力,在这个大作业上花了很长时间,也基本完成pta的要求,其中深刻的理解到了类的设计的好处,比如第二次到第三次迭代时,我只需要将主函数输入进行修改,请求类做一些小改,最后在控制类中增加一些新方法完成新算法,改进的地方并不多。但第一次到第二次迭代时就要基本重新设计了。一个将类的职责划分单一的代码,对于后续改进有很大好处,我有很明显的体会。
同时我也清楚,最难的正是类的设计和算法的分析,类的设计完全是开源的,所以核心只要解决算法就可以了,而这几次的核心算法其实都没什么太大变化。关于类的设计,我觉得从一开始就要设计清楚,并且明确类之间的关系,只有只有后续更改的时候就好改进。
在代码测试阶段,我其实在第三次测试屡屡碰壁,公开的两个测试点过了,但还是过不去,后续才发现,个别特殊情况有错误,所以我觉得在今后我应该提高自己的想法的全面性,以及去找寻题目算法的能力,我也是在同学的讲解下才理解电梯调度算法。