0625s

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

题目集 5-7 总结性 Blog

前言

题目集 5、6、7 以单部电梯调度程序为主,构建了从基础功能实现到复杂类设计的递进式学习场景。作为 Java 初学者,这三次作业对于我来说也较为困难:题目集 5 因对面向对象设计理解浅薄,陷入单类难以设计的困境;题目集 6 尝试类拆分却因职责划分模糊导致逻辑混乱;题目集 7 引入乘客类后,复杂的请求转换逻辑再次成为拦路虎。尽管三次作业均未完全通关在提交截止时间到来时,看着测试用例一个个亮起的红叉,满心都是遗憾与不甘,但也在不断试错中积累了宝贵的设计经验,深刻体会到面向对象编程的核心魅力与难度。本文将从设计缺陷、采坑经历、改进思路等角度展开,复盘这段充满挑战的学习历程。

一、题目集 5:单类设计的杂乱与运行超时陷阱

1. 初始设计思路与代码结构

面对题目集 5 的单类设计要求,我刚开始认为将所有功能封装在 Elevator 类中即可满足需求。类中包含电梯状态(当前楼层、方向、状态)、请求队列(内部请求 LinkedList<Integer>、外部请求 LinkedList<ExternalRequest>)、调度方法(处理请求、移动、判断方向等)。我的代码结构如下(原则上不能复制源码 但我认为源码比图片更加方便记录与以后的查看):

class Elevator {  
    private int currentFloor, minFloor, maxFloor;  
    private Direction direction;  
    private LinkedList<Integer> internalQueue;  
    private LinkedList<ExternalRequest> externalQueue;  

    public void processRequests() {  
        while (!internalQueue.isEmpty() || !externalQueue.isEmpty()) {  
            determineDirection();  
            move();  
        }  
    }  
}  

2. 核心问题剖析:运行超时的根源

(1)较为低效的循环与遍历逻辑

move 方法中使用无限循环 while (true),每次移动后通过 checkStophasNextRequest 遍历队列判断是否停靠或继续。例如:

private boolean hasNextRequest() {  
    if (direction == Direction.UP) {  
        for (int floor : internalQueue) if (floor > currentFloor) return true;  
        for (ExternalRequest req : externalQueue) if (req.floor > currentFloor && req.dir == Direction.UP) return true;  
    }
    return false;  
}  

当请求量较大时,如输入样例 2 中的重复请求,程序陷入高频次无效遍历,我认为这便是最终导致运行超时的元凶。
也有可能是在对下一楼层查找时出现了问题,以及内部楼层和外部楼层的冲突导致了代码的冲突 或者什么,暂且还没有解决这一问题。
通过 SourceMonitor 对当时代码的分析(如下方图表所示),也能从侧面印证上述问题。从文本信息部分的 “Lines” 可知代码总行数为 276 行,“Statements” 为 80 条,结合 “Percent Branch Statements”(分支语句占比 38.7% )和 “Percent Lines with Comments”(含注释代码行占比 34.1% )等信息,可了解代码整体结构情况。再看图表部分,雷达图能直观展现代码在注释比例、方法数量等多个维度的指标,柱状图则反映出语句数量随代码块深度的分布情况。这些分析数据为我们进一步理解代码的复杂性、查找运行超时根源提供了有力依据。

(2)方向判断的片面性

determineDirection 方法仅比较队首请求,未综合所有请求的方向与楼层关系。例如,当内部队首请求为 3 楼,外部队首请求为 5 楼 UP,当前楼层为 1 楼时,电梯正确向上;但若内部队列后续还有 7 楼请求,外部队列有 6 楼 DOWN 请求,电梯到达 5 楼后可能错误转向,导致同方向请求未完全处理。

3. 试错过程:从盲目编码到初步调试

初期认为问题源于输入处理,反复检查正则表达式解析代码,却忽略了算法效率的核心问题。通过 IDE 调试发现,hasNextRequest 在每次楼层变化时被高频调用,占用 70% 以上运行时间。尝试引入 PriorityQueue 对请求按楼层排序,却因方向判断逻辑复杂未能成功,最终意识到单类设计的天然缺陷 —— 调度逻辑与状态管理混杂,难以进行高效优化。
正是因为这些问题,导致最终未能成功通关题目集 5 的作业,但这次经历也让我意识到算法效率和类设计合理性的重要性,为后续的学习改进指明了方向

二、题目集 6:类设计迭代的探索与职责划分的深渊

1. 类设计目标与初期方案

题目集 6 要求遵循单一职责原则,拆分为电梯类、控制类、请求队列类。初期设计如下:

  • 电梯类(Elevator):管理当前楼层、方向、状态,提供 isValidFloor 等状态校验方法。
  • 控制类(Controller):负责调度逻辑,包括方向判断、移动控制、停靠处理。
  • 请求队列类(RequestQueue):管理内外请求,支持去重与有效性校验。

2. 类职责划分的错误

(1)控制类与电梯类的职责混淆

初期在 Controller 中直接操作 ElevatorcurrentFloor 属性:

// 错误写法:控制类直接修改电梯状态  
elevator.currentFloor = nextFloor;  

违背封装原则,电梯状态变更应通过电梯类自身方法完成,如:

// 正确写法:通过电梯类提供的接口修改  
elevator.setCurrentFloor(nextFloor);  

此错误导致电梯状态校验逻辑(如楼层范围检查)无法统一管理,若后续增加 “电梯不能在移动中反向急停” 等规则,需多处修改控制类代码。

(2)请求队列类的去重逻辑漏洞

addInternalRequest 方法中,仅通过 contains 方法去重,但未正确重写 Integer 包装类的比较逻辑(实际应为值比较而非引用比较),导致重复请求如 <3><3> 被错误保留。正确做法是使用 equals 方法判断值相等:

if (!internalQueue.contains(targetFloor)) {  
    internalQueue.add(targetFloor);  
}  

3. 算法设计的不严谨性:同方向请求处理缺失

checkDirection 方法中,初期仅检查队首请求是否同方向,未遍历队列中所有请求:

// 错误逻辑:仅检查第一个请求  
if (firstInternalRequest > currentFloor) direction = Direction.UP;  

导致电梯在处理完队首请求后,可能忽略队列中后续同方向请求,例如内部队列有 [5,7],电梯到达 5 楼后,未发现 7 楼请求而错误转向。修正后需遍历所有请求,判断是否存在同方向未处理项:

// 正确逻辑:遍历所有内部请求  
boolean hasUpRequest = internalQueue.stream().anyMatch(floor -> floor > currentFloor);  

三、题目集 7:乘客类引入后的复杂协作与逻辑难以结合

1. 需求变化带来的设计挑战

题目集 7 将外部请求改为 <源楼层,目的楼层>,需引入 Passenger 类存储源与目的楼层,并在处理外部请求时将目的楼层加入内部队列。初期设计的 Passenger 类未正确区分内部请求(无源远请求)与外部请求(有源请求),导致逻辑混乱:

// 错误设计:内部请求与外部请求共用同一类,属性混淆  
class Passenger {  
    int sourceFloor, destinationFloor;  
    // 内部请求初始化时sourceFloor为0,导致后续判断错误  
}  

2. 请求转换逻辑的断层

ControllerstopAtFloor 方法中,处理外部请求时未正确关联源楼层与目的楼层:

// 错误逻辑:直接移除外部请求,未将目的楼层加入内部队列  
externalQueue.removeIf(req -> req.sourceFloor == currentFloor);  
// 正确逻辑:先提取目的楼层,再加入内部队列  
externalQueue.stream()  
    .filter(req -> req.sourceFloor == currentFloor)  
    .forEach(req -> internalQueue.add(req.destinationFloor));  

此错误导致乘客进入电梯后,目的楼层未被记录,电梯无法继续前往目标楼层,出现 “开门后无后续动作” 的异常现象。

3. 类协作的复杂度提升:依赖关系混乱

乘客类、请求队列类、控制类之间的依赖关系初期设计为双向关联,如 Passenger 类直接引用 RequestQueue 进行入队操作,违背 “高内聚、低耦合” 原则。正确做法是通过控制类统一协调,乘客类仅作为数据载体,不涉及业务逻辑:

// 错误:乘客类依赖请求队列  
class Passenger {  
    public void addToQueue(RequestQueue queue) {  
        queue.addRequest(this);  
    }  
}  
// 正确:控制类处理请求入队  
controller.processExternalRequest(passenger);  

四、采坑历程:从语法错误到设计思维的问题

1. 输入处理的 “隐性陷阱”

(1)楼层范围校验缺失

题目集 5 初期未校验输入楼层是否在 [minFloor, maxFloor] 范围内,导致用户输入 22 楼(max=20)时,电梯仍尝试前往,触发数组越界异常。修正后在 addRequest 方法中增加校验:

if (floor < minFloor || floor > maxFloor) {  
    System.out.println("Invalid floor, ignored.");  
    return;  
}  

(2)方向字符串解析错误

外部请求方向 UP/DOWN 大小写不敏感处理时,初期使用 Direction.valueOf(parts[1]),未转换为大写,导致输入 Up 时抛出 IllegalArgumentException。修正后统一转换为大写:

Direction requestDirection = Direction.valueOf(parts[1].toUpperCase());  

2. 状态管理的处理

电梯状态(State 枚举:MOVING/STOPPED)初期未被正确使用,Controller 在电梯移动时仍允许修改方向,导致 “运行中反向” 的逻辑错误。例如,电梯向上移动时,若下方出现紧急请求,错误代码会立即转向,违背 “优先处理同方向请求” 规则。正确做法是在 StateMOVING 时,禁止方向变更,仅允许在 STOPPED 状态重新判断方向。

3. 测试用例驱动开发的重要性

通过输入样例 1 发现,电梯在处理完所有同方向请求后,未正确切换方向处理反方向请求。例如,到达 7 楼(最高请求)后,未检测到 6 楼 DOWN 请求,导致电梯静止。通过逐步调试,发现 checkDirection 方法在方向切换时,未清空已处理请求标记,最终通过增加 “方向切换时重置请求遍历指针” 解决。

五、改进思路:从代码补丁到系统性优化

回顾三次未完成的作业,暴露出诸多问题,针对这些不足,我梳理出以下系统性的优化思路。

1. 类设计的重构策略

(1)严格遵循单一职责原则!!!!!!!!!必须必须 必须

  • 电梯类:仅负责状态存储与基本校验(当前楼层、方向、状态、楼层有效性),不涉及任何调度逻辑。
  • 控制类:专注调度算法(LOOK 算法实现),通过电梯类接口获取状态,通过请求队列类接口操作请求。
  • 请求队列类:封装请求的去重、校验、入队 / 出队操作,提供 “获取同方向请求” 等业务方法。

(2)引入枚举与常量类

定义 DirectionState 枚举明确状态,避免魔法值;创建 Constants 类存储请求格式正则表达式、输出模板等,提升代码可读性:

enum Direction { UP, DOWN, IDLE }  
class Constants {  
    public static final String INTERNAL_REGEX = "<(\\d+)>";  
    public static final String OUTPUT_OPEN = "Open Door # Floor %d";  
}  

2. 算法优化的核心方向

(1)请求队列的优先级管理

使用 TreeSet 存储内部请求,按楼层排序;外部请求按 “源楼层 + 方向” 分组,使用 Map<Direction, TreeSet<Integer>> 存储,便于快速获取同方向请求:

// 内部请求按楼层升序排列  
TreeSet<Integer> internalRequests = new TreeSet<>();  
// 外部请求按方向分组  
Map<Direction, TreeSet<Integer>> externalRequests = new EnumMap<>(Direction.class);  

(2)LOOK 算法的完整实现

在控制类中,维护当前运行方向的 “最远楼层”:

  • 向上运行时,记录最高目标楼层;
  • 向下运行时,记录最低目标楼层;
    到达最远楼层后,切换方向处理反方向请求,避免无效往返。

3. 代码健壮性的提升路径

(1)异常处理与日志记录

对输入解析、请求入队等关键步骤添加异常捕获,记录错误日志:

try {  
    // 解析输入楼层  
    int floor = Integer.parseInt(parts[0]);  
} catch (NumberFormatException e) {  
    System.err.println("Invalid floor format: " + parts[0]);  
    return;  
}  

(2)单元测试覆盖关键逻辑

针对 determineDirectionshouldStop 等核心方法编写单元测试,使用 JUnit 验证不同场景下的行为:

@Test  
public void testDetermineDirection_UpRequest() {  
    Elevator elevator = new Elevator(1, 20);  
    elevator.setCurrentFloor(3);  
    RequestQueue queue = new RequestQueue();  
    queue.addInternalRequest(5);  
    Controller controller = new Controller(elevator, queue);  
    controller.determineDirection();  
    assertEquals(Direction.UP, elevator.getDirection());  
}  

六、总结:在试错中理解面向对象的本质

1. 学习收获:从技术到思维的三重突破

(1)类设计思维

认识到 “类是职责的载体”,而非数据与方法的简单堆砌。题目集 5 的单类臃肿源于职责混杂,题目集 6、7 的类拆分迫使将 “状态管理”“逻辑处理”“数据存储” 分离,体会到单一职责原则如何降低复杂度、提升可维护性。

(2)算法与数据结构的联动意识

理解 “高效的算法需要合适的数据结构支撑”,如使用有序集合存储请求,可将遍历查找的 O(n) 复杂度降为 O(log n),从根本上解决运行超时问题。

(3)Java 语言特性的深度应用

掌握枚举、泛型、集合框架的高级用法,例如通过 EnumMap 优化外部请求的方向分组,利用 Stream API 简化请求过滤与转换逻辑,代码从冗长的循环嵌套进化为更简洁的函数式表达。

2. 未来之路

(1)设计模式的引入

  • 策略模式:将调度算法(LOOK、SCAN)封装为策略接口,允许运行时切换,提升扩展性。
  • 观察者模式:当电梯状态变化(如到达楼层、处理请求)时,通知日志模块、监控模块,实现模块解耦。

(2)复杂场景的支持

  • 多人请求并发处理:当前假设请求串行处理,未来可引入多线程模拟并发请求,使用 BlockingQueue 实现线程安全的请求队列。
  • 负载均衡优化:若扩展为多部电梯,需实现请求分配算法,如根据电梯当前负载、运行方向动态分配请求。

结语

题目集 5-7 的电梯调度之旅,是从 “面向过程思维” 向 “面向对象思维” 蜕变的痛苦却充实的过程。尽管三次作业均未完美收官,但每一次代码调试、每一次类图重画、每一次逻辑重构,都在加深对 “封装、继承、多态” 的理解。面向对象设计如同雕琢玉器,需要不断打磨类的职责边界,优化协作逻辑,而这正是软件开发的核心魅力所在。未来将带着这些宝贵的试错经验,在更复杂的系统设计中实践所学,让每一行代码都成为理解编程本质的基石。

posted on 2025-04-20 19:55  Stt0625  阅读(25)  评论(0)    收藏  举报