BUAA面向对象设计与构造——第二单元总结

时间:2023-12-25 10:33:49

BUAA面向对象设计与构造——第二单元总结

第一阶段:单部傻瓜电梯的调度

第二阶段:单部可捎带电梯的调度

  (由于我第一次写的作业就是可捎带模式,第二次只是增加了负数楼层,修改了一部分参数,因此一起总结。)

  1.设计策略

  这次作业我设计了两个线程:Elevator和DealWithInput,前者模拟电梯,而后者用来处理输入。队列PersonQueue作为一个单例模式下的对象,被两个线程所共有,充当调度器的角色。

  DealWithInput基本套用下发的模板,只是增加了每次读到新请求就通知调度器pq.add(requst),并且pq.notifyall(),以用来唤醒电梯。在读到EOF后,设置标志输入结束的flag,并再次唤醒电梯,防止电梯一直等待。

  PersonQueue调度器里包含了一个Floor类型的数组,将每个楼层独立处理。每个Floor类中包含一个记录等待乘客的数组waitingPersion,乘客按到来的先后顺序进入数组。描述乘客的Person类记录了每个乘客的ID、起始楼层、目的楼层和运行方向。电梯向调度器询问主请求时,调度器扫描所有楼层,查看是否为空,只要有一个楼层不为空,电梯就仍需运行。当到达某一层后,电梯需要捎带,这时又要向调度器发出询问。调度器将这个询问转交给对应的Floor对象。Floor遍历本楼层全部乘客,如果有相同方向的乘客,则把这些乘客打包成一个数组返回给调度器,并且从该楼层的等待队列删去。之后调度器再返回给电梯。

  Elevator采用扫描算法,即当电梯静止时,先向调度器询问主请求,根据主请求决定运行方向,之后到达每一层如果还有同方向的乘客则捎带。下乘客则是由电梯自己判断,电梯包含了一个Person类型的eleList数组,记录电梯里的人。每到一层,先下后上,电梯遍历数组查看是否有人的目的楼层是当前层,如果有,则让他下电梯并且从电梯数组中删除。上下乘客结束后,电梯要向调度器询问是否需要改变方向。改变方向后进行二次上乘客。这个操作是针对这种情况:例如电梯向上运行到达5层,所以第一次查找乘客时只会查找5层要向上的乘客。但是如果这层没有人向上,电梯内已经没人了,5层以上的楼层也没有需求,因此电梯可以改变方向下,并且接5层向下的乘客。并且二次查找乘客也可以尽量多的捎带。

  时序图如下:

  BUAA面向对象设计与构造——第二单元总结

  第五次和第六次作业的一个比较大的区别在于:

  第五次作业电梯采用轮询方式,比较占用CPU。

  第六次作业改成wait()和notifyall()的模式,如果楼里没有请求,那么电梯会wait,而当输入进程读到了新的请求时会唤醒电梯进程。当读到EOF时,也需要唤醒电梯防止一直等待。

  2.结构度量

  类图如下:

  BUAA面向对象设计与构造——第二单元总结

  类图展现了类之间的实例化和创建关系。主进程MainClass创建了Elevator和DealWithInput两个进程,DealWithInput直接向PersonQueue增加请求,PersonQueue管理多个Floor类,每个Floor管理等待的Person.Elevator进程和调度器互相传递信息,同时也管理Person来记录电梯内部的人。

  BUAA面向对象设计与构造——第二单元总结

  以上是关于程序本身的一些数据统计。可以看到每个类的平均循环复杂度还是比较低的,说明方法的设计比较符合“高内聚、低耦合”的原则,而不是大多数功能集中在某一个方法中。

  3.程序bug

  第五次作业在公测中没有什么bug,但是用相同的架构在第六次的弱测就发现了bug。

  首先最致命的bug是程序CPU运行时间超时。因为我第五次作业采用轮询模式,电梯一直在while循环,每次循环都询问调度器是否需要运行。这种模式在第五次比较简单的测试中侥幸存活了下来,但是第六次就不行了。因此我改为了等待与唤醒的模式。电梯如果询问调度器得到的回答是暂时不需要运行,那么将调用pq.wait()进行等待。而输入进程一旦获取到新的需求,就调用pq.notifyall()来唤醒电梯。

  其次是电梯的方向问题。第五次作业我的电梯是送完乘客且运行方向的楼层没有乘客了,才进行方向的改变。但是这样有可能发生在某一层第一次开门下乘客,然后电梯意识到要改变方向,再次开门上乘客。因此为了节省时间以及更加符合实际,我在每次开关门之间增加了对运行方向的判断,修改为二次搭乘模式。

第三阶段:多部可捎带电梯的调度

  1.设计策略

  这次我依然选择将调度器作为一个共享对象,而不是进程的模式。输入进程依旧存在,电梯进程变成了三个,电梯单独运行的框架和前两次类似,也都是先向调度器询问主请求(即启动方向),每到一层都询问可捎带乘客和该下电梯的乘客。

  第七次作业比较大的一个改进在于赋予了Person类更多的功能。就像老师上课讲的工厂和工人的例子一样,乘客可以自己判断上下电梯,并且在一开始就规划换乘的路线。每当实例化一个Person对象时,将调用内部方法,来判断该乘客是否需要换乘。如果不需换乘,那么就当作正常的乘客,记录其起始楼层和目的楼层。如果需要换乘,那么将调用方法来得到他的换乘楼层(middleFloor),并且将这位乘客的目的楼层存入换乘楼层。当该乘客在目的楼层(即换乘楼层)下电梯后,再修改自身的起始楼层为换乘楼层,目的楼层为真正的目的楼层,将更新后的这名乘客再次加入等待队列。这样相当于把要换乘的乘客分为两名乘客,先后加入等待队列。这样就把换乘这一难题交付给乘客来完成,调度器辅助更新,而电梯只需要正常运行,更加清晰有效地分配了功能。因此IN和OUT这两个方法都在Person类,而不是电梯进程来完成。

  按照题目中电梯楼层的分配,有一些乘客是可以乘坐多个电梯都能到达的。例如ABC三部电梯都可以从1层到15层。因此我先在Person类中加入了可乘坐的电梯这一属性,用三位二进制数表示是否能乘坐对应电梯,例如其值为111表示三部电梯都可乘坐。这一属性是在判断换乘时同时完成的,而对于要换乘的人,在他第一次OUT后更新楼层信息时,同时更新这个属性。

  为了实现三部电梯的配合,我又引入了按钮属性。每个Floor类增加了三个数组,每个数组有两个元素,这六个值描述了ABC三部电梯上下行共六个按钮。当乘客第一次加入某一楼层等待队列时,考虑以下三种情况:

  1.只能坐一部电梯,那么就按对应电梯对应方向的按钮;

  2.可以坐两部电梯AB,那么若A到达时间短/A排队人数没有满员/B的反方向按钮被按则按A电梯,否则按B电梯。关于B的反方向按钮被按这一点,主要是因为如果B电梯要先以反方向经过本楼层的话,那么同方向会等更久。因此如果A的理论到达时间更短就优先选A。

  3.可以坐三部电梯,那么类似于第2种情况,从三部中选一部按按钮。

  因此每个乘客在刚进入等待队列时,都只按了一个理论上最合适的电梯。当然这只是理论值,不能排除电梯因为停靠楼层开关门所导致的时间延误。因此当电梯A(BC同理)每运行一层时,还要通知调度器刷新按钮。对于楼层x,如果此时A刚从x层上行,那么我们可以推测对于A再次以下行方向返回x层需要比较久的时间,因此对于低于x层的楼层,如果等待A电梯的人势必要等很久。因此这时刷新所有<=x层的楼层,如果等待A电梯的乘客中,有可以乘坐BC电梯的人,那么也按BC的按钮,这样如果BC电梯先于A电梯到达,那么这些人就可以早一步上电梯。

  这样一来,每个人虽然最开始只按了一个按钮,但可能因为刷新操作按多个。因此当电梯停靠一层并进行了上下乘客操作后,要遍历该楼层的所有乘客再次进行按电梯操作。这里的按电梯原则与最开始是一样的,都只按一个。按多个的刷新是在电梯运行到其他楼层时才进行。

  通过按钮这个属性,使得电梯之间的调度更加充分,而不是只集中在一部电梯上。每部电梯在向调度器询问时,调度器遍历每一楼层的按钮即可安排电梯接下来的运行方向了。

BUAA面向对象设计与构造——第二单元总结

  2.结构度量

  类图如下:

  BUAA面向对象设计与构造——第二单元总结

  类图展现了类之间的实例化和创建关系。主进程MainClass创建了Elevator和DealWithInput两个进程,DealWithInput直接向PersonQueue增加请求,PersonQueue管理多个Floor类,每个Floor管理等待的Person.Elevator进程和调度器互相传递信息,同时也管理Person来记录电梯内部的人。Person类拥有了更多的功能,诸如向调度器报告按按钮,在电梯进程中进行IN和OUT的操作等。

  BUAA面向对象设计与构造——第二单元总结

  这一次作业Floor类和Person类的复杂度增长幅度很大,主要是因为赋予了Person更多的功能,以及Floor添加了按钮属性所导致的。但总体来看复杂度还算比较平均,没有出现某一类包揽大多数功能的情况。

  3.程序bug

  公测中全部通过,但性能分不太高。我认为是因为我的电梯调度还是针对于一些细节部分的优化,例如尽量多按按钮来使得乘客尽早上电梯。但是这也可能导致电梯空跑。我觉得一个绝对的优化策略还是很难找(几乎不存在)的,因为电梯对即将到来的乘客是未知的。因此优化这一点只能尽力从架构上设计地更完备,考虑更多的情况。

  在弱测和本地测试中,我发现我还是遇到了CPU超时的情况。虽然单部不会忙等,但是由于多部电梯同时工作,才会导致这一错误。当AB电梯都没有需求后,他们发现输入也结束了,但是楼里还有人,因此会进入等待状态,此时C还在工作,当C结束工作后,他检测到输入也早早结束了,因此结束了进程。但是此时AB没有被唤醒,也就无法检测楼里没人这个真正的结束条件。另一种情况是最后一个乘客需要换乘,他从C电梯出来后,AB电梯还在等待,也就没有电梯来接他了。因此我对于电梯在每一层的运行结束后都加入了notifyall,这可以解决以上两个问题,并且对于不需要工作的电梯,即使被唤醒后也能马上再次进入等待。

总结 

  根据SOLID原则分析:

  SRP(单一责任原则):我一共设计了MainClass, DealWithInput, Elevator, PersonQueue, Floor, Person六个类,前三个类分别是三个进程,第四个作为调度器,后两个分别针对楼层和乘客两个角度设计功能。实现了功能的划分,使得程序架构条理清晰。

  OCP(开放封闭原则):指架构应该是可扩展的。这个单元我做的比较好的一点就在于减少了代码的重构,第六次和第五次代码几乎一样,只是针对bug和指导书不同的要求进行一小部分的修改。第七次作业里,每部电梯的运行架构也和第六次一样,只是增加了电梯间配合调度的功能。

  由于本次作业没有使用继承、接口和抽象类,因此其他三个原则不做讨论。

  对于线程安全方面,我采用synchronized加锁的模式。所有针对调度器即等待队列的操作都需要加锁,包括添加乘客,捎带乘客(即取走乘客)和判断是否为空等。这样才能保证对象的安全,防止出现一个乘客同时上两部电梯,或者是从队列中删除但是却没有上电梯的错误。

  总之经过这一单元的练习,增强了我对多线程的认识,对于线程安全有了更深刻的体会。不再像第一单元那样每次重构代码,是本单元最大的进步。