前言

  • 知识点
    • 正则表达式:对于不确定的字符串的输入,用之前学的方法直接对字符依次处理效率太低,而且代码长度很长,不利于阅读,较差的可读性也会使得后期修改代码变得复杂,因此在本次实验使用了正则表达式来进行字符串的处理
    • JavaAPI的使用:这个部分主要是指正则表达式和LinkedList相关的封装类,掌握重要的工具类可以大大提高效率
    • 面向对象设计原则与方法:
      • SRP:单一职责原则(Single Responsibility Principle,SRP)
      • LoD:迪米特法则 (Law of Demeter)

        单一职责原则:
​        ​定义​​:一个类应该只有一个引起其变化的原因
        即一个类只负责一项职责。
        优点​​:
        提高类的可理解性和可维护性。
        减少类之间的耦合,增强内聚性。
        便于单元测试和代码复用。
        迪米特法则:
        一个对象应当对其他对象有尽可能少的了解
        只与直接的朋友通信。简单说就是:不要和陌生人说话。
        表现1.只与直接的朋友通信
        表现2.避免出现a.b().c().d()这样的链式调用
  • 题量:每次的题量不算大,但是如果要认真完完全全按照要求完成的话,会非常花费时间
  • 难度情况:难度情况,如果只是要求通过题目的话,每一次的题目大概是一个较难的状态,但是如果要求每次都按照类设计来写,并且要通过所有的情况测试,那题目难度则会陡升,要从C语言的习惯转换到面向对象的设计理念,对于我们来说还是有较大的难度

设计与分析

下文的每一次电梯调度的题目的代码将分为两个部分分析

分别是代码逻辑分析以及结合代码数据分析

第一次电梯调度代码分析


代码逻辑分析

这部分主要是对代码逻辑的分析解释以及与代码逻辑上存在的问题

I、代码整体结构

  1. Main类:处理输入解析,构建三个请求队列(内部请求、外部楼层、外部方向)
  2. Chain类:单向链表结构,用于存储请求队列其实根据最后的问题分析来看,直接使用数组会更简单,而且java不支持支持也让手写链表相对更困难)
  3. Elevator类:模拟电梯运行,包含调度核心逻辑(每一层就调用一次)

II、输入处理逻辑

  • 正则表达式解析
    • pattern匹配"END"结束输入。
    • pattern2提取楼层数字。
    • pattern3匹配"UP"或"DOWN"方向。
  • 队列入队出队规则
    • 若输入行包含方向(UP/DOWN),将楼层加入outQue,方向加入outDirQue
    • 否则,楼层加入inQue(内部请求)。

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、关键问题总结

  1. 调度算法可能失效

    • 在有的测试样例下未正确处理电梯运行方向与请求方向的关系。
  2. 状态更新错误

    方向切换逻辑不严谨,可能导致电梯反复震荡。(这应该是绝大多数人超时的主要原因了)(悲伤

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、代码整体结构

  1. Main类:输入信息,初始化电梯、请求队列和控制器。
  2. Elevator类:封装电梯,如当前楼层、方向、楼层范围。
  3. RequestQueue类:管理内部请求(LinkedList<Integer>)和外部请求(LinkedList<ExternalRequest>)。(不用再像上次一样手搓链表了)
  4. Controller类:调度核心逻辑(移动、停靠、方向决策),符合LoD原则
  5. 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等工具评估代码复杂度,能够更好的分析自己的代码
  • 代码优化意识
    第一次作业的“巨型方法”到第三次作业的模块化设计,体会到代码优化的价值。
  • 需求明确(建议)
    在题目发布时提供更明确的需求说明(如增加测试样例),减少初期理解偏差。

  1. 这里最开始我不知道不需要去除重复数据,于是写个很多个和链表有关的方法,但是最后都没有用到,总之对于链表的方法,我写的也很乱,很容易由于指向NULL导致报错 ↩︎

  2. 由于我暂时不确定如何根据这些测试出来的数据评价代码的质量,因此该部分的评价标准来源于网络上的我了解到的大致评价标准 ↩︎

posted on 2025-04-20 20:39  Autine  阅读(20)  评论(0)    收藏  举报