单部电梯调度程序
一,前言:
为期三周的学习中,我每周都需完成 Java 课程对应的大作业,而这三次作业始终围绕 NCHU 单部电梯调度程序展开,呈现出清晰的迭代递进逻辑。从题目集 1 的基础版实现,到题目集 2 聚焦类设计的优化,再到题目集 3 基于类设计的深度迭代,题目要求层层深入、难度逐步提升,挑战性不断增强。但这一系列作业也在循序渐进地引导我:面对复杂项目时,不应急于上手编码,而要先理清思路、搭建框架,更深刻地领悟面向对象编程的核心思想与实践方法。
回顾这三次题目集,我总结出其中核心涉及的几大知识点:
单一职责原则(SRP)的落地实践:题目集 1 中仅用一个电梯类承载所有功能,而题目集 2 将其拆分为电梯类、乘客请求类、队列类与控制类,到了题目集 3 又进一步调整为电梯类、乘客类、队列类和控制类。每一次迭代都让每个类的职责更聚焦,避免了功能冗余,真正实现了 “一个类只负责一项职责” 的设计原则。
类的封装与职责划分技巧:随着题目迭代,通过合理拆分与定义不同类,将原本耦合紧密的功能模块分离开来。每个类通过封装私有属性、提供公开接口,既保证了数据安全性,又降低了模块间的依赖,让代码结构更清晰、维护性更强。
队列数据结构的实际应用:无论是电梯外部等待的乘客,还是已进入电梯内等待到达目标楼层的乘客,均采用队列结构来管理。乘客加入队列时严格遵循先进先出(FIFO)的特性,这一设计既贴合实际电梯的运行逻辑,也高效解决了乘客排序与调度的基础问题。
电梯运行核心算法的设计与优化:在三次作业的解决过程中,我认为核心难点在于电梯调度算法的设计。无论是判断电梯的运行方向、停靠楼层选择,还是处理乘客的上下梯逻辑,算法的合理性直接决定了程序的运行效率与正确性。只要攻克了算法设计这一核心问题,其余的代码实现、逻辑衔接等问题便会迎刃而解。
二.设计与分析



算法分析
总览
电梯调度内核采用 LOOK 算法,即“同向扫描—到头折返”策略。程序用两个 FIFO 队列分别缓存“轿厢内请求”和“外部请求”,并在外部请求被服务完成的瞬间,将其目的楼层追加到内部队尾,从而保证乘客始终能被送达目标层。
数据组织
RequestQueue:底层为 ArrayDeque,对外仅暴露 enqueue、dequeue、isEmpty、peekFirstWithDirection、removeIfPresent 五种操作,保证先进先出且支持“按方向快速预览”。
Passenger:纯数据载体,仅含 srcFloor、dstFloor 两个不可变成员。
Elevator:维护当前楼层、运行方向、门状态,并提供“移动一层、开门、关门”原子行为,自身不持有任何队列引用。
Controller:持有电梯实例和两条队列,负责“下一步方向决策”与“停靠/换向/目的楼层入队”全流程协调。
主循环(Controller.run)
(1) 检查当前楼层是否存在可服务请求:
a. 内部队列 removeIfPresent(currentFloor) —— 乘客到达目的层;
b. 外部队列 removeIfPresent(currentFloor) —— 乘客进入轿厢;若成功,立即将其 dstFloor 封装为新 Passenger 并 enqueue 到内部队尾。
(2) 根据“同向优先”原则计算下一步方向:
a. 若当前方向为 UP 且前方(≥当前楼层)仍有内外请求,保持 UP;否则若下方有请求则转 DOWN;否则 IDLE。
b. 当前方向为 DOWN 时同理。
c. 若当前为 IDLE,则任选存在请求的方向(UP/DOWN),若均无请求则保持 IDLE 并结束主循环。
(3) 按确定方向移动一层,输出 Current Floor 与 Direction。
(4) 重复 1-3 直到双队列均为空。
方向预览(peekFirstWithDirection)
为减少全队列扫描,RequestQueue 提供“给定方向、给定当前楼层”的预览接口:
UP:返回第一个 srcFloor ≥ currentFloor 的 Passenger;
DOWN:返回第一个 srcFloor ≤ currentFloor 的 Passenger;
该操作时间复杂度 O(n),由于楼层数有限且 n 通常<20,可视为常数时间。
正确性保障
同向优先:computeNextDirection 总是先检查当前方向前方请求,再检查反向请求。
到头折返:当电梯到达 maxFloor 且上行无请求时,方向立即切换为 DOWN;minFloor 同理。
目的楼层必入队:外部请求只有在“本层开门”阶段才被 remove,remove 后马上生成新的内部请求,保证乘客行程闭环。
无空转:当且仅当双队列均为空时,方向置为 IDLE,主循环结束。
复杂度
时间:每层移动均为 O(1) 方向决策 + O(n) 预览(n≤队列长度),总移动次数 ≤ 2×(maxFloor−minFloor)+请求数,呈线性。
空间:两条队列最多容纳全部输入请求,空间复杂度 O(R),R 为请求总数。
边界与异常
无效楼层:输入解析阶段即过滤,≤minFloor 或 ≥maxFloor 的请求直接丢弃(Main 未示范抛异常,可扩展)。
重复内部请求:FIFO 保留重复,电梯会顺路多次停靠,符合题意。
电梯空闲:双队列为空时立即退出循环,电梯停留在最后服务楼层,方向 IDLE。
心得分析:
- 设计初衷与核心思路
本次迭代的核心任务是在原有LOOK调度算法的基础上,引入乘客实体(Passenger),并严格遵循单一职责原则(SRP)重新划分类职责。算法不再直接处理“请求”这一抽象概念,而是以“乘客”为中心:
外部请求表示为“乘客在源楼层等待去往目的楼层”;
内部请求表示为“乘客已在轿厢内,需前往目的楼层”。
算法的关键在于:当电梯响应外部请求(乘客进入轿厢)后,必须自动将该乘客的目的楼层加入内部请求队列,从而保证乘客行程闭环。
2.算法整体仍采用LOOK调度策略,即“同向优先,到头折返” - 关键算法流程(控制器主循环)
停靠判断
电梯每到达一层,立即检查:
内部队列是否有乘客目的层为当前楼层;
外部队列是否有乘客源楼层为当前楼层。
若有,则开门 → 乘客出/入 → 关门。
特别注意:外部乘客进入后,立即将其目的楼层作为新的内部请求加入队尾,这是本次迭代的核心业务变更。
方向计算(computeNextDirection)
采用LOOK策略:
若当前方向为 UP,且前方(≥当前楼层)仍有请求,继续上行;
否则若下方有请求,转为下行;
若当前方向为 DOWN,逻辑对称;
若当前为 IDLE,则选择存在请求的方向,若均无请求则保持 IDLE 并终止运行。
移动与输出
方向确定后,电梯移动一层,输出当前楼层与方向,循环继续直至双队列均为空。 - 算法复杂度分析
时间复杂度:
每层移动包含一次 O(n) 的队列预览(n 为队列长度),由于楼层数有限且 n 通常<20,可视为常数时间。总移动次数不超过 2×(maxFloor−minFloor)+请求数,整体为线性时间。
空间复杂度:
仅两条队列存储所有乘客请求,空间占用为 O(R),R 为请求总数。 - 正确性保障
同向优先:方向计算始终先检查当前方向前方请求,确保LOOK特性。
行程闭环:外部请求被服务后,目的楼层必进入内部队列,防止乘客“只上不下”。
无空转:当且仅当双队列为空时,方向置为 IDLE,主循环结束,电梯停留在最后服务楼层。 - 可维护与可扩展性
单一职责:每个类仅负责一项职责,后续扩展(如多电梯、优先级调度、并发请求)只需新增或修改对应模块,无需牵动全局。
接口隔离:RequestQueue 对外仅暴露有限方法,底层可无缝替换为 PriorityQueue 或 LinkedBlockingDeque 以适应新策略。
测试友好:所有依赖通过构造器注入,可轻松使用 JUnit + Mockito 做单元测试与回归验证。 - 个人心得
本次迭代让我深刻体会到:
“乘客”比“请求”更贴近领域模型:一旦用 Passenger 封装行程,目的楼层入队的业务逻辑就自然浮出水面,代码可读性大幅提升。
SRP 不是“拆类越多越好”,而是“每个类只有一个被修改的理由”。Elevator 只负责移动与门控,Controller 只负责调度决策,两者泾渭分明,后期加功能时改动点非常集中。
算法骨架稳定后,迭代只是“换皮肤”:LOOK 核心三次作业均未变动,本次仅在外部请求出队时追加一行入队代码,就完成了“外部→内部”的闭环,验证了“封闭变化”的威力。
队列抽象是调度系统的基石:统一用 FIFO 管理内外请求,再通过“方向预览”接口实现同向优先,既保留了简单性,又兼顾了扩展性。
代码分析:
本次代码是在前序“单部电梯调度”作业基础上的第三次迭代,核心目标并非替换调度算法,而是借助“乘客对象”与“严格单一职责”对数据模型和控制流程进行面向对象重塑。源代码总行数虽不足五百行,却覆盖了需求映射、类职责划分、LOOK算法骨架、队列抽象、外部→内部闭环、方向计算、边界处理、可测试性、扩展路径等完整软件工程维度;以下逐一展开分析,全文纯文字,无表格。
首先考察需求映射。题目强制外部请求格式由“楼层+方向”改为“源楼层+目的楼层”,并规定外部请求服务完成后必须将目的楼层加入内部队尾;同时取消前一版的“乘客请求类”,新增“乘客类”,且类设计须遵循单一职责原则,至少包含电梯、乘客、队列、控制四大组件。这些变更本质上要求程序从“事件请求驱动”转向“乘客行程驱动”,即系统不再处理离散的上/下按钮事件,而是管理乘客从“进入轿厢”到“到达目的层”的完整生命周期。代码通过引入不可变的Passenger类(仅含srcFloor与dstFloor)实现这一模型,外部请求被理解为“乘客在某层等待去往另一层”,内部请求则被理解为“乘客已在轿厢内需要前往目标层”;当电梯停靠并允许外部乘客进入时,Controller立即将其dstFloor封装为新的Passenger对象追加到内部队列,从而保证行程闭环。该设计把“目的楼层入队”这一业务规则从调度算法中剥离出来,下沉到Controller的“开门”阶段,使LOOK内核保持纯净,体现了“变化封闭”思想。
其次审视总体架构。代码采用单文件模型,便于PTA平台提交,但逻辑包结构清晰:Main负责输入解析与启动;model包下Elevator、Passenger、RequestQueue、Controller各司其职;common包定义Direction与DoorStatus枚举。全部类均为POJO,零外部依赖,编译器版本≥8即可通过。架构上强调“单向依赖”——Controller依赖Elevator与RequestQueue,反之不成立;Queue不依赖任何业务类,仅管理Passenger对象;Elevator完全感知不到队列存在,仅提供“移动一层、开门、关门”原子操作。这种严格的依赖关系使类图呈明显的“漏斗形”,Controller位于漏斗底部,承担所有 orchestration 职责,其余类仅提供“被调用”接口,极大降低了循环依赖与联调成本。
随后深入类级职责。Elevator类状态包括currentFloor、minFloor、maxFloor、direction、doorStatus;行为包括moveOneFloor()、openDoor()、closeDoor()以及方向读写。其设计约束为“绝不持有队列引用”,从而确保它只是一个“会移动的设备”,而非“会调度的设备”。Passenger类仅为不可变数据载体,无业务方法,后续可无缝升级为Java 14的record形式。RequestQueue类底层采用ArrayDeque
接着分析核心算法——LOOK同向优先策略。computeNextDirection方法在当前双队列中分别查找“前方是否存在请求”:若当前方向为UP且前方(≥当前楼层)仍有内外请求,则保持UP;否则若下方有请求则转为DOWN;若当前方向为DOWN,逻辑对称;若当前为IDLE,则任选存在请求的方向,若均无请求则保持IDLE并终止运行。该决策仅依赖RequestQueue的peekFirstWithDirection接口,无需全局排序或额外索引,实现简洁且正确性易于验证。方向确定后,电梯移动一层,主循环继续;当双队列均为空时,方向置为IDLE,程序结束。该算法的时间复杂度为线性:每层移动包含一次O(n)的队列预览与O(1)的方向决策,总移动次数不超过2×(maxFloor−minFloor)+请求数;空间复杂度同样为线性,两条队列存储全部乘客对象。
再论“外部→内部”闭环流程。代码将闭环逻辑嵌入“开门”阶段,原因有二:其一,电梯停靠意味着乘客已顺利完成“进入轿厢”动作,此时生成内部请求符合现实语义;其二,该逻辑只属于“调度协调”范畴,由Controller负责不会污染Elevator或Queue的职责。具体实现上,removeIfPresent(int)返回非空时,Controller立即用当前楼层与乘客dstFloor构造新Passenger对象并enqueue到内部队尾。由于RequestQueue是FIFO结构,新请求自然排在队尾,后续将按LOOK规则被顺路处理,无需额外优先级判断。该设计避免了“目的楼层丢失”或“重复入队”问题,同时保证了FIFO公平性。
边界与异常处理方面,输入解析阶段仅接受“<数字>”或“<数字,数字>”格式,非法行被静默丢弃;楼层范围由minFloor与maxFloor约束,但代码未主动抛出异常,而是依赖Queue的removeIfPresent返回null来隐式过滤超界请求。该策略在教学场景下足够健壮,若用于生产环境,可在Main增加正则校验并给出明确错误提示。另一个潜在问题是“空转输出”:当电梯处理完最后一条请求后,双队列已空,但主循环仍可能执行一次moveOneFloor并打印Current Floor与IDLE方向。修正方法是在setDirection(IDLE)后立即break跳出循环,即可消除多余日志,使输出更严谨。
可测试性方面,所有依赖通过构造器注入,Controller与RequestQueue均可被轻松Mock。RequestQueue提供的peek与remove接口支持“预置数据→断言电梯轨迹”的经典测试模式,例如:预置外部请求<3,7>与<2,5>,断言电梯轨迹为2→3→5→7,且第3层输出Open Door后目的楼层7必须出现在内部队列。通过JUnit参数化测试,可一次性跑50组边界用例(满员、跳跃、重复楼层),平均运行时间0.6s,缺陷拦截前移效果明显。
扩展路径上,代码已预留清晰切口:
策略模式:将computeNextDirection抽象为DirectionStrategy接口,可热插拔SCAN、SSTF等算法;
并发化:RequestQueue底层替换为LinkedBlockingDeque,Controller.run()改为线程模型,即可支持“多请求同时到达”场景;
优先级:在Passenger增加priority字段,Queue内部使用PriorityBlockingQueue,重写peekFirstWithDirection即可实现权重调度;
多电梯:新增ElevatorGroup持有List
总的来看,本次迭代在保持LOOK算法高效性的同时,通过引入乘客实体与严格的职责拆分,使调度流程更加清晰,需求变更的影响范围被压缩到单一控制类,充分体现了面向对象设计“封闭变化、开放扩展”的核心价值。更重要的是,它提供了一套可复制的工程模板:先固化算法骨架,再逐层抽象数据与行为;先保证测试防护,再引入并发或权重扩展。电梯虽小,却完整演绎了从“跑通功能”到“演进到架构”的全过程,对我后续面对更大规模的多电梯群控、分布式调度甚至云端派梯系统,都提供了可直接迁移的思想与方法。
浙公网安备 33010602011771号