NCHU_面向对象程序设计-阶段一题集总结
前言
- 在OOP题目集5 - 7的练习中,涉及的知识点丰富多样,涵盖了面向对象编程的核心概念,如类的封装,单一职责原则(SRP),迪米特法则(Law Of Demeter),还有电梯调度算法(LOOK)等。
- 题目集5题量不多,主要聚焦基础语法与简单类的设计,最后要求在题目给出的要求下完成基础的电梯运行程序设计,对于初次编写来说题量还是较多的,需要花大量时间理解题目要求以及思考如何具体实现电梯运行逻辑。
- 题目集6相较于5题量少了不少,同时最后以单部电梯调度问题为核心,对之前电梯调度程序进行迭代性设计,需要把单个类拆分成多个类,并且类设计要求遵循单一职责原则(SRP)。
- 题目集7则是重于多类设计,最后对题目集6单部电梯调度问题进一步优化,要求对题目集6的设计再次进行迭代性设计,提高代码的可维护性和扩展性,着重于综合运用所学知识,对代码的性能和扩展性提出更高要求。
设计与分析
第一次作业分析
单部电梯调度程序原题:
设计一个电梯类,具体包含电梯的最大楼层数、最小楼层数(默认为1层)当前楼层、运行方向、运行状态,以及电梯内部乘客的请求队列和电梯外部楼层乘客的请求队列,其中,电梯外部请求队列需要区分上行和下行。
电梯运行规则如下:电梯默认停留在1层,状态为静止,当有乘客对电梯发起请求时(各楼层电梯外部乘客按下上行或者下行按钮或者电梯内部乘客按下想要到达的楼层数字按钮),电梯开始移动,当电梯向某个方向移动时,优先处理同方向的请求,当同方向的请求均被处理完毕然后再处理相反方向的请求。电梯运行过程中的状态包括停止、移动中、开门、关门等状态。当电梯停止时,如果有新的请求,就根据请求的方向或位置决定移动方向。电梯在运行到某一楼层时,检查当前是否有请求(访问电梯内请求队列和电梯外请求队列),然后据此决定移动方向。每次移动一个楼层,检查是否有需要停靠的请求,如果有,则开门,处理该楼层的请求,然后关门继续移动。
使用键盘模拟输入乘客的请求,此时要注意处理无效请求情况,例如无效楼层请求,比如超过大楼的最高或最低楼层。还需要考虑电梯的空闲状态,当没有请求时,电梯停留在当前楼层。
输入格式:
第一行输入最小电梯楼层数。
第二行输入最大电梯楼层数。
从第三行开始每行输入代表一个乘客请求。
- 电梯内乘客请求格式:
<楼层数> - 电梯外乘客请求格式:
<乘客所在楼层数,乘梯方向>,其中,乘梯方向用UP代表上行,用DOWN代表下行(UP、DOWN必须大写)。 - 当输入“end”时代表输入结束(end不区分大小写)。
输出格式:
模拟电梯的运行过程,输出方式如下:
- 运行到某一楼层(不需要停留开门),输出一行文本:
Current Floor: 楼层数 Direction: 方向 - 运行到某一楼层(需要停留开门)输出两行文本:
Open Door # Floor 楼层数
Close Door
题目分析
题目要求设计一个简单的电梯运行程序,仔细研究了一下题目给出的电梯运行规则,我首先思考的是电梯所具有的属性和方法,电梯的属性包括当前楼层、运行方向、是否正在运行等,方法包括电梯的开关门、运行、设置运行方向等。对于电梯请求队列的数据处理,通过正则表达式匹配对应的输入,我使用了3个数组进行存储读入的数据,分别是电梯内部请求队列,电梯外部楼层队列以及电梯外部方向队列,按照我的设计在电梯运行过程要保证外部楼层与方向队列的同步入列与出列。
针对电梯运行逻辑的设计,根据题目要求以及老师给出的指导文档,我首先考虑了电梯首次运行的情况,方向默认为“UP”,电梯需要决定首个目标楼层的前往,之后需要考虑电梯后续运行对目标楼层的选择以及电梯运行方向的改变,由此以来应该能够基本实现电梯的运行。
设计
我首先设计了一个Elevator类,其中包含了电梯的属性和方法,其中Initialize()方法用于电梯的初始化,GotoTargetfloor()方法用于目标楼层的选取与判断改变电梯运行方向,到达目标楼层后调用ArriveTargetfloor()方法实现开关门,run()方法用于电梯运行,在之后套上条件为“内外队列不为空”的while循环,每次循环都通过对应的队列出列方法将已处理的数据出列保证电梯的正常运行。
电梯运行过程方面我以“当前楼层”与内外队列队头的大小关系为出发点进行不同运行情况的分类:
if (lift.getCurrentFloor() < Queue.getOutQueue()[0] && lift.getCurrentFloor() < Queue.getInnerQueue()[0])
else if (lift.getCurrentFloor() < Queue.getOutQueue()[0] && lift.getCurrentFloor() > Queue.getInnerQueue()[0])
else if (lift.getCurrentFloor() > Queue.getOutQueue()[0] && lift.getCurrentFloor() < Queue.getInnerQueue()[0])
else if (lift.getCurrentFloor() > Queue.getOutQueue()[0] && lift.getCurrentFloor() < Queue.getInnerQueue()[0])
else if(lift.getCurrentFloor() == Queue.getInnerQueue()[0]&&lift.getCurrentFloor() < Queue.getOutQueue()[0])
else if(lift.getCurrentFloor() == Queue.getInnerQueue()[0]&&lift.getCurrentFloor() > Queue.getOutQueue()[0])
else if(lift.getCurrentFloor() < Queue.getInnerQueue()[0])
else if(lift.getCurrentFloor() > Queue.getInnerQueue()[0])
else
每次决定目标楼层时,需要遵循题目意思“方向优先,优先最近楼层”。首先判断内外队列是否存在数据,若只有一个队列存在数据,直接选择该队列的队头作为目标楼层,电梯运行直到队列为空。若内外队列均存在数据,我是通过比较出当前楼层与内外队列队头楼层的大小关系分类后,再将“方向优先,优先最近楼层”原则与电梯当前运行状态相结合后进行目标楼层的判断。
- 例如电梯的当前楼层为
3,内外队列队头分别为<5>和<6,DOWN>,电梯当前运行方向为"UP",我先比较出当前楼层与内外队列队头的关系,显然当前楼层低于两个队列的队头楼层,此时看方向,由于外部队列方向为"DOWN",与电梯当前运行方向相反,那么此时选择队头最小的队列的请求楼层作为目标楼层。
SourceMonitor分析

从报表数据来看,平均复杂度(Avg Complexity)较高,这表明当前程序需要消耗较多的资源来完成任务,最大复杂度(Max Complexity)以及方法平均语句数(Avg Stmts/Method)较高,说明单个方法内逻辑分支较多,代码结构不够清晰。例如[Elevator.GotoTargetfloor()],由于多重条件判断导致复杂度上升,后续可考虑优化复杂逻辑,提高代码可读性和维护性。
第二次作业分析
单部电梯调度程序第一次迭代分析
题目集6对之前电梯调度程序进行迭代性设计,目的为解决电梯类职责过多的问题,类设计要求遵循单一职责原则(SRP),要求必须包含但不限于设计电梯类、乘客请求类、队列类以及控制类。此次作业输入输出格式与先前的题目集5相同,只需要对题目集5中的类拆散成几个类,并且要合理调整类之间的关系,保证SRP原则的实现。但此次作业新增对异常处理请求处理的考察:
电梯运行规则与前阶段单类设计相同,但要处理如下情况:
- 乘客请求楼层数有误,具体为高于最高楼层数或低于最低楼层数,处理方法:程序自动忽略此类输入,继续执行
- 乘客请求不合理,具体为输入时出现连续的相同请求,例如
<3><3><3>或者<5,DOWN><5,DOWN>,处理方法:程序自动忽略相同的多余输入,继续执行,例如<3><3><3>过滤为<3>。
设计
此次迭代针对异常请求的处理对主方法的数据读入进行修改,通过if-else语句进行判断,当输入的请求楼层位于正常范围内,并且与前一个请求楼层不同时,将请求楼层添加到电梯请求队列中,否则忽略该请求。这样能够保证电梯请求队列中的请求楼层不重复,同时解决了输入异常的情况,并且符合电梯运行规则。
//内部请求读取
if(nextfloor<=MaxFloor&&nextfloor>=MinFloor) {
if(a==0) {
r.addInnerQueue(nextfloor);
a++;
}
else if(r.getInnerQueue()[a-1]!=nextfloor){
r.addInnerQueue(nextfloor);
a++;
}
// 外部请求读取
if(nextfloor<=MaxFloor&&nextfloor>=MinFloor) {
if(b==0){
r.addOutQueue(nextfloor,parts[1]);
b++;
}
else if(!(r.getOutQueue()[b-1]==nextfloor&&r.getOutD()[b-1].equals(parts[1]))) {
r.addOutQueue(nextfloor,parts[1]);
b++;
}
与此同时,原本的类需要拆散成多个类以程序能够遵循SRP原则,参考题目中给出的UML类图针对自己的代码进行对应的拆分,并且保证类与类之间的关系合理,例如电梯类与乘客请求类之间的关系,电梯类与队列类之间的关系,电梯类与控制类之间的关系。
SourceMonitor分析

观察报表发现,此次迭代的代码度量结果与题目集5相比,平均复杂度(Avg Complexity)从7.47降到3.61,处于绿色圈内,这表明经过迭代后程序的复杂度降低,处理任务更加高效,同时方法平均语句数(Avg Stmts/Method)从15.2降到7.0,代码结构更加清晰,可读性更强。
类图分析

类图中,ExternalRequest类记录外部队列的请求楼层、方向等信息;电梯类包含电梯状态、运行方向、当前楼层等属性,以及处理乘客请求的方法;调度类负责接收乘客请求,并将其分配给电梯类。各角色分工明确,通过类之间的交互实现电梯调度功能。
第三次作业分析
单部电梯调度程序第二次迭代分析
题目集7对之前电梯调度程序再次进行迭代性设计,加入乘客类(Passenger),取消乘客请求类,类设计要求遵循单一职责原则(SRP),要求必须包含但不限于设计电梯类、乘客类、队列类以及控制类。此次作业输入格式与先前有变,虽然电梯运行规则与前阶段相同,但有如下变动情况:
- 乘客请求输入变动情况:外部请求由之前的
<请求楼层数,请求方向>修改为<请求源楼层,请求目的楼层> - 对于外部请求,当电梯处理该请求之后(该请求出队),要将
<请求源楼层,请求目的楼层>中的请求目的楼层加入到请求内部队列(加到队尾)
输入格式:
第一行输入最小电梯楼层数。
第二行输入最大电梯楼层数。
从第三行开始每行输入代表一个乘客请求。
- 电梯内乘客请求格式:
<楼层数> - 电梯外乘客请求格式:
<请求源楼层,请求目的楼层>,其中,请求源楼层表示乘客发起请求所在的楼层,请求目的楼层表示乘客想要到达的楼层。 - 当输入“end”时代表输入结束(end不区分大小写)。
经过分析,我发现本次迭代的主要变动在于外部请求的处理方式,需要对请求源楼层和目的楼层进行处理,经过思考后发现本质还是不变的,只不过这次为了得到外部队列原本的“方向”需要写一个根据请求源楼层与请求目的楼层大小关系来判断方向的方法,并按顺序写入原本的外部方向队列中,再将对应的队列整理好,把请求目的楼层放置到内部队列的队尾后,电梯就能够正常运行。
设计
针对外部请求的处理,我在Passenger类中设计了一个getDir()方法,用于根据请求源楼层和目的楼层的大小关系来判断电梯的运行方向,然后将判断出的方向写入外部方向队列中,最后将外部请求队列和外部方向队列整理好,将请求目的楼层放置到内部队列的队尾。
class Passenger {
private int [] soursefloor;
private int [] destinationfloor;
private int b;
public Passenger(){
}
public Passenger(int[] soursefloor, int[] destinationfloor, int b) {
this.soursefloor = soursefloor;
this.destinationfloor = destinationfloor;
this.b=b;
}
public int[] getSoursefloor() {
return soursefloor;
}
public void setSoursefloor(int[] soursefloor) {
this.soursefloor = soursefloor;
}
public int[] getDestinationfloor() {
return destinationfloor;
}
public void setDestinationfloor(int[] destinationfloor) {
this.destinationfloor = destinationfloor;
}
public void addPassengerfloor(int sourfloor,int destfloor) {
soursefloor[b] = sourfloor;
destinationfloor[b] = destfloor;
b++;
}
public String getDir(int sourse,int destination) {
String dir = "";
if(destination-sourse>0) dir="UP";
else if(destination-sourse<0) dir="DOWN";
return dir;
}
}
其他方面与题目集6的迭代设计相同,电梯运行逻辑不变,但是需要注意新增的Passenger类与之前的类之间的关系,以及Passenger类与Controller类之间的关系,保证程序的正常运行。要做的不止这些,我需要根据前两次作业积累的经验,对本次迭代的代码进行优化,这次针对大量的if-else语句做出优化,把重复的地方作了删减与提炼,减少代码的复杂度,提高代码的效率。
SourceMonitor分析

根据报表数据,我发现本次迭代的代码度量结果与题目集6相比,平均复杂度(Avg Complexity)从3.61降到3.16,这表明经过迭代后程序的复杂度降低,处理任务更加高效,同时方法平均语句数(Avg Stmts/Method)从7.0降到6.12,平均深度(Avg Depth)从3.1降到2.88,代码结构更加清晰,可读性更强。但是最大复杂度(Max Complexity)虽然有所下降,但是还是处于较高水平,仍需要进一步调整优化。
类图分析

类图中,没有了第一次迭代中的ExternalRequest类;但是取而代之的是Passenger类,记录乘客的请求源楼层,请求目的楼层等信息;需要关注的是Elevator类与Passenger类之间的关系变化,以及Elevator类与Controller类之间的关系变化。
踩坑心得
- 在处理数据的输入时,对正则表达式的使用不是很熟悉,在查阅资料后才完成了输入的处理,但是针对队列的处理时,有点懵,不知道该使用几个数组来满足数据的输入与储存,综合考虑了一下发现使用三个数组的方案还是有可行性的,同时使用同一个外部队列索引就能够保证外部楼层队列和方向队列的数据入队和出队同步。
- 在处理电梯运行逻辑时,一开始头脑中只有题目给出的输入样例情况,没有考虑完全就着手写逻辑了,一边写一边想情况,虽然写出的程序能够得到输入样例的正确结果,但是提交后得到的是绿色的结果“运行超时”,调试了多种情况,发现确实有导致无限循环的样例。所以首次编写并没有完全实现对所有情况的考虑,而且遇到问题一头雾水不知道要先解决什么。与同学交流后,发现自己思考的过于片面了,我先放下手中的代码,好好思考了一下,发现通过判断电梯当前楼层与内外队列队头的大小关系能够保证所有的情况都能被考虑到,写好if-else语句用于情况分类,之后逐步慢慢实现具体的代码逻辑。


-
审题不清导致走弯路,例如要求中对输入的end不区分大小写,我在写代码时就没有注意到,导致非零返回,查阅资料发现可以通过
"end".equalsIgnoreCase(input1)来忽略字符的大小写进行匹配。

-
在调试过程中while循环条件设置为
内外队列不为空,在运行各种样例时会导致空指针访问
Exception in thread "main" java.lang.NullPointerException: Cannot load from object array because "this.OutQueueD" is null
初次判断原因是在if判断中对OutQueueD进行了访问,并不能保证OutQueueD此时为空,修改之后仍然会发生空指针访问,一步步调试后发现在while循环中的条件判断中没有保证内外队列不同时为空,导致程序在运行过程中出现了空指针访问的情况,这次吸取教训,每次访问数组时必须保证数组不为空,避免出现空指针访问的情况导致报错。
-
写代码前一定要有具体的类图,例如这次从单类改成多类体会到了有类图的重要性,这样能够更好地规划代码的结构,避免出现类之间的关系混乱,导致代码无法正常运行。
-
坚持就是胜利:还记得大家都激起了斗志,凌晨一伙人在寝室聚在一起讨论电梯运行逻辑。然而现实却给了我们当头一棒,经过了各种样例的输入测试,互相讨论了得出的结果,发现大家的代码还存在未考虑周全的地方,在逐步完善代码之后,我们试着提交代码,发现还是无法通过测试样例,只有冷冰冰的“答案错误”。在老师为我们打开的讨论区中尝试着跑大家给出的各种样例,发现得出的结果与别人也相同提交后却仍然答案错误,大家都一头雾水,已经很晚了,满怀失望地丢下手中的代码睡觉了。结果第二天起床听到消息说是评判系统的问题,静待系统恢复正常后再次提交代码,这次成功通过了测试样例,大家都很开心。
改进建议
- 直到此次题集结束后,我代码的
平均深度(Avg Depth)和最大复杂度(Max Complexity)仍处于较高水平,原因是编写电梯运行逻辑时使用了大量的if-else语句,改进中应该思考清楚其中的复杂关系,提取公共部分来减少判断语句以实现更加简洁高效的逻辑。 - 在编写代码时,应该注意代码的可读性和可维护性,避免出现过于复杂的逻辑,应该将代码模块化,每个模块只负责一个功能,这样能够提高代码的可读性和可维护性。
- 虽然老师在指导过程中提到了LOOK算法,但是此次题目给出的电梯运行逻辑与LOOK算法并不完全相同,而且当前的电梯调度算法可能在某些情况下效率不高,可以考虑使用更高效的算法来优化电梯调度。
- 一定要提升自己的思维能力,以及加强自己应对抽象问题的解决能力,这样才能保证接下来的Coding生涯中遇到复杂的问题能够做到不盲目不慌乱。同时也要重视代码的注释,有时写着写着就忘记了自己刚写完的前几十行代码是什么意思了,所以在编写代码时应该养成良好的注释习惯。
- 交流能够让自己快速成长,通过这次作业训练,我在交流中学到了很多东西,同时也从别人视角感受了不同的思维方式,也避免自己大量踩坑。
总结
通过完成题目集 5 - 7 的作业,我对面向对象编程的理解有了很大的提升。在这个过程中,我深入学习了类的封装、单一职责原则(SRP)等面向对象编程的核心概念,并将这些概念应用到实际的代码设计中。经过了一次次“答案错误”,“运行超时”或是“非零返回”的现实反击,以及三次程序的不断迭代历练与不断的思考,换来的是最终的“答案正确”,这次的题集训练也是画上了一个句号。
也许提升自己最快的方法就是在做项目中学,在学中做项目吧!

浙公网安备 33010602011771号