OO第二单元总结
(1)多线程协作与同步:
首先考虑电梯每一个运行周期需要的内容:
关于请求分配:
第一步获取当前楼层需要上楼的乘客的时候会遇到作业设计的第一个难题,就是乘客的分配问题,我采取的是不基于电梯当前运行状态的动态分配,即调度器中的请求队列被放入请求时不进行任何分配,仅在电梯访问每一楼层时再考虑分配。对于这种分配,采取贪心的算法,能上则上。
关于换乘:
采用静态表的方式实现换乘,当需要换乘时如:A-B-C,对于电梯而言换乘是透明的,电梯仅会收到一个A-B的人,同时再等待队列中加入B-C。为此需要修改Person类增加一个valid位,当换乘A-B完成之后激活B-C
关于wait与notify的时机:
wait的条件十分重要,设计错误可能会出现死锁问题。我采用的是等待队列为空且输入未终止,就让“无乘客需要引导”的电梯wait,当出现新请求或换乘请求的后段被激活时均需要notifyAll
关于终止:
由于采用了请求的有效性判断,终止条件均为输入终止且等待队列为空且自身为空,判断十分简单,但是每一次等待队列为空都要notify进行一次判断。
(2)设计的可扩展性
功能方面:
第三次作业采用简单工厂模式来创建电梯对象,电梯类的设计过程中也充分考虑了电梯各个属性的可变性,没有采用hard code,因而从电梯功能的角度来说,扩展性良好;电梯调度算法的基础部分(不考虑优化),可以适应可达楼层的各种变化,新建了一个包含静态变量的Config类来统一调整各个参数,因而从楼层功能的角度来说,扩展性良好。
性能方面:
①第三次作业在电梯换乘调度方面,采用了静态表的方法来实现调度,这种方法扩展性极差。首先,静态表的制作较为麻烦,如果楼层数量/电梯种类大幅增加,工作量巨大,并且如果电梯的适用性较差,出现多次换乘的情况,复杂度进一步提升。其次,静态表的制作严重依赖于各个电梯能到达的楼层,如果一种电梯发生变化,整张表可能需要重制。综上静态表是一种扩展性极差的优化方式。不过唯一的好处在于,静态表一旦制作完成,在实际运行中时间复杂度为常数。关于换乘的一种比较好的实现方法是基于图的可达性,自动计算出加权最短路径和换乘节点。
②关于调度算法,采用了可同向捎带的SSTF+LOOK,对于捎带条件、回头条件的扩展都较为方便。
SOLID原则:
Open Closed Principle:开闭原则
从三次作业迭代的角度来看,开闭原则实现的较好,基本上都是在原有的基础上增加一些功能,没有发生重构。
Liskov Substitution Principle:里氏替换原则
作业中没有出现继承关系,并认为继承并非必要或优的,故不考虑此原则
Interface Segregation Principle:接口隔离原则
作业中没有使用接口,对于接口隔离原则本身我觉得对我启发极大,但是本次作业中认为使用接口并非必要或优的,故不考虑此原则
Dependence Inversion Principle:依赖倒置原则
反思自己的代码,确实高层级的类过分依赖于低层级的类,降低了扩展性,但是考虑到电梯的实际背景,是可以接受的,因为扩展主要会出现在电梯功能和楼层到达上。
(3)基于度量的分析
先上三次作业的类图
第一次:
第二次:
第三次:
优点:可以发现三次作业类的区别不大,只有三次作业因为出现不同型号的电梯引入了ElevatorFactory类,此外类间关系符合生产者-消费者模式,架构比较好。
缺点:主要的复杂度都出现在了scheduler的getPassenger方法和Elevator的run方法,主要原因在于getPassenger复杂主要的请求分配,情况复杂采用了较多分支结构,Elevator的run方法由于内含的过程比较多也比较复杂。
时序图:
(4)分析自己程序的bug
三次作业强测/互测均未发现自己代码的bug,还是分析一下自己coding过程中发现的bug吧
①ConcurrentModificationException:这个报错在第一次作业中被发现,经过查阅资料发现主要出现于遍历某些容器时同时对容器进行增删操作时出现。当时没有发现具体的原因,但是定位到是我电梯类中的一个HashMap出现了问题,于是将其改为ConcurrentHashMap解决了bug。以后作业中未再出现类似问题,不过在一次静态阅读代码时,发觉我的一个方法在遍历HashMap时同时调用了remove方法,从代码逻辑上来讲没有问题,但是从HashMap迭代器实现的角度来说,迭代过程中如果remove了某个key可能会出现问题,最终找到了原因
Debug过程中出现的问题(希望路过的老师同学可以解答一下哦):多线程的调试很大程度上依赖于在程序关键位置打印信息。在C语言中可以用预编译指令#ifdef等等来方便的控制调试语句的打印,同时又不将调试代码编译进最终的可执行文件。而在java中没有预编译指令,一种类似的办法是设置一个全局变量debug,并用条件分支语句来判断打印与否。但是这样的问题在于一方面我们是有checkstyle的并且规定了每个方法不能超过60行,而往往需要调试的方法都是关键性的方法,可能已经接近60行,加入调试语句之后往往不得不再次注释掉,降低了迭代开发下的效率;此外,照理讲正是提交的文件不该包括调试相关代码,而仅仅是产品必须的代码,以达到精简的目的。
(5)发现别人程序的bug
首先自我深刻反省一下,在第三次作业中不小心一并提交了自己代码的zip压缩文件,结果压缩文件的名字正好是当前目录名同时也是远端仓库名oo_course_2020_18231094_homework_7,暴露了学号违反了课程相关规定,也痛失了互测的分数。不过在前两次作业中,还是hack了别人8.5分,嘿嘿嘿。
本次作业依然采取的是自动测评+手动构造样例的测试方法。
自动测评:
本单元作业随机测试样例的生成非常简单,不再赘述。关于测评机的自动发射问题,可以改写输入接口,采用定时sleep的方法来进行按时发射。正确性的测试是一个不容易的地方,需要维护对应电梯的相关信息,随着指令不断update相关信息并且设计好合法边界才行。
手动构造样例:
主要考虑两方面的样例:①功能性测试,穷举所有可能的from-to样例,尤其是在第三次作业中,不少同学采用静态表的方式实现调度,有可能会出现某种from-to调度不能实现,比方说与3层相关的调度②大量同时样例,比如在[1.0]时刻投入50个样例,前两次作业中均发现有同学wa于这样的样例,没有仔细阅读代码但是一种可能是:某些操作的原子性未完全实现,假设一个操作需要A与B整体是原子性的,可能其A与B本身都是原子性的,但是A+B没能实现原子化并且鲁棒性不强,大量样例同时输入时就会出错。我的代码中也有类似的问题,但是经过细致分析,我的代码出问题只会导致性能降低(需要多跑一个循环),但是不会出现结果错误,因而没有予以调整。
发现线程安全相关问题策略:由于前面也提到,我没有出现线程相关安全问题,因而没有特别的去针对线程安全相关问题,仅在手动构造样例中有所考虑。
与第一单元差异:应该注意到的一个问题在于,第一单元自动测评程序,按操作系统课的说法,是计算密集程序,CPU占用率极高(需要进行求导、反复代值计算);而本次作业测评时大部分时间花在了等待电梯运行上,CPU实际上被空置,因而为了提高效率测试程序本身也应该是多线程的(或者通过同时打开数十个测试程序实现多进程),来提高测试效率。否则一个小时只能跑几十个样例。
(6)心得体会
线程安全:虽然自己没有出现什么线程安全相关错误,但是也看见听见不少同学出现各种奇奇怪怪的问题,仔细一想只是自己设计的比较简单而已,稍微复杂的一点的设计就很容易出现难以调试的bug。此外自己coding的时候都简单的使用方法级的synchronized来进行控制,但是还有很多方式比方说各种lock自己还不会用,还有很多可以学的!
设计原则:感觉设计原则就和数学中的一些思想一样,高度凝练但是能够帮助我们实现高内聚低耦合,尤其是依赖倒置原则有些反直觉但是确实是对的!