Elevator三次方

目录

一.前言

二.设计与分析

三.踩坑经验

四.改进分析

五。总结

 

 

一.前言

本次有关电梯调度程序实现的三次题目集让我们系统性的学习并巩固的有关类与设计的思维与方法,基于电梯LOOK算法迭代性的考察了学生拆解问题并逐块解决的思维逻辑能力,同时锻炼了我们将生活实例转化为可编码运用的抽象思维,并初步培养了面向对象思维逻辑。

习题集5是对电梯程序的初步设计与分析,整理出电梯运行的初步逻辑,要求能够正确并合理的分散方法中的功能,避免因功能过于复杂导致的逻辑混乱,同时不利于后期的修改与优化。

习题集6是对题集5的初次迭代,要求将题集5的程序分化为多个类,并执行不同的职责与功能,此次习题考察了学生对于不同的类之间的关系的判断与不同类之间方法的调用,同时要求考虑到需求重复的特殊情况,要求程序能够判断有效需求再进行输出,本次题集较之前一次难度与复杂度均有较大提升,进一步锻炼了学生面向对象编程能力与逻辑思考能力。

题集7则再次进行了迭代,在前两次的基础上改变了输入方式要求学生更细致的考虑队列中请求的删除与增加,同时将外部请求类改为乘客类则更贴近现实需求也更符合逻辑,要求我们能够更细致的解决复杂问题的能力。

二.设计与分析

题目要求:

设计一个电梯类,具体包含电梯的最大楼层数、最小楼层数(默认为1层)当前楼层、运行方向、运行状态,以及电梯内部乘客的请求队列和电梯外部楼层乘客的请求队列,其中,电梯外部请求队列需要区分上行和下行。
电梯运行规则如下:电梯默认停留在1层,状态为静止,当有乘客对电梯发起请求时(各楼层电梯外部乘客按下上行或者下行按钮或者电梯内部乘客按下想要到达的楼层数字按钮),电梯开始移动,当电梯向某个方向移动时,优先处理同方向的请求,当同方向的请求均被处理完毕然后再处理相反方向的请求。电梯运行过程中的状态包括停止、移动中、开门、关门等状态。当电梯停止时,如果有新的请求,就根据请求的方向或位置决定移动方向。电梯在运行到某一楼层时,检查当前是否有请求(访问电梯内请求队列和电梯外请求队列),然后据此决定移动方向。每次移动一个楼层,检查是否有需要停靠的请求,如果有,则开门,处理该楼层的请求,然后关门继续移动。
使用键盘模拟输入乘客的请求,此时要注意处理无效请求情况,例如无效楼层请求,比如超过大楼的最高或最低楼层。还需要考虑电梯的空闲状态,当没有请求时,电梯停留在当前楼层。
请编写一个Java程序,设计一个电梯类,包含状态管理、请求队列管理以及调度算法,并使用一些测试用例,模拟不同的请求顺序,观察电梯的行为是否符合预期,比如是否优先处理同方向的请求,是否在移动过程中处理顺路的请求等。为了降低编程难度,不考虑同时有多个乘客请求同时发生的情况,即采用串行处理乘客的请求方式(电梯只按照规则响应请求队列中当前的乘客请求,响应结束后再响应下一个请求),具体运行规则详见输入输出样例。

输入格式:

第一行输入最小电梯楼层数。
第二行输入最大电梯楼层数。
从第三行开始每行输入代表一个乘客请求。

  • 电梯内乘客请求格式:<楼层数>
  • 电梯外乘客请求格式:<乘客所在楼层数,乘梯方向>,其中,乘梯方向用UP代表上行,用DOWN代表下行(UP、DOWN必须大写)。
  • 当输入“end”时代表输入结束(end不区分大小写)。

输出格式:

模拟电梯的运行过程,输出方式如下:

  • 运行到某一楼层(不需要停留开门),输出一行文本:
    Current Floor: 楼层数 Direction: 方向
  • 运行到某一楼层(需要停留开门)输出两行文本:
    Open Door # Floor 楼层数
    Close Door

关键代码分析:

//电梯运行主方法

    public void run() {
        // 初始状态输出
        System.out.println("Current Floor: 1 Direction: UP");

        // 如果电梯静止,确定初始方向
//        printRequests();
        if (direction.equals("STILL")) {
            int nextFloor = findNextFloor();
            direction = nextFloor > 1 ? "UP" : "DOWN"; // 当前是1层,首次必为UP
        }

        // 主循环:当有任何请求未处理时继续运行
        while (!internalRequests.isEmpty() || !externalRequests.isEmpty() ) {
            // 如果电梯静止,寻找下一个目标楼层
            if (direction.equals("STILL")) {
                int nextFloor = findNextFloor();
                if (nextFloor > currentFloor) {
                    direction = "UP";
                } else if (nextFloor < currentFloor) {
                    direction = "DOWN";
                } else {
                    // 目标楼层就是当前楼层,直接处理
                    processCurrentFloor();
                    continue;
                }
            }

            // 根据方向移动电梯
            if (direction.equals("UP")) {
                moveUp();
            } else if (direction.equals("DOWN")) {
                moveDown();
            }
        }

        // 所有请求处理完毕,重置状态
        direction = "STILL";
        status = "STOP";
    }

这是电梯运行的主方法,确定了电梯的底层运行逻辑,包括确定电梯运行状态,寻找下一楼层和处理楼层。

// 寻找下一个目标楼层
private int findNextFloor() {
    if (direction.equals("UP")) {
        // 上行时找上方最近的请求
        int nextFloor = maxFloor + 1;  // 初始设为超出范围的值

        // 检查内部请求
        if (!internalRequests.isEmpty() && internalRequests.peek() > currentFloor) {
            nextFloor = internalRequests.peek();
        }

        // 检查上行外部请求
        if (!externalRequests.isEmpty() && externalRequests.peek().floor > currentFloor) {
            nextFloor = Math.min(nextFloor, externalRequests.peek().floor);
        }

        return nextFloor <= maxFloor ? nextFloor : currentFloor;

    }
    else if (direction.equals("DOWN")) {
        // 下行时找下方最近的请求
        int nextFloor = minFloor - 1;  // 初始设为超出范围的值

        // 检查内部请求
        if (!internalRequests.isEmpty() && internalRequests.peek() < currentFloor) {
            nextFloor = internalRequests.peek();
        }

        // 检查下行外部请求
        if (!externalRequests.isEmpty() && externalRequests.peek().floor < currentFloor) {
            nextFloor = Math.max(nextFloor, externalRequests.peek().floor);
        }

        return nextFloor >= minFloor ? nextFloor : currentFloor;
    }
    else {
        // 静止时找最近的任何请求
        if (!internalRequests.isEmpty()) {
            return internalRequests.peek();
        }
        if (!externalRequests.isEmpty()) {
            return externalRequests.peek().floor;
        }
        return currentFloor;  // 没有请求则保持当前楼层
    }
}

 

 

这是寻找下一个目标楼层的方法,其中分不同情况查找了电梯在不同运行状态下内外部首项请求的要求方向和比较在内外首项请求都满足时的优先满足哪一项的辨析,以及内外队列是否为空的不同情况的不同分析。

// 判断是否应该在当前楼层停止

private boolean shouldStop(int floor, String dir) {
    boolean shouldOpen = false;

    // 处理内部请求(无条件停靠)
    if (!internalRequests.isEmpty() && floor == internalRequests.peek()) {
        internalRequests.remove();
        shouldOpen = true;
    }

    // 处理外部请求
    if (!externalRequests.isEmpty() && floor == externalRequests.peek().floor) {
        ExternalRequest req = externalRequests.peek();

        // 情况1:方向匹配时直接停靠
        if (dir.equals(req.direction)) {
            externalRequests.remove();
            shouldOpen = true;
        }
        // 情况2:方向不匹配,但内部请求为空或者满足特殊条件
        else if (internalRequests.isEmpty() ||
                (dir.equals("UP") && !internalRequests.isEmpty() && internalRequests.peek() < currentFloor) ||
                (dir.equals("DOWN") && !internalRequests.isEmpty() && internalRequests.peek() > currentFloor)) {
            externalRequests.remove();
            shouldOpen = true;

            // 需要转向
            direction = req.direction;
        }
    }

    return shouldOpen;
}
在实行是否需要停靠的方法中我对两个队列的首项满足要求时进行了对方向的判断,根据LOOK算法中优先完成同方向请求的逻辑指导需要满足一些特殊情况下的方向匹配与检查,如此处if语句的条件判断
// 情况2:方向不匹配,但内部请求为空或者满足特殊条件
        else if (internalRequests.isEmpty() ||
                (dir.equals("UP") && !internalRequests.isEmpty() && internalRequests.peek() < currentFloor) ||
                (dir.equals("DOWN") && !internalRequests.isEmpty() && internalRequests.peek() > currentFloor))

 

考虑了只有在内部请求为空或内部请求的首项请求也要求转向时才考虑外部请求首项的要求方向与当前运行方向不一致时的请求。

 

根据setup中提供的数据分析(这里主要分析Elevator类)

  1. 代码结构特征:

行数共322行

分支语句占比23.9%

可执行语句数量117

类中的方法数7

注释百分比为23.8%较为正常

  1. 代码复杂度

最大复杂度1

最大深度4

平均深度 1.32

平均复杂度1

 

优点分析

  • 适度注释:注释行百分比为8% ,说明代码中有一定量的注释,这有助于开发人员理解代码逻辑,特别是对于维护人员或新接手该项目的人员,能降低理解代码的难度。
  • 方法平均语句数较合理:平均每个方法的语句数(Avg Stmts/Method )为71 ,意味着方法功能相对聚焦,没有出现一个方法包含大量复杂语句的情况,代码的内聚性较好,可读性和可维护性相对较高。
  • 类方法数量适中:每个类的方法数(Methods per Class)为00 ,说明类的职责划分相对明确,没有出现类过于臃肿(包含过多方法)或过于单薄(方法过少)的情况,符合良好的面向对象设计原则。

缺点分析

  • 分支语句占比相对较高:分支语句百分比(Percent Branch Statements )为9% ,较高的分支比例可能意味着代码逻辑较为复杂,存在较多的条件判断。过多的条件判断会使代码的执行路径变得复杂,降低代码的可读性和可维护性,也增加了测试的难度。
  • 存在复杂度较高的方法:图中明确指出了最复杂方法的行号(Line Number of Most Complex Method )和方法名(Name of Most Complex Method ) ,说明代码中存在单个方法复杂度较高的情况。复杂度过高的方法不利于理解、测试和修改,容易成为代码维护过程中的 “痛点”。

 

 

第六次题目集分析

 

电梯运行规则与前阶段单类设计相同,但要处理如下情况:

  • 乘客请求楼层数有误,具体为高于最高楼层数或低于最低楼层数,处理方法:程序自动忽略此类输入,继续执行
  • 乘客请求不合理,具体为输入时出现连续的相同请求,例如<3><3><3>或者<5,DOWN><5,DOWN>,处理方法:程序自动忽略相同的多余输入,继续执行,例如<3><3><3>过滤为<3>

关键代码分析

public void deleteReputeIn() {
    List<Integer> deleteList = new ArrayList<>(elevator.getRequestQueue().getInternalRequests());
    ListIterator<Integer> iterator = deleteList.listIterator();
    if (iterator.hasNext()) {
        Integer key = iterator.next();
        while (iterator.hasNext()) {
            Integer current = iterator.next();
            if (current.equals(key)) {
                iterator.remove();
            } else {
                key = current;
            }
        }
    }
    elevator.getRequestQueue().getInternalRequests().clear();
    elevator.getRequestQueue().getInternalRequests().addAll(deleteList);
}

  

deleteReputeEx()方法与前者类似,此处不展示。

在该方法中先创建了一个与内(外)部请求相同的队列,然后使用了迭代器,对内(外)部相同且相邻的请求进行了遍历判断与移除,最后清除了原有内部序列并将更新后的新数列添加入内部序列完成对重复请求的筛查。

其他主要逻辑去前一次代码基本相同此处不展开分析

Controller类的细分图如下

  • 缺点
  • 分支语句占比高:分支语句百分比达3% ,比 “elevator.java” 更高,意味着代码中条件判断复杂,执行逻辑路径繁多,易导致代码混乱,测试覆盖难度大。
  • 方法复杂度高:最大复杂度达到 20 ,最复杂方法 “Controller.findNextFloor ()” 行号为 174 ,说明存在逻辑极为复杂的方法,不利于代码理解、修改和调试。
  • 类方法数量偏多:每个类的方法数为00 ,相比 “elevator.java” 的 7.00 更多,可能意味着类的职责不够单一,违反高内聚原则,使类功能混杂。
  • 优点
  • 方法平均语句数尚可:平均每个方法的语句数为60 ,和 “elevator.java” 的 6.71 相比差距不大,说明方法内语句数量未过度膨胀,一定程度上保证了方法的聚焦性。

与 “elevator.java” 比较

  • 复杂度方面:“Controller.java” 最大复杂度 20 高于 “elevator.java” ,且最复杂方法行号更高,说明 “Controller.java” 代码在方法复杂度上更突出,问题更严重。
  • 注释情况:“elevator.java” 有 23.8% 的注释行,而 “Controller.java” 完全无注释,“Controller.java” 在代码可理解性上劣势明显。
  • 分支语句:“Controller.java” 分支语句百分比 31.3% 高于 “elevator.java” 的 23.9% ,代码逻辑复杂程度更高。
  • 类方法数量:“Controller.java” 每个类方法数 15.00 多于 “elevator.java” 的 7.00 ,类的职责划分上 “Controller.java” 更可能存在混乱问题。

总体而言此次迭代并未达到使复杂度减小的优化反而增加了复杂度,该点值得反思。

 

 

 

第七次题目集

 

 

对之前电梯调度程序再次进行迭代性设计,加入乘客类(Passenger),取消乘客请求类,类设计要求遵循单一职责原则(SRP),要求必须包含但不限于设计电梯类、乘客类、队列类以及控制类,具体设计可参考如下类图。

 

电梯运行规则与前阶段相同,但有如下变动情况:

  • 乘客请求输入变动情况:外部请求由之前的<请求楼层数,请求方向>修改为<请求源楼层,请求目的楼层>
  • 对于外部请求,当电梯处理该请求之后(该请求出队),要将<请求源楼层,请求目的楼层>中的请求目的楼层加入到请求内部队列(加到队尾)

主要改动的方法:

// 判断是否应该在当前楼层停止
private boolean shouldStop(int floor, Direction dir) {
    boolean shouldOpen = false;

    // 检查内部请求
    if (elevator.getRequestQueue().hasInternalRequests() &&
            floor == elevator.getRequestQueue().peekInternalRequest()) {
        elevator.getRequestQueue().removeInternalRequest();
        shouldOpen = true;
    }

    // 检查外部请求
    if (elevator.getRequestQueue().hasPassengerRequests() &&
            floor == elevator.getRequestQueue().peekPassengerRequest().getSourceFloor()) {
        Passenger req = elevator.getRequestQueue().peekPassengerRequest();

        if (dir == req.getDirection()) {
            // 方向一致,接乘客并添加目标楼层
            elevator.getRequestQueue().addInternalRequest(req.getDestinationFloor());
            elevator.getRequestQueue().removePassengerRequest();
            shouldOpen = true;
        } else if (!elevator.getRequestQueue().hasInternalRequests() ||
                (dir == Direction.UP && elevator.getRequestQueue().hasInternalRequests() &&
                        elevator.getRequestQueue().peekInternalRequest() < elevator.getCurrentFloor()) ||
                (dir == Direction.DOWN && elevator.getRequestQueue().hasInternalRequests() &&
                        elevator.getRequestQueue().peekInternalRequest() > elevator.getCurrentFloor())) {
            // 特殊情况处理(如无其他请求或需要改变方向)
            elevator.getRequestQueue().addInternalRequest(req.getDestinationFloor());
            elevator.getRequestQueue().removePassengerRequest();
            shouldOpen = true;
            elevator.setDirection(req.getDirection());
        }
    }

    return shouldOpen;
}

  

 

此处因为输入方法的改变在接到外部请求的乘客后将其目标楼层添加进了内部序列,如

if (dir == req.getDirection()) {
    // 方向一致,接乘客并添加目标楼层
    elevator.getRequestQueue().addInternalRequest(req.getDestinationFloor());
    elevator.getRequestQueue().removePassengerRequest();
    shouldOpen = true;
}

  

 

  • 优点
    • 分支语句占比尚可:分支语句百分比 24.2% ,虽高于理想状态,但比之前 “Controller.java” 的 31.3% 有所降低,代码逻辑复杂程度有所缓解。
  • 缺点
    • 类方法数量过多:每个类的方法数高达 17.00 ,比之前的 15.00 还多,意味着类的功能可能过于繁杂,不符合高内聚原则,不利于代码维护和扩展。
    • 方法平均语句数略高:平均每个方法的语句数为 8.41 ,较之前变化不大,虽不算过高,但结合较多的方法数量,可能使类整体逻辑仍较复杂。
    • 存在复杂方法:明确指出最复杂方法 “Controller.deleteReputeEx ()” ,说明代码中存在局部逻辑复杂的情况,会增加理解和维护难度。

与前两次比较

  • 与第一次 “elevator.java” 比较
    • 分支语句:“elevator.java” 分支语句百分比 23.9% ,和此次 “Controller.java” 的 24.2% 接近,复杂程度相当。
    • 类方法数量:“elevator.java” 每个类方法数 7.00 远低于此次 “Controller.java” 的 17.00 ,“Controller.java” 类职责混杂问题更突出。
  • 与第二次 “Controller.java” 比较
    • 注释:从 0.0% 提升到 12.0% ,有明显改善。
    • 分支语句:从 31.3% 下降到 24.2% ,代码逻辑复杂程度降低。
    • 类方法数量:从 15.00 增加到 17.00 ,类功能混杂问题加剧。

三.踩坑经验

首先在最初的一周的前5天内我都陷入了一个应该把外部序列分为向上和向下两个队列的误区,在最初测试样例叫简单的情况下与测试样例结果一致但提交时一直是答案错误或运行超时,直到最后老师又添加了几个测试样例以后我才琢磨过来原来外部只需要一个请求队列。现在回想当时未完全理解需求仅靠着我觉得这么分成两个序列考虑效率会更高无疑是欠考量且无效的,也切身体会到为什么软件测试这一行60%的无效代码是因为未能理解用户需求导致的。

                                                          

如图当测试样例的复杂度一高输出结果就明显有误。在重新分析需求和测试样例后我才得到正确思路。

此外我始终没有解决一些极端情况,

如对于测试样例3我的运行结果是这样的(左)而正确运行结果应为(右)

                                              

 

在线程运行至三楼后我的代码只会判断此时内外部的头部请求是否符合并会将其remove但对于外部请求的第二个需求并不会接着判断是否也符合在该层的shouldstop而是检查到没有上方的请求直接转向当运行到2楼时才能检测到此时上方还有一个请求再向上运行执行该外部请求。

此外,在处理最初的转向逻辑时我没有添加当内部请求的首项要求应该转向或者内部请求为空时可以不理会外部请求的方向与当前运行方向不一致的问题直接执行外部请求。因此导致了如图的结果

我的代码在到达8层以后选择直接下行而非前往9层

于是在shouldstop中增加

的判断后该问题得到了解决。

四.改进建议

对于第六次实验集(第二次迭代的代码)可以做出以下改善

  1. 对时间复杂度的优化

当前问题:

  • findNextFloor()方法中使用了双重循环遍历请求队列,时间复杂度为O(n)
  • deleteReputeIn()deleteReputeEx()方法中使用ListIterator进行去重操作,效率不高

改进建议:

  • 使用优先队列(PriorityQueue):可以按照楼层排序存储请求,这样获取下一个目标楼层的时间复杂度可以降到O(1)
  1. 条件逻辑的优化

当前问题:

  • shouldStop()方法中嵌套了多层if-else条件判断
  • 方向判断逻辑重复出现在多个方法中

改进建议:

  • 策略模式:将不同方向的移动逻辑封装成单独的策略类
  • 状态模式:将电梯的不同状态(上升、下降、空闲)封装为状态对象

 

  1. 代码结构的进一步优化

当前问题:

  • Controller类职责过重,包含了几乎所有业务逻辑
  • 电梯状态和方向管理分散在多个方法中

改进建议:

  • 职责分离
    • 将请求调度逻辑提取到单独的Scheduler类
    • 将移动逻辑提取到MovementStrategy类

 

对于第七次题目集(最后一次迭代)可以做出以下改善

查找资料后发现对于内外部的申请队列比之Queue可以使用TreeSet,这样代码就可以做到自动排序且去重,高效完成下一楼层的查找

// 使用TreeSet替代Queue,自动排序且去重

class RequestQueue {

    private NavigableSet<Integer> upInternalRequests = new TreeSet<>();

    private NavigableSet<Integer> downInternalRequests = new TreeSet<>(Collections.reverseOrder());

    private Queue<Passenger> passengerRequests = new LinkedList<>();

 

    // 添加内部请求时根据方向存入不同集合

    public void addInternalRequest(int floor, Direction direction) {

        if (direction == Direction.UP) {

            upInternalRequests.add(floor);

        } else {

            downInternalRequests.add(floor);

        }

    }

   

    // 查找下一个楼层可以高效完成

    public Integer getNextFloor(int currentFloor, Direction direction) {

        if (direction == Direction.UP) {

            return upInternalRequests.higher(currentFloor);

        } else {

            return downInternalRequests.lower(currentFloor);

        }

    }

}

  

这样使用TreeSet后,查找下一个楼层的时间复杂度从O(n)降到O(log n),实现性能提升。

五.总结

在这三周迭代行的题集练习后我培养了面向对象程序设计的思维,学会并巩固了关于ArrayList、LinkedList、和Queue的相关用法,同时充分锻炼了我的逻辑设计能力,对复杂程序设计的拆解和不同情况考虑有了更全面的把握。

还有一个比较重要的点是我终于能够较为熟练的使用debug的功能,因为此前遇到的题目比较简单,逻辑并不复杂,大部分的错误不需要一点点去debug调试就能解决,直至本次题集因为逻辑的复杂性导致我必须耐着性子一点点去跟着线程变量去看内外部序列的变化以及究竟是在哪一步中出现了纰漏才导致需求过早或过晚的处理,我认为这是一个比较大的进步。

但是在其中也暴露出了我的一些缺点

比如在后续的两次题目我始终没有解决controller类复杂度过高的问题,同时我也意识到在没有题集提供的类图的情况下我对问题的分析和拆解能力较差,在解决问题时也未用到MVC原则,这些地方还有待加强。

关于改进建议

建议老师在以后的习题中可以多给出一些测试样例,因为题目的某些描述比较笼统,可能会误解需求,在多种测试样例的辅助下可以使我们更容易把握题目需求。

 

posted @ 2025-04-19 15:09  24201706-张子悦  阅读(80)  评论(0)    收藏  举报