电梯题目集总结性Blog
一、前言
对这三次题目集的总结:
这三次作业的难度是层层递进的。题目数量安排得当,给的时间也足够用。前面的基础题像搭积木一样,带着我们一步步熟悉怎么设计类和对象;但每次的最后一题就像突然升级的关卡,特别考验真本事。第一次的电梯题简直是“新人杀手”——光搞懂电梯怎么智能调度就得反复琢磨,调代码调到半夜是常事。不过咬牙啃下这块硬骨头之后,后面两次迭代题反而轻松不少,重点变成了优化设计:怎么拆分不同类的任务、怎么让算法跑得更流畅。这种先练基本功、再挑战复杂设计的节奏,确实能让人感受到自己的进步。
二、设计与分析
因为题目集的前面几题都比较基础,所以该部分只对电梯迭代题做分析。
第一次电梯题目的设计与分析
题目要求:编程实现电梯程序,对于乘客的输入,分为外部请求和内部请求,然后分别加入到请求队列中去。再根据请求队列以及电梯调度算法来确定电梯的运行逻辑,请求的合理移除。最后打印出电梯每次运行的楼层和状态。
自己设计的类图:
类的总体设计
- Main类:使用正则表达式来正确读取每行输入当中所需的数据,然后按数据的形式来进行外部或者内部的请求的加入。
- ExternalRequest类:更好的存储和拿取外部请求的楼层与方向。
- Elevator类:该类与外部请求类相关联,主要用来控制电梯的运行,和方向的确定,以及打印电梯当前所在的楼层、状态和运行方向。
- 枚举:使用枚举来表示电梯的运行方向,使代码更加简洁易懂。
Source Monitor分析结果:
点此处看方法具体复杂度
分析与心得
由上面的Source Monitor分析的结果来看不难发现,这次题目集提交的通过代码的存在的主要问题有这两个:
- 代码的平均函数复杂度较高:平均函数复杂度达到了5.19,已经远远超过了java良好代码的平均复杂度,究其原因可能就是因为第一次的题目还是不能设计好类的单一职责原则,把添加请求队列、停靠、确定方向和运行到下一层这些方法都放在了电梯这一个类当中。尤其还有在确定方向和判断电梯是否变为停靠状态的方法当中运用了大量的if-else语句去处理一些特殊的情况,嵌套的层次较深,复杂度和深度很高,代码逻辑难以理解和调试,导致了代码的平均复杂度和平均深度变得很高。
- 代码注释量过少,可读性差:代码的注释量只有0.5,也就是几乎没有的水平。说明当时在完成这个题目集的时候只是为了完成当前这道电梯题目,并没有考虑到以后代码还需要不断地改进和维护。
心得:通过深刻剖析第一次的代码,以及使用特定工具来检查代码不难发现这次的代码在设计合理的类间关系、优化算法结构和增强代码的可读性上面还有很大的改进和提升的空间。
第二次电梯题目的设计与分析
题目要求:新增了过滤掉连续重复的输入请求,以及要求根据题目给出的参考类图,解决电梯类职责过多的问题,将上一次代码中类设计遵循单一职责原则(SRP)。
单一职责原则(SRP)
单一职责原则(Single Responsibility Principle,SRP)是面向对象设计中的一个重要原则,由罗伯特・C・马丁(Robert C. Martin)提出。该原则强调一个类应该仅有一个引起它变化的原因。
定义
简单来说,单一职责原则要求一个类只负责一项职责。如果一个类承担的职责过多,就等于把这些职责耦合在一起了。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力,这种耦合会导致脆弱的设计。
目的
单一职责原则的核心目的是将不同的职责分离,从而降低类的复杂度,提高代码的可读性、可维护性和可扩展性。
好处
- 提高可维护性:当一个类只负责一个功能时,修改这个类的代码只影响该功能,不会对其他功能产生意外的影响,降低了维护的难度。
- 增强可读性:职责单一的类代码结构更清晰,功能明确,便于开发者理解。
- 提升可扩展性:如果需要对某个功能进行扩展,只需要修改对应的类,不会影响到其他类。
类图:
设计
根据题目给的参考类图和自己的想法来设计的每个类的功能:- Main类:与上一次题目集电梯题目的Main类的设计相似,均是先用正则表达式解析每行用户所输入的数据然后按他们的种类的不同分为电梯内部请求和电梯外部请求,分别加入到RequestQueue类的匹配请求队列当中去。
- Elevator类:包含电梯可达的楼层范围、当前所在楼层、楼层合法性检验、电梯的运行方向和运行状态,用于Controller类的电梯运行逻辑分析。
- ExterRequest类:主要用来存储电梯外部请求的楼层与方向,用于RequestQueue类当中添加外部请求,使添加或使用外部电梯请求的逻辑更清晰。
- RequestQueue类:包括电梯内部请求和电梯外部请求的两个队列,以及添加请求、获得当前请求和检验请求是否重复方法,用于Controller类当中的电梯运行方向分析和判断。
- Controller类:关联Elevator类和RequestQueue类,主要用来执行电梯的运行方向及状态判断、控制电梯的移动、完成请求队列中的请求和打印当前电梯的运行状态和楼层。
- 枚举:用于描述电梯的三种运行方向以及电梯的三种不同的运行状态,方便后续的逻辑运行,使代码更加简洁易懂。
Source Monitor分析结果:
点此处看方法具体复杂度
分析与心得
由上面的Source Monitor分析的结果来看,第二次提交的通过代码在有了题目给的参考类图的设计后,代码行数从 180 增至 320,职责拆分后新增 Controller 类和RequestQueue 类,符合高内聚设计预期。质量明显要比第一次的好的多,但是还是存在一些问题的:
- 代码的注释还是太少:注释量从一开始的0.5到现在的1.6,提升的很少。可能是因为在重新设计类后,代码行数的增多,导致第二次的逻辑部分即使增加了代码注释,注释的量依旧没有达到Java规范的量。
- 代码的函数平均复杂度减少到了规范范围外:代码平均复杂度从一开始的5.19下降到了现在的1.57,而代码的最大复杂度却变化不大,这个数据有点奇怪了,让我一时间摸不着头脑,后来查看了每个函数的复杂度发现main方法和控制类当中的运行请求的方法复杂度还是比较高。所以得出最终的结论就是:因为我在每个类里都设置了每个属性的getter和setter的方法,而有些属性方法并没有运用上,导致平均的复杂度降到Java规范范围以外了。
心得:虽然这次的代码仍有两个方面没有达到要求的范围,所以还是要养写代码注释的习惯,尤其是复杂度逻辑部分。但相比上次题目集已经有了很大的改进了,比如在最大复杂度上,这次的复杂度通过对算法的不断优化,减少嵌套的层次,使代码逻辑容易理解和调试后,基本上符合Java规范了。
第三次电梯题目的设计与分析
题目要求:变动了乘客请求输入格式;新增了处理完某个外部请求后将其目的楼层加入内部队列尾部的逻辑;加入乘客类(Passenger),取消外部乘客请求类(ExternalRequest)。
类图
设计
这次的题目集相较于上一次的,变化了输入楼层请求的格式,参考类图删除了ExternalRequest类,新增了一个Passager类,并且和请求队列是依赖关系,所以这次的设计的一些类要做出以下改变:- Main类:由于输入格式的变化,所以这次用于处理输入数据的正则表达式要做出相应的变化,然后这次的某一行数据按Passenger类里面不同的构造方法来储存,然后根据数据的格式在RequestQueue类里面调用是否添加内部或外部请求的方法。
- RequestQueue类:依赖Passenger类,其中队列的类型变为Passenger,并且把乘客请求添加到队列里的方法变为传入Passenger参数。
- Controller类:为了满足“对于外部请求,当电梯处理该请求之后(该请求出队),要将<请求源楼层,请求目的楼层>中的请求目的楼层加入到请求内部队列(加到队尾)”的题目要求,在移除请求(removeRequest)的方法中要改为:在处理完外部某个请求时,要先将外部请求中的目的地楼层加入到内部请求队列的队尾,再移除当前这个外部请求。
Source Monitor分析结果:
点此处看方法具体复杂度
分析与心得
通过Source Monitor工具给出的分析结果来看,这次的代码质量和上一次的差不多。这次题目变化了输入的格式、类的简易重新设计和简单逻辑处理情况。所以我并没有对上一次代码的电梯运行算法做过多的优化,只是简单的增加了一个当外部请求要被处理时,把目标楼层加入到内部队列的尾部这个逻辑以及在输入的时候变动一些格式。仔细分析代码的话,还是有不少问题的,比如电梯的主要运行逻辑方法的复杂度和深度依旧很高,证明了我的电梯调度算法还可以进行改进;还有给代码写注释的情况,也没有比上次进步,所以这也只能归咎于我自己的懒惰和当时光顾着完成这个题目集的心态了。
三、踩坑心得
本部分对每次题目集提交代码出现的问题和心得进行总结(说明一下:由于第一次写BLOG,有些问题可能没有保存下来,只能用文字来说明,请见谅!)
-
1.正则表达式的错误使用:在主方法里使用正则表达式来解析输入的数据时由于当时对正则表达式的理解还不够深刻,在解析外部请求的时候,无法将正确的数据给读取进来。这个问题当时困扰了我很久,我也不断地在哪思考问题所在,最后发现我在定义表达式模式的时候少打了一个“,”导致无法正确的去匹配外部请求数据。
-
2.对于电梯调度算法的错误理解:当我第一次看到这么长的题目后,一时间不知道怎么下手,然后对于题目中描述的算法也是不知所措。过了几天老师在群里说了这个电梯的算法是简易版的Look算法。然后我就去了解了一下Look算法的内容,其描述是:考虑完当前方向才会换方向。再加上样例中电梯的方向只改变了一次,所以我就以为题目算法只要让电梯变化一次方向就可以处理完所有的电梯内外请求,于是我便开始写代码,写完后发现测试样例能过,但是提交了又过不去测试点。然后我就不断地修改、提交、修改、提交,提交了40多次回应我的只有绿绿的答案错误。后来才发现是算法不沾边,做了两天的无用功。所以也警示我们一定要理解好题目意思,设计好算法再开始写代码。
LOOK调度算法(全称为 Elevator LOOK Algorithm)是一种电梯调度策略,其核心逻辑为:电梯仅在当前运行方向上响应请求,若该方向无请求则切换方向。
-
3.LinkedList中方法的错误使用:在使用LinkedList中的getFirst方法来取出电梯内部请求队列头部和电梯外部请求来判断电梯的运行方向的时候没有考虑到队列可能为空的情况,导致提交了几次都显示我非零返回。上网了解后才发现,使用getFirst方法的时候如果链表为空的话就会出现非零返回的错误。
-
4.电梯运行的逻辑错误:某次提交的过程当中显示我运行超时,仔细分析后发现是处理过程的时候,先判断电梯方向后检查是否停靠,然后控制电梯运行到下一层。但是这种运行逻辑,当上升方向请求运行完了后,外部请求头部的下降的楼层大于上升的楼层时,就会无法清除掉这个外部队列头部请求,导致电梯一直在停靠楼层上下运动,进而运行超时。
-
5.处理连续重复输入问题:在第二次的电梯题目集当中,题目要求去除连续重复的主句,我在处理外部请求时由于逻辑判断有误,因为输入的外部请求方向和目标楼层数只要其他任意一个与这个时候队列尾部的不同就可以加入队列尾部,而当时我把这个“||”逻辑判断,理解成了用“&&”的逻辑来判断,导致有些外部的请求不能加入到电梯请求队列当中来,然后一直答案错误。同时,我觉得这种的问题很难改。当时我是从main方法的数据读入到电梯的运行,队列请求的处理,每个都检查了一遍,但还是没发现错误,还有些代码没写注释再次理解起来浪费了大量的时间。最后我把主函数里面正则表达式读取并储存在两个请求队列里的数据全打印出来看,才注意到这一个小小的逻辑问题。
-
6.弄反了楼层:在最后一次的电题题目集中,由于改了外部请求的输入方式,变为了起始楼层和目的楼层,这就导致了我在编程的时候,写到后面已经完全弄反了这两个数据,然后就连样例也过不去。但其实这个问题在类设计合理的情况下,修改起来还是很快的。只要把get属性的名字改了就行。这也反映了遵循类的原则来设计类的好处。
四、改进建议
点击查看可改进的代码
class Controller{
private Elevator elevator;
private RequestQueue queue;
public Controller(){
this.queue=new RequestQueue();
}
public Controller(Elevator elevator,RequestQueue requestQueue){
this.elevator=elevator;
this.queue=requestQueue;
}
public Elevator getElevator(){
return elevator;
}
public void setElevator(Elevator elevator){
this.elevator=elevator;
}
public RequestQueue getQueue(){
return queue;
}
public void setQueue(RequestQueue queue){
this.queue=queue;
}
public void processRequests(){
if(elevator.getState()==State.STOPPED)
determineDirection();
else if(elevator.getState()==State.MOVING){
int currentFloor=elevator.getCurrentFloor();
if(elevator.getDirection()!=Direction.IDLE)
System.out.printf("Current Floor: %d Direction: %s\n",currentFloor,elevator.getDirection());
if(shouldStop(currentFloor)){
elevator.setState(State.STOPPED);
openDoors();
removeRequests(currentFloor);
}
if(elevator.getState()==State.STOPPED)
determineDirection();
move();
}
}
private void determineDirection(){
LinkedList<Passenger> internalRequests=queue.getInternalRequests();
LinkedList<Passenger> externalRequests=queue.getExternalRequests();
Integer internalFloor = internalRequests.isEmpty()?null:internalRequests.getFirst().getDestinationFloor();
Passenger externalReq=externalRequests.isEmpty()?null:externalRequests.getFirst();
Integer externalFloor=externalReq!=null?externalReq.getSourceFloor():null;
Direction externalDir=externalReq!=null?externalReq.getDirection():null;
Direction currentDir=elevator.getDirection();
int currentFloor=elevator.getCurrentFloor();
Integer targetFloor1=null,targetFloor2=null;
// 1. 检查内部请求是否在方向内
if (internalFloor!=null){
boolean isValid=(currentDir==Direction.UP&&internalFloor>currentFloor)||
(currentDir==Direction.DOWN&&internalFloor<currentFloor);
if(isValid)
targetFloor1=internalFloor;
}
// 2. 检查外部请求是否在方向内
if (externalFloor!=null){
boolean isValid=false;
if (currentDir==Direction.UP){
isValid=externalDir==Direction.UP&&externalFloor>currentFloor;
} else if(currentDir==Direction.DOWN){
isValid=externalDir==Direction.DOWN&&externalFloor<currentFloor;
} else{//电梯停止时,外部请求直接有效
isValid=true;
}
if (isValid)
targetFloor2=externalFloor;
}
// 3. 优先处理同方向最近的请求
Integer targetFloor=null;
if (targetFloor1!=null&&targetFloor2!=null){
// 同方向选择最近的
targetFloor=getClosest(targetFloor1,targetFloor2);
}
if(targetFloor1!=null&&targetFloor2==null){
targetFloor=targetFloor1;
}
if(targetFloor1==null&&targetFloor2!=null){
targetFloor=targetFloor2;
}
if(targetFloor1==null&&targetFloor2==null){
// 都不在方向,选择所有头部中最近的
targetFloor=getClosest(internalFloor,externalFloor);
}
if (targetFloor!=null){
if(targetFloor==currentFloor){
openDoors();
removeRequests(currentFloor);
elevator.setDirection(Direction.IDLE);
elevator.setState(State.STOPPED);
}
else{
Direction newDir=targetFloor>currentFloor?Direction.UP:Direction.DOWN;
elevator.setDirection(newDir);
elevator.setState(State.MOVING);}
} else{
elevator.setDirection(Direction.IDLE);
elevator.setState(State.STOPPED);
}
}
private void move(){
if (elevator.getDirection()==Direction.IDLE) return;
int nextFloor=getNextFloor();
if (nextFloor<elevator.getMinFloor()||nextFloor>elevator.getMaxFloor()){
elevator.setDirection(Direction.IDLE);
elevator.setState(State.STOPPED);
return;
}
elevator.setCurrentFloor(nextFloor);
}
private boolean shouldStop(int floor){
if(!queue.getInternalRequests().isEmpty()){
int internalHead=queue.getInternalRequests().getFirst().getDestinationFloor();
if(internalHead==floor)
return true;
}
else{
if(!queue.getExternalRequests().isEmpty()){
int externalHead=queue.getExternalRequests().getFirst().getSourceFloor();
if(externalHead==floor)
return true;
}
}
if(!queue.getExternalRequests().isEmpty()&&!queue.getInternalRequests().isEmpty()){
int internalHead=queue.getInternalRequests().getFirst().getDestinationFloor();
int externalHead=queue.getExternalRequests().getFirst().getSourceFloor();
Direction externalHeadDir=queue.getExternalRequests().getFirst().getDirection();
if(externalHead==floor&&internalHead<floor&& externalHeadDir==Direction.DOWN){
elevator.setDirection(Direction.DOWN);
return true;
}
}
if(!queue.getExternalRequests().isEmpty()){
int externalHead=queue.getExternalRequests().getFirst().getSourceFloor();
Direction externalHeadDir=queue.getExternalRequests().getFirst().getDirection();
if(externalHead==floor&&externalHeadDir==elevator.getDirection())
return true;
}
return false;
}
private Integer getClosest(Integer a,Integer b){
if(a==null)return b;
if(b==null)return a;
if(a==null&&b==null)return null;
int distanceA=Math.abs(a-elevator.getCurrentFloor());
int distanceB=Math.abs(b-elevator.getCurrentFloor());
return distanceA<distanceB?a:b;
}
private void openDoors(){
System.out.printf("Open Door # Floor %d%n",elevator.getCurrentFloor());
System.out.println("Close Door");
}
private int getNextFloor(){
Direction dir=elevator.getDirection();
int f=elevator.getCurrentFloor();
switch(dir){
case IDLE:break;
case UP:++f;break;
case DOWN:--f;break;
}
return f;
}
private void removeRequests(int currentFloor){
int internalHead=-1,externalHead=-1;
if(!queue.getInternalRequests().isEmpty())
internalHead=queue.getInternalRequests().getFirst().getDestinationFloor();
if(!queue.getExternalRequests().isEmpty())
externalHead=queue.getExternalRequests().getFirst().getSourceFloor();
if(internalHead==currentFloor)
queue.getInternalRequests().remove(0);
if(externalHead==currentFloor) {
queue.getInternalRequests().add(new Passenger(queue.getExternalRequests().getFirst().getDestinationFloor()));
queue.getExternalRequests().remove(0);
}
}
}
做好类的单一职责,降低耦合度
在这段Controller类的设计代码中,很明显承担了多个职责:控制电梯的运行、检验电梯是否停靠、请求队列的移除和打印电梯的信息。当用户需求需要做出改变时(展示形式的改变、运行逻辑的优化、新增停靠情况),都需要在这个类里面进行修改,没有做好类单一职责,耦合性较高,需要改进。
方法做到单一功能,使逻辑更清晰
拿Controller类中的determineDirection方法来说,首先,该方法代码长度接近了70行,直接说明了方法的设计不合理。其次方法中嵌套了大量的if—else语句,逻辑变得更加混乱不堪。所以需要将方法中的判断逻辑和构建模式拆分成不同的方法,做到单一功能,使代码逻辑更加清晰明了。
添加合适的注释,增强代码可读性
这段代码全部看下来,只有判断电梯运行方向的方法中添加了注释,而电梯的停靠和请求队列的移除,这些复杂的方法并没有注释。这就会导致,当自己再次阅读代码或者别人查看代码时,会变得很吃力,所以需要给出合适的代码注释,增强代码的可读性。
五、总结
收获和建议:
收获
通过这三次迭代性题目集的综合性学习,我收获到了很多,首先,理解题目电梯调度的算法并且成功用Java编程实现出来,使我的逻辑思维和编程能力有了大幅度的提升。然后在不断编写和调试代码的过程中, 对于Java的语法掌握、正则表达式的灵活应用和面向对象的程序设计有了全方面的理解和提升,尤其是对于类的设计以及对类间关系的理解这一块,在通过第一个题目集迭代到第二个题目集解决电梯类职责过多的问题的训练,现在我对于类设计的遵循单一职责原则和类间关系的设计有了更加深刻的认识。但还是需要对可扩展性的设计结构进行深入的研究和学习。
建议
最后根据这次迭代作业,给面向对象程序设计课题组的一些建议:
- 1.建议课程针对迭代题目提供阶段性帮助:通俗来说就是,在每次的迭代题目结束后,给予学生一些引导式的帮助,让同学们在下次题目集时能有一个正确的方向。同时也能保证同学们了解和学习到正确的设计模式。
拿这次的题目来举例:首次电梯调度题暴露的核心痛点在于需求边界模糊——同学们对"同方向优先处理""方向切换时机"等关键规则存在差异化解读,导致前期试错成本过高。在保证题目质量的基础上,可以尝试分阶段提供资源帮助:首轮作业完成后发布基础算法模板(如电梯调度核心逻辑),既保留自主设计空间,也能给予第一次没能完成题目集的同学一些信心,还能建立统一认知基线;二次迭代后给出经典设计模式解析,通过一些范例引导同学们关注扩展性设计,让同学们在完成下次迭代题目时,能够有目的性的去优化自己的代码结构。这种递进式帮助体系既能缓解初期认知过载(不知道怎么下手),又能为后续复杂迭代奠定可延续的技术基础。 -
- 建议迭代题目的分值应该递增来设置,这样可以让那些没做出来前一次迭代题目的同学更加重视起来。因为分值递减的话有些同学可能就会不愿意花时间在迭代题上面,只追求前面的简单分值。进而得不到编程能力的提升。
你在实现电梯算法时遇到了哪些挑战?欢迎在评论区分享你的解决方案!感谢您的阅读!