前言
- 知识点
- 正则表达式:对于不确定的字符串的输入,用之前学的方法直接对字符依次处理效率太低,而且代码长度很长,不利于阅读,较差的可读性也会使得后期修改代码变得复杂,因此在本次实验使用了正则表达式来进行字符串的处理
- JavaAPI的使用:这个部分主要是指正则表达式和LinkedList相关的封装类,掌握重要的工具类可以大大提高效率
- 面向对象设计原则与方法:
- SRP:单一职责原则(Single Responsibility Principle,SRP)
- LoD:迪米特法则 (Law of Demeter)
单一职责原则:
定义:一个类应该只有一个引起其变化的原因
即一个类只负责一项职责。
优点:
提高类的可理解性和可维护性。
减少类之间的耦合,增强内聚性。
便于单元测试和代码复用。
迪米特法则:
一个对象应当对其他对象有尽可能少的了解
只与直接的朋友通信。简单说就是:不要和陌生人说话。
表现1.只与直接的朋友通信
表现2.避免出现a.b().c().d()这样的链式调用
- 题量:每次的题量不算大,但是如果要认真完完全全按照要求完成的话,会非常花费时间
- 难度情况:难度情况,如果只是要求通过题目的话,每一次的题目大概是一个较难的状态,但是如果要求每次都按照类设计来写,并且要通过所有的情况测试,那题目难度则会陡升,要从C语言的习惯转换到面向对象的设计理念,对于我们来说还是有较大的难度
设计与分析
下文的每一次电梯调度的题目的代码将分为两个部分分析
分别是代码逻辑分析
以及结合代码数据分析
第一次电梯调度代码分析
代码逻辑分析
这部分主要是对代码逻辑的分析解释
以及与代码逻辑上存在的问题
I、代码整体结构
- Main类:处理输入解析,构建三个请求队列(内部请求、外部楼层、外部方向)
- Chain类:单向链表结构,用于存储请求队列
其实根据最后的问题分析来看,直接使用数组会更简单,而且java不支持支持也让手写链表相对更困难) - Elevator类:模拟电梯运行,包含调度核心逻辑(每一层就调用一次)
II、输入处理逻辑
- 正则表达式解析:
pattern
匹配"END"结束输入。pattern2
提取楼层数字。pattern3
匹配"UP"或"DOWN"方向。
- 队列入队出队规则:
- 若输入行包含方向(UP/DOWN),将楼层加入
outQue
,方向加入outDirQue
。 - 否则,楼层加入
inQue
(内部请求)。
- 若输入行包含方向(UP/DOWN),将楼层加入
III、链表(Chain类)逻辑
本来这部分是考虑到重复元素的删除的,但是实际上老师在这次题目测试点上没有设置这个需求,所以这部分其实没有发挥作用(小声~~
- 关键方法:
push()
:添加新节点,若值与尾节点相同则跳过(避免重复)。forcedPush()
:强制添加节点,不检查重复。top()
:返回头节点的下一个节点值(若链表空则返回0)。
IV、电梯调度逻辑(Elevator类)
1. move()方法核心流程
-
主循环条件:
while (inQue.top() != 0 || outDirQue.top() != 0)
依赖
top()
返回0判断队列是否为空 -
目标楼层更新逻辑:
- 到达当前目标楼层后,尝试处理内部和外部请求。
- 处理相同楼层请求:
可能误判不同队列中的相同楼层请求为同一请求。if (inQue.top() == outQue.top()) { ... }
- 队列更新逻辑:
直接修改next
指针(并非~~)(如outQue.next = outQue.next.next
),有概率导致链表断裂或未处理所有节点。
-
方向与目标计算:
-
UpRequest()和downRequest()方法:
尝试合并内部和外部请求,但逻辑复杂且存在问题:- 未完全实现LOOK调度算法,有的地方仍然会出错。
-
目标选择错误:
例如,在电梯上行时,可能错误选择下行方向的外部请求作为目标。
这里是通过老师给的额外给的测试点发现的错误,在某些特殊的情况下会不遵循LOOK算法
-
2. 输出逻辑
- 每次移动后输出当前楼层和方向:
但System.out.println("Current Floor: "+temFloor+" Direction: UP");
temFloor
为移动前楼层,导致输出与实际楼层不同步,但这也是为了通过题目的一种妥协,想了很久没想到除了这种方法还有什么办法可以解决问题
V、关键问题总结
-
调度算法可能失效:
- 在有的测试样例下未正确处理电梯运行方向与请求方向的关系。
-
状态更新错误:
方向切换逻辑不严谨,可能导致电梯反复震荡。(这应该是绝大多数人超时的主要原因了)(悲伤
VI、总结
该代码在一定程度上实现电梯了调度,但存在以下问题:
调度算法不完全正确:自定义逻辑复杂且不完善。
链表功能存在问题:链表操作易出错,状态管理混乱[1]。
电梯逻辑的文字解释:
这里仅解释我一开始的想法与最终通过测试点的想法
1.我第一次写的代码的逻辑是判断最近的请求,如果是同向的
就优先处理同向,看着似乎是和题目要求的是一样的,但其实还是有本质区别的,这里距离的优先级要高于方向
在题目要求中是先处理完一个方向再转向,再去处理其他的请求,而我的写法是先处理目前所走的方向上的
根据目前所走的方向判断是否同向,不会进行单独转向的操作
我第一次写的代码主要分为三个部分,首先记录上一层楼梯的状态,每到新的一层楼梯就输出上一层楼梯的状态
加上现在的楼数,这样子就解决了输出当前楼梯运行状态的效果
然后就是判断电梯是否开门,以及接下来的运行方向,判断电梯是否开门,我分了多个变量来存储状态
总之我用较多的变量去详细的存储了每一个可能出现的状态,这样代码虽然复杂
但确实能够解决问题,当然这种方法不太好
造成这种现象的主要是有可能电梯是在外部,因为外部请求而停住,也有可能是因为内部请求而停住,而因为外部请求而停住
是有可能向上,也有可能向下的,这样子我就需要有多个变量来存放请求的类型
总之,我通过一种暴力的方法(多用变量)解决了这个问题,然后便是计算接下来电梯的运行方向,之所以逻辑有问题,正是这里有问题
这里我做的判断是在一个方向上判断离它最近的
即使某个队列对头的方向和他,目前电梯运行的方向不一致,他也会优先选择最近的,就比如现在电梯是在向上行驶
因为目前在五楼对头有两个请求,分别是四楼和七楼,并且都是向上的请求,那么他会优先走四楼
总之,这一部分还是逻辑的问题
2。然后就是提交通过的代码逻辑,这部分逻辑我是按照要求写的,只在之前的基础上改了一些,改为了如果当前方向上有请求楼层
那么处理请求如果没有,那就转一个方向,再按照原来的函数处理请求,因为一个方向如如果没有
那么另一个方向一定会有它对应的请求,虽然说逻辑上有较大改变,但是代码没有很大上的改变,基本就是根据已经写过的代码稍微修改了一下
我在写这些代码的时候,基本上没出现过什么太大的问题,主要的问题就是理解错了需求,所以其实这里并没对逻辑上的错误进行太多分析
结合代码数据分析
该部分主要包含代码存在的问题与对自身代码的改进建议代码质量分析[2]
• 类与接口:仅有3个
这个数据说明每个类负责的责任太多,没有做到单一责任原则,REMAIN
• 方法/类:3.33个
每个类包含约3个方法,分布较均衡(实际上是因为一个方法里的代码写的太复杂了),主要的方法过于复杂(如Elevator.move()
复杂度高达81)。
• 分支语句占比:35.3%
控制逻辑较复杂,包含大量if/else
• 注释覆盖率:11.8%
低于一般推荐值(15-20%),关键逻辑缺乏注释,影响可维护性。
• 平均方法复杂度:11.50
(Cyclomatic Complexity,即老师讲的圈复杂度)高于推荐值(通常≤10),表明方法逻辑嵌套过深,需重构。
• 最复杂方法:Elevator.move()
复杂度81,行号189,包含多层嵌套循环或条件分支,是重点优化对象。
块与复杂度分析
• 最大块深度:9+
代码块嵌套过深(如if
内嵌for
再内嵌if
等),逻辑混乱。这里应该拆分为子方法,尽可能的满足单一职责原则。
• 平均块深度:4.53
整体嵌套较深,需简化条件逻辑或编写辅助方法(利用多个类实现单一职责原则,并且需要降低类中方法的平均复杂度)。
可视化图表解析
• Kiviat图:
展示了多个指标的相对关系,如% Comments
低可能与高Avg Complexity
相关,说明复杂代码缺乏注释,增加修改难度。
• 块直方图:
大部分语句集中在深度4-5的块中,但存在深度9+的极端值(主要是Elevator.move()
这个方法),需优先优化(其实已经说过很多遍了)。
总而言之,这次的代码没有用到面向对象的思维,而是更多的面向过程,大量的if语句写在一个方法里,使得可读性非常差,虽然代码写的糟糕,但是逻辑上来说也算是通过了题目,逻辑思考一定程度上得到了训练,这次题目也算是从C语言到Java的过渡了
下面是SourceMonitor给出的分析以及本次代码的类图
第二次电梯调度代码分析
代码逻辑分析
I、代码整体结构
- Main类:输入信息,初始化电梯、请求队列和控制器。
- Elevator类:封装电梯,如当前楼层、方向、楼层范围。
- RequestQueue类:管理内部请求(
LinkedList<Integer>
)和外部请求(LinkedList<ExternalRequest>
)。(不用再像上次一样手搓链表了) - Controller类:调度核心逻辑(移动、停靠、方向决策),符合LoD原则。
- ExternalRequest类:封装外部请求的楼层和方向。
II、改进与遗留问题
改进点 | 第一次代码问题 | 第二次代码表现 |
---|---|---|
耦合性 | 各部分耦合性高 | 明确分为电梯、队列、控制器 |
调度算法 | 未标准化 | 实现方向优先级,基本遵循LOOK算法 |
第二次的设计逻辑主要还是使用if语句做判断,但是相对于第一次更有条理
这部分这里不赘述了,但是相对于上次这次多了一个删除重复元素的步骤
这个步骤主要是依靠LinkedList和其对应的迭代器完成的
(实际上这个迭代器挺不好用,至少没有C++的迭代器方便)
完成删除重复元素在逻辑上很简单,只要在每次插入新元素前,用迭代器移动到LinkedList的末尾,判断新元素是否与之相同,若相同,则不插入即可
结合代码数据分析
质量指标分析
参数 | Checkpoint1 | Checkpoint2 | 变化比例 | 说明 |
---|---|---|---|---|
代码行数 (Lines) | 439 | 355 | ↓ 19% | 代码更简洁,冗余减少。 |
语句数 (Statements) | 286 | 194 | ↓ 32% | 逻辑更高效,冗余操作减少。 |
分支语句比例 (%) | 35.3 | 26.3 | ↓ 26% | 条件逻辑简化,可维护性提高。 |
方法调用语句数 | 60 | 155 | ↑ 158% | 模块化增强,实际复杂度明显降低。 |
注释行比例 (%) | 11.8 | 5.4 | ↓ 54% | 注释减少了影响可读性,但其他优化弥补了这一点。 |
类与接口数 | 3 | 5 | ↑ 67% | 方法与类的职责更清晰,符合单一职责原则。 |
每个类的方法数 | 3.33 | 6.40 | ↑ 92% | 方法细化,功能更专注。 |
方法平均语句数 | 25.70 | 4.59 | ↓ 82% | 方法简短,可读性和可维护性显著提升。 |
最大复杂度 (Cyclomatic) | 81 | 8 | ↓ 90% | 某些极端的方法(如 Elevator.move() (笑))被重构,逻辑简化。 |
最大块深度 | 9+ | 5 | ↓ 44% | 嵌套层级减少,结构得到优化。 |
平均块深度 | 4.53 | 1.71 | ↓ 62% | 代码逻辑更清晰,降低理解难度。 |
平均复杂度 | 11.50 | 1.88 | ↓ 84% | 整体代码复杂度大幅降低,修改难度降低。 |
踩坑心得
这几次的题目除了第一次提交了很多次,另外两次基本都在几次之内就过了,所以这里着重针对第一次题目谈谈我遇到的问题与心得体会
点击折叠代码
下面是第一次的代码,不建议打开
你来晚了,代码已经被删掉了~~
不论是在哪一次题目集中,我遇到的最大的问题都是理解需求 我在理解需求上犯过很多次错,尤其是第一次,在认真理解需求之前,我尝试了很多种理解,写了很多份代码,但是都没有通过测试点,这也导致我的代码改了又改,结构越来越复杂,我写的方法本就复杂,又不符合SRP,这导致我的代码修改起来又很艰难,两者相互影响,现在我认识到在写题目之前应该先认真考虑题目的需求,再根据需求设计不同的类,要仔细考虑不同类之间的关系,是依赖关联聚合还是组合?明确不同类的功能,在确定类的功能之后,再确定各种方法的功能,最好画一个类图,画完类图之后捋清关系,然后就可以编写代码了,以这样的流程编写代码,虽然前期准备时间长,但是总的花费时间比较少,因为一旦做好了前期准备,后期编写代码的工作就会很迅速,这样子总体来说其实是节省了时间,而且这样子写出来的代码易于维护与迭代,如果第一次能写好这样的代码,那么后面两次其实就不会花费多少时间
改进建议
第一次题目集中,我把主要的功能全写在了一个函数里面,所有的功能只在一个方法里面实现,If语句数量到达了90之多,圈复杂度高的惊人,而且是写在电梯类里面,对于这次代码,除了将不同的功能拆分之外,还应该将和电梯相关性较低的功能拆出来,放在另一个类中,这个类是控制类,通过控制类来存放一些相关功能的方法,这样子可以降低代码的耦合性,还可以增加代码的可扩展性
在明确各个类和方法的分工的时候,要注意满足SRP和LoD原则,这样才方便后期实现代码的可扩展与可维护
对于第二次的代码,第二次的代码我基本上按照老师给的类图来设计,并且用相对较少的代码量实现了上一次的功能,(这次代码量仍然是300以上,但是写完老师提供的的类图对应的代码就已经到达了200多行,所以只有100多行的新增的代码)但这次的设计仍存在一些问题,在主要逻辑上还是使用的是if语句,这些部分仍然可以优化,应该加深对题目的理解,用逻辑上更优的方法来解决问题,并且虽然这次设计能够解决更多数的情况,但是仍有一些特殊的样例是无法通过的(这些样例是在偶然间测出来的,比如奇怪的某些一层停留多次的样例)
另外,对于这种代码量比较大的题目,我还应该写好注释,主要是在一些复杂和关键的地方写好相应的注释,这样子可以为后面的修改节省时间,如果没有注释,就需要重新花费时间去理解之前写的代码,而且从工程角度上来说,不便于与他人协作修改。虽然这次不是多人写作的项目,但是还是要养成好习惯,在关键处用简洁扼要的注释解释相应位置的功能。当然,这次题目我觉得更重要的还是要养成写代码要明确清晰分工,明确类和类之间的关系,方法和方法之间的职责,这样子的话,其实不写注释也易于看懂
总结
- 从面向过程到面向对象:
通过三次电梯调度题目集的训练,我逐渐从C语言的面向过程思维转向Java的面向对象设计,理解了类封装、职责分离的重要性。 - 设计原则的应用:
在几次实验中学习了单一职责原则(SRP)和迪米特法则(LoD),认识到高内聚、低耦合对代码可维护性的关键作用,并且争取将其应用到之后几次的实验中。 - 正则表达式:
用于高效处理复杂字符串输入(如电梯请求解析),替代了传统的字符遍历方法。 - 代码质量分析:
学会了通过SourceMonitor等工具评估代码复杂度,能够更好的分析自己的代码 - 代码优化意识:
第一次作业的“巨型方法”到第三次作业的模块化设计,体会到代码优化的价值。 - 需求明确(建议):
在题目发布时提供更明确的需求说明(如增加测试样例),减少初期理解偏差。