三次电梯题目迭代分析
三次电梯题目迭代分析
前言
三次题目模拟电梯运行,以LOOK算法为核心,涉及泛型类的使用等,三次迭代逐步实现由单个类到多个类设计,主要遵循单一职责原则,从而实现面向对象编程。题目总体难度偏难,题量偏多,有一定的代码量。
设计与分析
题目集五电梯题目
题目分析
在题目集五中的电梯题主要涉及算法设计,对类间的关系设计并未涉及。在设计上,题目集五的并未有太大的难度,主要涉及算法的设计,要明确LOOK算法的核心思想,以及电梯各种运行情况下的分析,总体而言,算法设计的难度较大。
起初因为理解错误以及对LOOK算法的不了解,以为电梯运行与现实情况一样(本次题目电梯运行规则与现实情况有所不同),于是便考虑的比较复杂,因此开始算法设计的完全错误。后来在了解LOOK算法核心与运行规则后,便豁然开朗。
总结得到的电梯的运行规则为同方向优先,只取队列头部请求
可以用以下流程图表示:

设计类图如下

类图设计解释
第一次题目集只要求设计一个电梯类,但为了方便操作,我又设计了Request类,这在一定程度上实现了职责单一,但是在类的设计时Request里面的数据是public类型,这使得数据不具有封装性,数据并不安全,所以这一块并未设计好。
然后,Request类与Elevator之间的关系为依赖的关系,耦合性也较低。
SourceMonitor的分析结果如下



SourceMonitor的分析结果解释
可以看到,通过SourceMontior的分析代码存在以下问题,主要是代码复杂度问题:
- 分支语句占比占比32.7% ,意味着约三分之一的语句是分支语句,可见代码逻辑存在较多条件判断,
逻辑比较复杂,说明没做到单一职责原则,代码不够简化,过于冗杂
- 最大复杂度为6 ,说明明存在一定复杂逻辑的方法。
- 最大深度为5 ,代码中循环嵌套结构最深达到 5 层,说明代码的逻辑仍是过于复杂,同样也没做到职责单一。
- 平均复杂度为2.00 ,尽管处于良好代码平均复杂度之间(良好的代码复杂度在2.0-4.0之间),但此次设及的类比较少,从最大复杂度上来看,仍存在不少代码复杂度过高。
- 平均深度为2.19 ,平均嵌套深度相对较浅,但结合最大深度来看,部分代码块仍镶嵌较深,说明代码过于复杂。
- 从柱状图来看,可以看到在深度为 1 - 3 时,柱状条较高,语句数量较多 ,说明大部分语句集中在较浅的嵌套层次中。
- 代码长度为402,代码过长,算法设计不够简明,过于复杂,设计方法时可能重复造轮子。
此外,从代码质量来看,代码注释较少,只占4.7%,不在图中良好代码应有的范围内,不利于代码的理解与维护性。然后,每个方法的平均语句数为14.60 ,方法内部平均语句数较多,说明功能不够单一,说明没有做到职责单一。总体上来看,代码的质量并不好。
心得总结
- 首先理解题目很重要!哪怕写出了代码,那也是不可用的代码。此前因为曲解题意,导致浪费了很多的时间去写代码,重复修改多次,但都是无用。
- 结合上面类图及SourceMonitor的分析,可以看出此次代码实现的并不好(尽管过了测试点),代码复杂度很高,并且代码的质量也不行。总结起来就是以下问题:首先最大的问题就是没有做到单一职责原则,if-else、循环语句过多,导致代码过长,结合我的代码,我发现其实很多地方可以改进,有的方法甚至可以合并(比如StartMove和getaimfloor,二者功能是差不多的),但由于时间限制,在设计算法时并未进行精细化设计,只想先把测试点先过了(这也导致我下一次迭代沿用上一次算法时出了问题),这其实是很不好的习惯,在设计算法应做到细化,把相同的方法合并,遵循单一职责原则。此外,还有一个问题就是注释过少,不利于代码维护,也不利于自己后期的修改。然后方法命名其实也不规范(参考阿里巴巴Java开发手册)。所以归纳起来就是要设计好算法,遵循单一职责原则,遵循命名规范,并要写上一定的注释。
题目集六电梯题目
题目分析
核心算法还是LOOK算法,电梯的运行规则未变,要求比上一次明显增多。这次题目要求类的设计要求遵循单一职责原则(SRP),要求设计成多个类,把单一的的电梯类又分出了乘客请求类、外部请求类、还有控制类,控制类把业务逻辑和表现逻辑分开,在控制类里面集中处理各种业务需求,以降低类间的耦合性,实现职责单一原则,提高代码的复用性,使代码逻辑、业务流程更清晰。除此之外,还要求对输入有误进行行处理
类图设计如下

类图设计解释
与前一次类图设计相比,此次类的种类明显增多,不再把功能只放在Elevator类里面集中处理,而把乘客请求类单独分出,进行对乘客请求的处理,而电梯则持有自己本身的功能,对于电梯运行、停靠等功能则集中放在了新类:控制类。
控制类把业务逻辑和表现逻辑分开,在控制类里面集中处理各种业务需求,以降低类间的耦合性,实现单一职责原则,提高代码的复用性,使代码逻辑、业务流程更清晰,同时也便于代码的扩展和修改,提高代码的可读性。
SourceMonitor的分析结果如下



SourceMonitor分析结果的解释
- 可以看到,与上一次的分析结果相比,代码行数只有361行,明显有所减少,说明在本次题目集中,多个类使职责分化,明显提高了代码得复用性。
- 代码注释只占4.4%,占比较少,这会不利于代码的维护性与可读性。
- 平均复杂度为5,与上一次相比明显增多,说明代码修改后逻辑较为复杂,可能仍存在职责不单一等情况。
- 分支语句占比27.3%,尽管比上次的有所减少,但仍占大部分。减少的原因可能是多个类使得代码复用性提高,但仍占大部分可能在一些方法里面仍存在大量的if-else等分支语句,没有完全实现单一职责原则。
- 每个类的方法数为18.00,,意味着类的职责可能不够单一,可能导致代码臃肿、复杂度上升,违反单一职责原则,不利于代码的修改、测试和复用。
- 除此之外,平均深度、最大复杂度等基本处于良好的范围内。
心得总结
问题基本与上次大致一样,单一职责原则是我今后写代码需要注意的点,代码复杂度过高,分支语句过多,说明仍需要
拆分复杂方法、降低代码块深度、合理分配类和方法职责来实现单一职责原则,此外,代码的注释仍过少,说明代码的刻度性不够高。单一职责原则和代码注释是我今后写代码必须要注意的点
题目集七电梯题目
题目分析
在题目集七的迭代中,外部乘客请求发生了变化,乘客的输入不再有方向,而是输入请求源楼层和请求目标楼层,内部楼层则为输入目标楼层,加入了乘客类(Passenger),取消了外部乘客请求类(ExternalRequest),并且要求在清理外部请求后要将这个外部请求的目标楼层加到内部请求队列的最后。但电梯的运行规则仍未改变,算法的核心仍是LOOK算法,仍设计多个类的设计,需要面向对象编程,实现单一职责原则。题目难度上有所提升,需要我们调整算法设计,考虑一些新的情况出现。
类图设计如下

类图设计解释
类的种类与上次有所不同,但在功能实现起来是差不多的,这里不做过多解释。
SoureceMonitor分析结果如下



SourceMonitor分析结果的解释:
从代码复杂程度上来看:
- 最大复杂度,为43,说明代码复杂度很高,存在多个循环或分支语句,又或者递归等复杂度高的方法的运用,从图中发现在Controller类里面的findaimfloor方法的复杂性最高,而我的代码在这个类里面的这个方法里面确实存在多个分支语句和循环语句,并调用了两次递归的方法,除此之外,在其他方法里面也存在多个循环,分支等的情况,造成代码最大复杂度明显提高。
- 平均深度,为3.04,明显不在良好代码应有的范围内,并且明显比上次高,推测是由于最高复杂度和最高深度上升导致。
- 在柱状图中,与前两次相比,5到7占比明显增多,说明高深度的代码语句明显增多,也侧面反应最大复杂度与平均深度的提高。
从代码结构与质量上来看:
- 代码注释为11.9%,与前两次相比有明显的提高,说明代码的可读性与可维护性有了较大的提高。
- 分支语句占比为23.6%,尽管比前两次少,但是仍占有较高的比例,代码逻辑较为复杂,不利于代码维护。
心得总结
通过第三次SourceMonitor的分析,代码存在一些提高也存在一些问题。首先代码注释占比提高了,这是一个良好的习惯,在今后编码中也要保持。此外,代码仍存在较多分支、循环、递归等复杂方法,这说明代码职责分化仍不够细致,没有做到完全单一职责(这与我写代码前没有考虑好算法及方法调用也有关系)总之给我的教训就是:设计好算法,不断修改代码以实现单一职责原则。
踩坑心得
题目集五
踩坑心得1
总结:我第一次写这道题时,由于没有弄清电梯的运行规则,便直接上手写代码(明知道有可能是错误的),导致浪费大部分时间写了错误的代码,后来弄清楚后,就很快弄出来。通过这一次的题目集我明白写代码不能直接上手,一定要弄清楚题意再去设计好流程与算法。
踩坑心得2
当我都过了老师给的测试数据,以为没问题时,第一次提交便出现了答案错误
在自己多次设置测试数据时,修改了多次,提交仍是答案错误

询问了一下老师,当我测试数据以下数据时
1
20
<2>
<4>
END
会报非零返回的错误

发现我在Elevator这个方法里面(这个方法其实没有遵循职责单一原则,在后面的题目集迭代时我就把它删除了,使用另一个和它同功能的方法,不使用其实可以避免,但当时我直接修改这个方法了)
1 //第一个方向 2 3 public int StartMove(){ 4 5 Request request = ExternalRequests.get(0); 6 7 int firstaimfloor = 0; 8 9 if(request.direction.equals("UP")){ 10 11 firstaimfloor = (request.floor<internalRequests.get(0))?request.floor:internalRequests.get(0);} 12 13 else{ 14 15 firstaimfloor = internalRequests.get(0); 16 17 } 18 19 direction = "UP"; 20 21 return firstaimfloor; 22 23 }
并没有考虑外部为空的情况,就直接访问列表了,自然会非零返回,于是我修改了这个方法,考虑了外部为空的情况。
总结:其实修改这个对于最后通过测试点并没有多大的联系,但也为我提了一个醒,同时我对如何设置测试点有了更深入的领悟
踩坑心得3
修改多次仍无果后,我发现当我输入以下数据时

输出的结果是:

发现并没有去四楼,说明我在内部为空,外部不为空时电梯运行的代码有问题
于是我找到这段代码(在Contoller类里面的findaimfloor()中):
1 else if(!ExternalRequests.isEmpty()&&internalRequests.isEmpty()){ 2 3 clearaimfloor(aimfloor); 4 5 shiftdirection(); 6 7 aimfloor = findaimfloor(); 8 9 10 11 } 12 13 } 14 15 else if(direction.equals("DOWN")){ 16 17 if(request.floor<currentfloor){ 18 19 aimfloor = request.floor; 20 21 } 22 23 else if(request.floor>currentfloor){ 24 25 aimfloor = request.floor; 26 27 MoveToFoor(aimfloor);//错误的地方:没有转换方向就移动了,应该在前面加上shiftdirection() 28 29 clearaimfloor(aimfloor); 30 31 shiftdirection(); 32 33 aimfloor = findaimfloor(); 34 35 36 37 } 38 39 } 40 41 } 42 43 else if(!request.direction.equals(direction)){ 44 45 if(direction.equals("UP")){ 46 47 if(request.floor<currentfloor){ 48 49 shiftdirection(); 50 51 aimfloor = request.floor; 52 53 } 54 55 else if(request.floor>currentfloor){ 56 57 aimfloor = request.floor; 58 59 MoveToFoor(aimfloor);//错误的地方:没有转换方向就移动了,应该在前面加上shiftdirection() 60 61 clearaimfloor(aimfloor); 62 63 shiftdirection(); 64 65 aimfloor = findaimfloor(); 66 67 } 68 69 } 70 71 else if(direction.equals("DOWN")){ 72 73 if(request.floor>currentfloor){ 74 75 shiftdirection(); 76 77 aimfloor = request.floor; 78 79 } 80 81 else if(request.floor<currentfloor){ 82 83 aimfloor = request.floor; 84 85 MoveToFoor(aimfloor); 86 87 clearaimfloor(aimfloor); 88 89 shiftdirection(); 90 91 aimfloor = findaimfloor(); 92 93 } 94 95 } 96 97 } 98 99 }
可以发现,在向上时,当读取的下一个楼层向上但是楼层小于当前楼层时,在取得这个楼层信息后并没有转换方向再递归,这就导致不会去四楼,同样方向向下时也存在同样的问题,修改这两点后就通过了。
总结:想不通多设置几组测试数据,要涉及不同的情况,学会合理设置测试用例,其实对于解决问题有很大的帮助。
题目集六
踩坑心得1
这次题目集修改较多,当我修改多次提交上去后,发现部分正确,修改了很多方面都没用。

百思不得其解,找别人测试数据也无果(PTA提交的数据没有涉及到),感觉代码也没问题。所以我重新梳理了一下思路,发现有一个点:假设当电梯向上运行时(假设此时为八楼),如果读取到外部(方向为UP)和内部楼层信息,当它们均小于当前楼层时改如何取下一个目标楼层,我开始的思路是取近的然后转换方向,如果取到的外部的在完成移动后还要转换方向(即响应外部向上的请求),我感觉这个点可能出了问题,于是我测了一下,当我输入如下数据后:

我原来错误代码输出的结果是
Current Floor: 1 Direction: UP
Current Floor: 2 Direction: UP
Current Floor: 3 Direction: UP
Current Floor: 4 Direction: UP
Current Floor: 5 Direction: UP
Open Door # Floor 5
Close Door
Current Floor: 6 Direction: UP
Current Floor: 7 Direction: UP
Current Floor: 8 Direction: UP
Open Door # Floor 8
Close Door
Current Floor: 7 Direction: DOWN
Open Door # Floor 7
Close Door
Current Floor: 6 Direction: DOWN
Current Floor: 5 Direction: DOWN
Current Floor: 4 Direction: DOWN
Open Door # Floor 4
Close Door
Current Floor: 5 Direction: UP
Open Door # Floor 5
Close Door
很显然,它采取就近原则,到八楼后直接去了七楼,我起初是觉得没问题的,然后找正确的同学测了一下数据,得到的结果:
Current Floor: 1 Direction: UP
Current Floor: 2 Direction: UP
Current Floor: 3 Direction: UP
Current Floor: 4 Direction: UP
Current Floor: 5 Direction: UP
Open Door # Floor 5
Close Door
Current Floor: 6 Direction: UP
Current Floor: 7 Direction: UP
Current Floor: 8 Direction: UP
Open Door # Floor 8
Close Door
Current Floor: 7 Direction: DOWN
Current Floor: 6 Direction: DOWN
Current Floor: 5 Direction: DOWN
Current Floor: 4 Direction: DOWN
Open Door # Floor 4
Close Door
Current Floor: 5 Direction: UP
Open Door # Floor 5
Close Door
Current Floor: 6 Direction: UP
Current Floor: 7 Direction: UP
Open Door # Floor 7
Close Door
而原先我这部分的代码(只取了方向向上的为例):
1 else if(request.getFloor()<elevator.getCurrentfloor()&&queue.getInternalRequests().get(0)<elevator.getCurrentfloor()){ 2 shiftdirection(); 3 aimfloor = getClosest(request.getFloor(),queue.getInternalRequests().get(0)); 4 if(aimfloor == request.getFloor()&&aimfloor!=queue.getInternalRequests().get(0)){ 5 MoveToFoor(aimfloor); 6 clearaimfloor(aimfloor); 7 shiftdirection(); 8 aimfloor = findaimfloor();} 9 }
正确的输出结果是到八楼后再去四楼。为什么是这样得结果?我询问同学后明白电梯到了八楼后方向必然转变成DOWN,而外部<7,UP>是一个向上的请求,根据电梯的运行规则可知:同方向优先原则。所以,当选择去七楼实际上是不符合该原则的,因为还需要换方向。所以上面代码的判断是错误的,遇到此类情况则直接选择内部的即可,外部方向向下的情况同理。
总结:这次的问题实际上属于上一次的遗留问题,但在这次被发现了,归根结底是我对同方向优先原则没理解透彻,导致考虑时出错,所以,认真理解题意与仔细思考、设计算法很重要!!!一定要时时记得题目的要求!此外,学会设置测试用例也很总要!!!
题目集七
踩坑心得1
在题目集七中,修改后提交,发现第一个测试点没过去,多次提交后也如此

后来,在输入如下测试数据后:

输出如下结果:

发现并没有到六楼,单步调试后发现在Controller类里面的processRequests方法里,<3,6>并没有被存进去,检查代码发现存在以下问题:
1 if (request.contains(",")) { 2 3 String[] str = request.split(","); 4 5 int floor1 = Integer.parseInt(str[0]); 6 7 int floor2 = Integer.parseInt(str[1]); 8 9 Passenger p1 = new Passenger(floor1,floor2); 10 11 if (elevator.isValidFloor(floor1)&&elevator.isValidFloor(floor2)) { 12 13 if(queue.getExternalRequests().isEmpty()){ 14 15 queue.addExternalRequests(p1);} 16 17 else if(!queue.getExternalRequests().isEmpty()){ 18 19 int m = queue.getExternalRequests().size()-1; 20 21 Passenger p = queue.getExternalRequests().get(m); 22 23 if(!(p.getSourceFloor()==floor1&&p1.getDestinationFloor()==floor2)){//这里的p1应该是p 24 25 queue.addExternalRequests(p1);}} 26 27 } 28 29 }
由上面23行的注释可知,因为在判断时是否重复输入时p不小心打成了p1,导致出错,正可谓“失之毫厘,谬以千里”。
当然,在命名上也有问题,如果我的p和 p1 在命名上如果有明显的区别,而不只是单纯以数字区别,那么就不会出现这样细微的错误,更有利于寻找错误,利于代码的维护性
总结:从上面的错误可以看出敲代码时一定要细心,命名规范可以减少一些简单而难以发现的错误。此外,学会设置测试用例仍旧很重要,它可以帮我们找到那些被忽略的问题
踩坑心得2
其实是前两次的,但是我不记的具体在哪个地方了,就拿这次的题目来讲解,同样还是刚才上面的代码,如果是if(!(p.getSourceFloor()==floor1&&p1.getDestinationFloor()==floor2)),输入和上面踩坑心得1一样的数据,输出的会是
Current Floor: 1 Direction: UP
Current Floor: 2 Direction: UP
Open Door # Floor 2
Close Door
Current Floor: 3 Direction: UP
Open Door # Floor 3
Close Door
Open Door # Floor 3
Close Door
Current Floor: 4 Direction: UP
Current Floor: 5 Direction: UP
Open Door # Floor 5
Close Door
Current Floor: 6 Direction: UP
Open Door # Floor 6
Close Door
而如果改成这样if((p.getSourceFloor()!=floor1)&&(p.getDestinationFloor()!=floor2))(改成if(!(p.getSourceFloor()==floor1)&&!(p.getDestinationFloor()==floor2))也是一样的),输出的是

显然,结果是不一样的,第一个要求源楼层和目标楼层都不等于指定楼层才会添加请求;而第二个if语句只要源楼层和目标楼层不同时等于指定楼层就会添加请求。写的时候没注意的话就会犯这样的错误。
总结:写代码一定要细心,对与&&和||的用法一定要熟练掌握。
改进建议
- 单一职责原则实现不完全。代码很多地方是可以合并,比如在题目集五的电梯题目和题目集六的电梯题目中,我就把题目集五StartMove在题目集六的时候合并在一起了。在题目集六中也对findaimfloor()做了一定简化。在题目集七的Controller类里面的clearRequests方法里,对于删除头部的情况分的太细,其实不用分只有外部为空或内部为空的情况,就直接检查列表,不为空就把列队里面第一个符合的删除掉,这样可以简化代码,然后Controller类里面的findaimfloor方法也不够简结,分支语句过多,还使用了递归等复杂度较高的方法。这些都可以做到进一步简化,从而实现单一职责原则。
- 代码注释问题:前两次代码注释过少(从SourceMonitor的分析结果就可知),第三次好一点,但是代码注释还是不够,代码可以适量多加一些注释。
- 命名规则规范问题:方法名、变量名的命名仍不规范(比如前文常提到的findaimfloor,应该为findAimfloor,代码里面的MoveToFoo也拼写错误了,应该为MoveToFloor),这造成代码难以维护与修改,可以适量修改、完善一下方法、变量等的名字,更有利于代码可读性与维护性。
- 这次我使用的是ArrayList没有使用LinkedList,二者使用上无太大的区别,其实LinkedList对于删除等操作更方便,可以尝试一下使用LinkedList。
总结
通过这三次电梯迭代,我学到了以下内容
- 学会了使用ArryList的基本用法
- 了解并学会了LOOK算法
- 通过每次题目集的迭代,学会了面向对象编程,从原来C语言的面向过程转变到了面向对象编程。学会了类的使用,如何设计类间关系、以及通过设计多个类来实现单一职责原则。
- 一定程度上提高了思考能力与解决问题的能力。这次题目集的题目有一定的难度,在不断思考、写代码、以及纠正过程中提高了解决问题的能力,同时也学会了自己设置测试用例来纠错,对于自己的学习能力有很大的帮助
当然,在这几次迭代中也暴露了诸多问题,也需要我去学习改正
- 加强对基础知识的学习。其实对于ArrayList的用法并不熟练,基本上是我想用哪个就去搜索,应该加强对这些基础知识的掌握
- 单一职责原则仍未掌握完全,导致代码仍过于复杂,这需要我在今后的学习和实践过程去不断去理解、掌握
3. 课后学习使用LinkedList
4. 养成给代码写注释的习惯
5. 学会设计测试用例
6. 学会命名规范
建议
- 有的题目集测试用例过少(比如第一次的电梯题目),这样可能会导致其实代码有问题但是仍能过测试点。在题目集六的迭代中,我找别人测试一组数据,找了好几个,他们测出来的结果各不相同但是都能过,说明测试用例考虑的情况太少,可能会导致这个问题存在
2.题目集迭代结束后可以试着推荐一下优秀学生的思路或方法(他们如果同意的话),可以促进同学间的互相学习、共同进步。
 
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号