Java电梯调度作业总结
一、前言:
当着手开始第一次作业之际,那满屏幕的题目映入眼帘,对我而言着实犹如当头一棒。在历经了多种不同方法的尝试之后,运行超时的问题却依旧是无法得到解决。虽说在这几次作业当中,我始终因为运行超时未能通过测试点,可从这三次作业里我着实也学到了诸多的知识。在第一次作业中,我对于类和对象的概念还颇为模糊,就把所有的代码全堆在了一个类当中,如此一来,在运行的时候便出现了各种各样的报错情况。到了第二次作业的时候,老师提及了单一职责的要求,我开始得把一个类拆分成不同的类,让它们各司其职、分工协作。第三次作业又加入了乘客类,外部请求还得转变成内部队列,刚开始看到这些的时候,只觉得头晕目眩的,不过在慢慢地梳理顺畅之后,便发觉代码整体变得更加有条理起来了。
这三次作业,其核心所围绕的均为电梯调度,但是每次作业又都呈现出了全新的挑战。第一次重点是实现电梯运行逻辑,第二次是优化类结构,第三次是扩展请求功能。题量不算大,但每道题都需要反复调试,比如第一次作业我改了十几次才通过样例测试,每次以为没问题了,运行时又冒出新问题,比如输出少了某一行,或者电梯方向反转不对。但也正是这些问题让我对Java的类、队列、循环逻辑有了更深的理解。
二、设计与分析
(一)第一次作业:
1.设计思路
在初次着手撰写作业之际,我个人认为电梯这一类应当囊括所有的相关功能,像是对输入内容加以处理、对请求队列展开管理、对电梯的移动予以控制以及将状态进行输出等。基于这样的想法,我便在一个名为ElevatorSystem的类当中创建了一个Elevator的内部类。在这个内部类里,设置了innerQueue以及outerQueue这两个队列,它们分别被用于存储来自内部和外部的请求。此外,还设置了run方法,其作用是用来处理电梯的运行事宜。就比如下面所展示的这段代码便是如此情况:
public void run() { while (有请求) { 确定方向; 移动一层; 输出当前楼层和方向; 如果需要停靠 { 开门关门; 移除队列里的请求; } } }
当时觉得这样写很完整,但后来发现问题很多:run方法的代码行数多达200多行,其复杂程度颇高,涵盖了方向的判定、楼层的移动以及输出逻辑等方面,甚至连输入处理都是直接在main方法里对队列展开操作的。利用代码分析工具加以查看后发现,这个类的复杂程度着实很高,哪怕只是想要修改一个小小的功能,都得在一大片代码当中寻觅许久,而且极其容易找错地方进而改错了。
2.代码分析
最初,题目设定的任务是要达成电梯调度的那些基本功能,像对内外部请求队列展开处理,对LOOK算法实施方向方面的控制,还有完成状态输出等等事宜。一开始的代码,是把所有的逻辑全都集中放置在了ElevatorSystem这个类当中,这里面还包含有Elevator内部类,这个内部类主要负责的就是电梯的相关状态。而关于输入处理这块呢,则是在main方法里面来完成的,其主要工作就是对请求进行解析,然后再把解析好的请求添加到对应的队列当中去。
3.SourceMonitor 报表分析
优点:
代码行数为 217,相对简洁。
方法平均语句数为 7.83,方法长度较为适中。
缺点:
最大复杂度为 8,如ElevatorSystem.determineInitialDirection()方法行数为 59,逻辑复杂,可读性和维护性差。
百分比分支语句为 20.7,分支判断较多,逻辑不够清晰。
从报表当中能够看出,Elevator类所具备的方法在数量上是比较多的,其中涵盖了像enqueueInner、enqueueOuter、determineInitialDirection、run等这样的核心方法,并且该类的复杂度也颇高。其主要缘由在于它把输入处理、队列管理以及调度算法都紧紧地耦合到了一块儿。就拿run方法来说吧,它包含了电梯运行时的主循环部分,要对方向的判断、楼层的移动以及停靠的逻辑等进行处理,其代码的行数已然超过了200行,这显然是违背了单一职责原则的,进而使得其可读性以及维护性都变得比较差了。
(二)第二次作业:
1.设计思路
第二次作业有着需要遵循单一职责的要求。于是,我便把代码拆解成了四个类。其中,Elevator这个类仅仅负责电梯的状态方面的事宜,同时也负责与之相关的移动逻辑;RequestQueue类则是专门用来管理请求队列的,会对输入的有效性进行处理,并且完成去重的操作;Controller类负责的是调度方面的逻辑;还有ExternalRequest类,它主要是对外部请求的楼层以及方向加以封装。就拿RequestQueue类来说吧,其内部的方法所承担的任务仅仅是把请求添加到队列当中,与此同时,还要对无效楼层进行过滤,并且处理重复请求的情况:
public void addInternalRequest(int floor) { if (楼层有效且队列里没有这个请求) { internalRequests.add(floor); } }
Elevator类变得干净了,只关注电梯怎么动,比如move方法处理越界反转方向:
public void move() { int nextFloor = 当前楼层 + (方向是UP ? 1 : -1); if (nextFloor超过最大或最小楼层) { 方向反转; nextFloor = 当前楼层 + 新方向的步数; } 当前楼层 = nextFloor; }
经过这样一番拆分处理之后,代码的可读性着实提升了不少。就拿发现电梯方向反转存在问题这一情况来说吧,此时仅仅需要去检查Elevator当中的reverseDirection这个方法就可以了,完全没必要在那一大摞繁杂的代码里面去费力寻找。
2.代码分析
第二次题目给出的任务是要把电梯类的职责进行拆分,为此引入了RequestQueue类来对请求队列加以管理,让Controller类去负责调度方面的逻辑,而Elevator类则主要聚焦于电梯状态以及移动逻辑,同时新增ExternalRequest类用于封装外部请求。这一版本是依照单一职责原则来开展相关工作的,它把原Elevator类当中的队列管理以及调度逻辑分离开来,进而形成了四层结构。
3.SourceMonitor 报表分析
优点:
方法平均语句数为 6.32,方法较为简洁。
缺点:
最大复杂度为 8,如ElevatorSystem.determineInitialDirection()方法行数为 59,逻辑复杂,可读性和维护性差。
百分比分支语句为 20.7,分支判断较多,逻辑不够清晰。
在完成重构之后,Elevator类所具备的方法数量出现了减少的情况。此时,Controller类承担起了调度方面的逻辑工作,而RequestQueue类则负责处理队列相关的操作,如此一来,各类的复杂度都有了显著的降低。就拿RequestQueue类来说吧,它的addInternalRequest以及addExternalRequest这两个方法主要是专注于将请求放入队列以及对请求进行去重的操作。再看Controller类,它的processRequests方法对调度流程进行了整合,使得逻辑分层变得清晰起来,而且代码行数的分布也更加趋于合理了。
(三)第三次作业
1.设计思路
第三次作业对外部请求格式做出了更改,改成了<源楼层,目的楼层>这样的形式。在处理完外部请求之后,还需要把目的楼层添加到内部队列当中。我又新增加了Passenger类。不管是那种只需要目的楼层的内部请求,还是既需要源楼层又需要目的楼层的外部请求,都可以通过Passenger类来进行封装处理。就拿外部请求的处理逻辑来说吧,它是在Controller里完成的:当电梯抵达源楼层的时候,就会把目的楼层当作内部请求添加到队列之中:
for (Passenger p : 外部请求队列) { if (当前楼层 == p的源楼层) { 队列.addInternalRequest(p的目的楼层); // 把目的楼层加入内部队列 外部请求队列移除这个乘客; // 处理完外部请求 break; } }
2.代码分析
第三次题目调整时,其外部请求的格式设定成了<请求源楼层,请求目的楼层>这样的形式。在此情况下,要求电梯在对外部请求完成处理之后,要把目的楼层添加进内部队列当中。基于这样的需求,新增加了Passenger类,这个类的作用是对内部请求以及外部请求进行统一的封装处理。另外,RequestQueue类也做出了相应调整,调整后它主要负责对Passenger对象展开管理工作。当处理外部请求的时候,在电梯停靠到源楼层之后,就会把目的楼层当作内部请求,然后将其加入到队列里面去。
3.SourceMonitor 报表分析
优点:
类和接口数为 4,职责划分更合理,符合面向对象设计原则。
方法平均语句数为 5.95,方法简洁,便于理解和维护。
缺点:
最大复杂度为 14,如Controller.hasSameDirectionRequests()方法行数为 184,复杂度高,可读性差。
百分比分支语句为 19.3,分支判断较多,逻辑复杂度较高。
三、采坑心得
1.方法权限使用错误
第二次作业中,我将Elevator里的reverseDirection方法设置成了private属性。结果Controller类调用时一直报错“无法访问私有方法”。在那个时候,我对于类的访问权限相关知识还不太了解。直至后来,我才真正明白过来,原来private方法仅仅只能在其所属的本类内部得以使用。而Controller和Elevator这二者属于不同的类,需要使用public来对其进行修饰才行。
2.输出少了初始楼层
第一次作业测试时,发现输出结果当中并未出现‘Current Floor: 1 Direction: UP’这样的一行内容。经过一番调试之后才发现,原来是在确定了电梯的初始方向之后,并没有即刻就进行输出操作,反而是直接让电梯开始移动了。基于此情况,便在determineInitialDirection方法的后面额外增添了一行输出的代码,如此一来,问题便得以顺利解决了。这让我知道,每一步状态变化都要及时记录,否则很难发现中间步骤的缺失。
3.边界条件
在处理第三次作业的外部请求之际,起初并未对源楼层以及目的楼层是否处于有效范围之内展开检查。就好比输入了<22,DOWN>这样的内容(这里假定最大楼层为20),程序便径直报错了。随后在RequestQueue的入队方法当中增添了相关判断环节:
if (floor < 最小楼层 || floor > 最大楼层) { return; // 忽略无效楼层 }
存在重复请求这样的问题,例如连续输入三个<3>时,实际上队列里只需存储一个3便足够了。我运用List.contains这一方法来核查是否已经存在该项请求,以此防止队列当中出现重复的项目,要知道若是出现重复项的话,电梯就会在同一楼层停靠三次,这显然是不合理的。
四、改进建议:
1.先画类图,再写代码
初次完成作业之时,并未绘制类图,完全是想到哪儿便写到哪儿,结果所构建的类结构显得十分混乱。而后,在运用类图加以规划之后,便能够清晰明了地知晓每个类应当具备哪些方法,以及它们之间是怎样进行交互的,这样在编写代码的时候,思路也变得更加清晰起来。在此建议,不妨先花费一点时间去绘制一个较为简单的类图,比如说,明确电梯类具备哪些属性,队列类需要哪些方法,控制类又是怎样去调用它们的,通过这样的方式,便能够有效避免在后续的作业过程中出现大量返工的情况。
2.分步骤测试,不要一次性写完
在完成三次作业的过程中,我均因一次性写完再调试而遭遇了问题,具体而言就是当代码量偏大的时候,要找出其中的错误就变得极为困难。之后我逐渐摸索出了分步骤完成作业的方法:首先着手编写输入处理部分,并且对其能否解析请求以及将请求成功加入队列进行测试;接着去写电梯移动相关内容,同时测试其是否能够对楼层以及方向做出改变;最后再撰写调度逻辑部分,并且检验其是否会依照既定规则来处理请求。在每一个步骤当中,都运用简单的测试用例来加以验证,就拿第一次作业来说,先是测试电梯从1楼运行到3楼时是否能够正常停靠,随后再测试当存在多个请求的情况下,其处理顺序是否正确。
3.代码注释:写给未来的自己
起初的时候,会觉得添加注释这件事挺麻烦的。然而,当过去了一周的时间之后,再去看第一次作业所写的那些代码时,却惊讶地发现有好多地方都已经看不明白了呢!从那之后,便逐渐养成了一个习惯,给每个类、重要方法、复杂逻辑加注释。就好比在Controller这个类里面的determineDirection方法当中,会添加注释说明要优先去处理那些离当前楼层距离最近的请求。这些注释能帮自己快速回忆逻辑。
4.学习使用工具:代码分析和调试
经由这几次作业,我得以学会运用IDE的代码分析工具去查看类的复杂度,还学会利用其调试功能来单步执行代码,进而观察变量的种种变化。在调试之时设置好断点,随后一步一步地去查看电梯是怎样移动的、队列又是如何变化的,这相较于盲目地打印日志而言,效率可就高得多了。
五、总结:
经历三次电梯调度作业,仿若历经三次代码的升级蜕变:最初是将所有逻辑杂乱无章地堆砌在一处,如同大杂烩一般;随后逐步拆分成多个不同的类,各个类之间分工协作;直至最终形成能够灵活应对需求变化的设计。于此过程中,我收获颇多。
1.将现实里的电梯、请求以及乘客等元素抽象化为各类,每个类只做自己的事。如此一来,代码会显得更为清晰明了,在后续进行修改时也会变得更加容易。
2.选择用LinkedList又或者ArrayDeque来对请求予以存储,采取先进先出的规则,在处理这些请求之时依照顺序去做检查。就好比在内部请求队列当中涉及到的楼层相关情况,电梯是会依照顺序逐一到达的。
3.在处理请求的过程中,要充分考虑各式各样的情况。比如无效楼层、重复请求、方向反转的条件,这些都需要在代码里用条件判断处理,否则程序就会出错。
当然了,我自身存在着诸多不足之处:三次作业均出现了运行超时的情况,而且到最后也没能将问题妥善解决;第三次作业的调度逻辑的高效程度尚有欠缺,在面临诸多请求之时,有可能会出现绕路的现象;类之间的交互还能够进一步加以简化,部分方法调用也是有优化的空间的。不过正是这些不足之处,使得我明确了接下来需要学习的内容:比如学习更多的设计模式,以此来让代码具备更高的灵活性,再比如学习单元测试方面的知识,从而让代码的可靠性得以提升。