电梯博客
前言:
这三次电梯迭代作业中涉及的知识点很多,大部分都可以从老师上课讲的知识点中和线上学习课程中学习到,例如类的设计,Linklist,Arraylist等。题量算不上小,每道题都要几天的时间来打磨,我每次都是一个一个错误的解决问题,错误有很多种,涉及到各种方法的使用,算法的合理性。其中类与类的关系需要处理好,如关联、依赖、聚合等,最主要是算法的设计,设计出算法之后这道题就会变得很通透。首先要理解电梯的运作规律,电梯采用串行处理乘客的请求方式(电梯只按照规则响应请求队列中当前的乘客请求,响应结束后再响应下一个请求),结合样例摸清电梯的运行方向,目标楼层之后就开始设计算法。
设计与分析:
第一次迭代作业:
第一张图是笔者使用powerdesigner生成的类图,第二张图是使用sourcemonitor生成的代码复杂度。
在sourcemonitor图中解释如下:
- Line Number of Most Complex Method:最复杂方法所在行号是 50 ,方法为 Elevator.getTargetFloor() 。**
- Maximum Complexity:最大复杂度为 24 ,对应方法同样是 Elevator.getTargetFloor() ,说明该方法逻辑复杂,维护难度高。**
- Line Number of Deepest Block:最深代码块所在行号为 65 。**
- Maximum Block Depth:最大代码块深度为 6 ,反映代码嵌套层次较深。**
- Average Block Depth:平均代码块深度 2.59 。**
- Average Complexity:平均复杂度 4.89 。**
最复杂的是Elevator.getTargetFloor() 复杂度 24 ,语句数 36 ,最大深度 6 ,调用次数 14
分析第一张类图,从做完三次作业后的角度来看,第一次作业的代码基本所有方法都集中在elevator类中,使代码看起来有点臃肿,且复用性不高,后面几次的作业中就会把功能细化,使每一种类实现自己特有的功能,这也就是我们专业课所需要掌握的知识——类的设计,第一次的作业中的类的设计明显不够好,这也是我们今后需要继续加强完善的部分,因为我们的专业名字就叫面向对象程序设计,重点在于设计,所以一个好的程序,类的设计是必不可少的。第一次代码的核心就是算法,核心在于将外部请求和内部请求添加到两个队列,采用串行处理乘客请求,每次只需要对外部队列的头部请求与内部队列的头部请求进行比较,处理完就删除队列头部请求。在第一次代码getTargetFloor的部分中,我的这段代码存在大量的if-else嵌套语句,导致了整个代码的复杂度很高。这种方法逻辑复杂且难以维护,目标楼层选择算法不够智能,考虑了每一种运行情况,使用if-else语句判断所有情况,导致电梯运行效率低下。而也正是大量使用if-else语句,导致修改算法中的逻辑错误时需要花费大量时间来找出和修正错误,这也正是为什么代码的复杂度越低,可读性和性能越好。
接着便是main的输入部分,
minFloor = Integer.parseInt(list.get(0));//电梯最低楼层
maxFloor = Integer.parseInt(list.get(1));//电梯最高楼层
测试样例中第一行输入起始楼层,第二行输入最高楼层,第三行则是以“end”结尾,接着用循环来读取数据,根据正则表达式(使用if-else判断输入数据属于<\d+,\s*(UP|DOWN)>还是<\d+>来判断输入是否合法),再使用String[] parts = request.replaceAll("[<>]", "").split(","); 将<>和,分别去掉,将数据分别存储进内部队列和外部队列。Ps:正则表达式真的很重要,一定需要学会并熟练使用。
https://www.runoob.com/regexp/regexp-tutorial.html 这个网站是个不错的学习与练习正则表达式的好地方!
第二次迭代作业:
第二次作业中新增了要求:(1)乘客请求不合理,具体为输入时出现连续的相同请求,例如<3><3><3>或者<5,DOWN><5,DOWN>,处理方法:程序自动忽略相同的多余输入,继续执行,例如<3><3><3>过滤为<3>。(2)乘客请求楼层数有误,具体为高于最高楼层数或低于最低楼层数,处理方法:程序自动忽略此类输入,继续执行。第一个要求很好解决,只需要在输入数据时顺便判断数据的合理性就行。第二个要求也比较好实现,设置一个方法,检查队列中是否有相同的数据,利用循环删除同一队列相邻请求就可以了。
这次作业主要是对类的设计做出了要求,要求遵循单一职责原则(SRP),将第一次作业中的电梯类转变成elevator(电梯类)、Requestqueue(请求类)、Externalqueue(外部请求类)、control(控制类)这四个类,将功能细化,让每一个类实现自己的功能。
Elevator 类
• 属性:当前楼层、方向、状态、最大和最小楼层。
• 方法:提供属性的获取和设置方法,有判断楼层有效性的方法。
ExternalRequest 类
• 属性:请求楼层和方向。
• 方法:提供属性的获取方法。
RequestQueue 类
• 属性:内部请求列表和外部请求列表。
• 方法:提供请求列表的获取、设置和添加请求的方法。
Controller 类
• 属性:控制的电梯和请求队列。
• 方法:
- processRequests():处理请求队列中的请求。
- determineDirection():确定运行方向和目标楼层。
- move():移动电梯到目标楼层。
- openDoors():模拟开门关门。
- removeRequests():移除已处理请求。
- getClosest():返回最近楼层。
Main 类
• main 方法:读取输入,验证有效性,初始化电梯、队列和控制器,处理请求。
第一张图是笔者使用powerdesigner生成的类图,第二张图是使用sourcemonitor生成的代码复杂度。
在sourcemonitor的图中解释如下:
- ercent Branch Statements:23.2% ,表明代码中约四分之一的语句是分支语句,意味着存在较多条件判断逻辑,如 if - else 、switch 等语句。
- 平均复杂度(Average Complexity):3.06,这反映了代码中所有方法的平均复杂程度。该数值表明整体代码的复杂程度处于一定水平,并非特别简单,但也没有达到极高的复杂度。
- 最大复杂度(Maximum Complexity):33,对应方法为 Controller.determineDirection(),说明这个方法内部逻辑十分复杂,可能包含大量的条件判断、循环嵌套或者复杂的算法逻辑,维护和理解该方法会有较大难度。
- 平均代码块深度(Average Block Depth):2.57,这意味着代码中方法内部代码块的平均嵌套层次约为 2.57 层,反映出代码存在一定程度的嵌套结构。
- 最大代码块深度(Maximum Block Depth):5,出现在第 201 行,说明代码中存在最深为 5 层嵌套的代码块,这种较深的嵌套可能会影响代码的可读性和可维护性。
从图中可以看出,我的controller类中的determineDirection()的复杂度很高,这是因为笔者使用了大量if-else来根据目标楼层与所在楼层的比较来更改电梯应该的运行方向,导致这一个方法的代码的长度逼近100行,也导致我在寻找代码的逻辑错误时所花费的时间较久,修改代码的难度也不小。因为笔者在测试中遇到了每次变换方向时的那一层的方向会与答案的方向是相反的问题,于是修改成在move()方法中直接比较当前楼层与目标楼层来判断输出DOWN还是UP,导致我的determineDirection()中每次都需要确定方向,而不是像其他同学一样在电梯更改方向时修改即可。而这一改动也正是让笔者的determineDirection()代码复杂度大幅度提高的原因。笔者还是需要继续学习更加简便快捷的计算方法,让自己的代码复杂度和可读性提高。
第三次迭代作业:
这次的作业新增了两个要求:
- 乘客请求输入变动情况:外部请求由之前的<请求楼层数,请求方向>修改为<请求源楼层,请求目的楼层>
- 对于外部请求,当电梯处理该请求之后(该请求出队),要将<请求源楼层,请求目的楼层>中的请求目的楼层加入到请求内部队列(加到队尾)
外部乘客请求发生了变化,乘客的输入不再有方向,而是输入请求源楼层和请求目标楼层,内部楼层则为输入目标楼层。输入数据的正则表达式变为:request.matches("<\d+,\d+>"),这次的类的设计中外部请求类取消了,变成了乘客类,属性具有源楼层和目的楼层,且读入数据后请求目的楼层加入到请求内部队列(加到队尾),这就导致算法需要进一步更新。
Elevator 类
属性:当前楼层、方向、状态、最大和最小楼层。
方法:构造函数初始化最大最小楼层;提供属性的获取和设置方法;isValidFloor 判断楼层是否有效。
RequestQueue 类
属性:内部和外部请求队列,存储 Passenger 对象。
方法:获取请求队列;添加内部和外部请求。
Passenger 类
属性:出发楼层和目标楼层。
方法:构造函数初始化楼层;提供属性的获取和设置方法;getDirection 判断乘客移动方向。
Controller 类
属性:电梯对象和请求队列。
方法:
- processRequests:处理请求,协调各步骤。
- IsAdd:若满足条件,将外部请求转为内部请求。
- judgeDirection 和 detDirection:判断电梯运行方向。
- removeRequests:移除已处理的请求。
- getNextFloor:确定下一个目标楼层。
- determineDirection:确定电梯运行方向。
- motion:使电梯移动到目标楼层并开门。
- move:电梯移动一层并输出信息。
- openDoors:模拟电梯开门关门。
Main 类
main 方法:读取输入,验证格式,创建电梯、请求队列和控制器,处理请求,直到队列为空。
SourceMonitor分析结果的解释
- Percent Branch Statements:25.5%,表明代码中约四分之一多的语句是分支语句,意味着存在相当数量的条件判断逻辑,诸如 if - else、switch 等语句频繁出现。
- 平均复杂度(Average Complexity):2.85,这体现了代码中所有方法的平均复杂程度。该数值说明整体代码复杂程度处于中等水平。
- 最大复杂度(Maximum Complexity):28,对应方法为 Controller.judgeDirection (),说明此方法内部逻辑颇为复杂,涵盖大量条件判断、循环嵌套或者精巧的算法逻辑。
- 平均代码块深度(Average Block Depth):2.33,这意味着代码中方法内部代码块的平均嵌套层次约为 2.33 层,反映出代码具备一定程度的嵌套结构。
- 最大代码块深度(Maximum Block Depth):4,出现在第 185 行,表明代码中存在最深为 4 层嵌套的代码块,这样的嵌套深度可能会对代码的可读性和后期维护造成一定影响。
总的来说,本次作业与第二次的作业总体的计算逻辑基本相同,只需要修改输入和增加修改类即可。目前代码仍然存在一些明显问题。一方面,代码复杂度过高,分支语句占比较大,这表明许多方法内部逻辑过于繁杂。这就需要进一步拆分复杂方法,降低代码块深度,通过合理分配类和方法的职责来践行单一职责原则,让每个类和方法专注于完成一项明确的任务。另一方面,代码中注释极为稀少,极大地降低了代码的可读性,使得他人理解代码意图变得困难。在今后编写代码时,务必高度重视单一职责原则的贯彻以及代码注释的添加,以此提升代码质量和可维护性。
踩坑总结
- 第一次的电梯作业我的代码在Idea上可以运行出正确结果,但是在pta的提交上会出现运行超时的结果,说明思路是对的,但是计算逻辑不够简单高效,导致运算时间过长,达不到最低的运行通过时长,而我就只能通过询问其他通过的同学来改进我的算法。
- 第二次的电梯作业中踩坑较多,首先是算法问题,我的算法虽然在第一次作业已经重写了一遍,可整个算法还是if-else的判断占了大部分,导致代码复杂度较高,性能和可读性较差,找出和解决问题所花的时间较久。在 determineDirection 方法中,存在多处重复的代码片段,在不同条件分支下,多次出现设置电梯方向为 UP 或 DOWN 并返回相关楼层的相似逻辑,导致算法的逻辑较差。
- 第三次的电梯作业缺少对特殊情况的考虑导致代码不够完善,有测试点未通过。
改进建议
- 当前代码在方法设计上存在显著不足,整体逻辑繁杂,不够精巧。if-else 嵌套层数过多,并且针对某一特殊情况进行了单独罗列,这不仅使得代码结构混乱,还极大地降低了代码的可维护性与可扩展性,在后续需求变更时,维护成本会显著增加。同时,if 语句中的判断条件过多,导致代码稳定性不佳,容易出现因条件判断不全面而引发的错误。
- 代码注释极为匮乏,可读性严重不足。部分方法的命名缺乏清晰的表意,对于不熟悉该代码的开发者而言,理解代码意图和业务逻辑困难重重。
- 为了提升代码质量,方法应尽可能遵循单一职责原则,将复杂的功能拆分成多个小的、独立的方法,以此降低代码的耦合性。类的设计也必须严格遵守单一职责原则,确保每个类只负责一项明确的功能。
总结
通过这几次代码迭代通过这三次电梯迭代,我学到了以下内容:
- 掌握了 ArrayList 的基础用法,理解其动态数组特性在存储请求队列等场景中的适用场景,学会通过 add()、remove() 等方法管理数据集合。
- 学习并应用了 LOOK 算法(电梯调度算法),理解其通过双向扫描优化电梯运行效率的核心逻辑,能结合楼层请求动态调整电梯方向。
- 从 C 语言的面向过程思维转向 Java 面向对象编程,学会定义类(如 Elevator、RequestQueue)并设计类间关系(依赖、聚合)。
- 在处理复杂调度逻辑时,通过 “思考逻辑→编写代码→调试纠错” 的循环,逐步提升问题拆解能力,并且学会了自主设计测试用例(如边界楼层、密集请求场景),通过模拟不同输入验证代码健壮性,强化了调试技巧。
也仍然有许多不足,如对单一职责原则的落实还不够透彻,在第二次作业中determineDirection 方法同时承担 “确定方向” 和 “设置电梯状态” 的职责,还有就是代码的复杂度太高,计算逻辑过于依赖if-else的条件判断,导致代码过于复杂,不便于纠错与修改,可读性较低。
通过此次迭代,我认识到编程不仅是实现功能,更是对逻辑清晰性、代码可维护性的追求。后续将注重基础巩固与规范养成,逐步提升工程化开发能力。