2022OO第二单元总结
----------------------------

前言

  • 第一次作业由于出现了奇怪的bug而在bug修复阶段进行了小范围重构,下文将该重构后的代码作为第一次作业的代码。
  • 三次作业的架构基本上没有变化,只是进行了一些策略上的更换,即更改了实现类,这是因为第一次作业的时候我就考虑到未来可能要有换乘的可能性,同时电梯的参数,如满载人数,运行速度等我也没有写死,而是通过构造函数决定,因而只需要改变工厂实现类即可完成适配。当然,也有一点改变在于将原来的Elevator类改成了抽象类,然后用NormalElevatopr和RoundElevator类继承,实际使用这两个类来区分横向和纵向电梯,但原有的Elevator接口保持不变,原有的调用Elevator的方法也不会产生问题。
  • 基于上一条,后续的章节不再特意区分三次作业,当然,会采用增量式的方式指出三次作业的变化(如果存在)。

架构分析

  本单元作业架构采用了一种流水线式的架构,总体可分为4部分,输入处理、总控器、分派器、电梯。输入程序只需要不断读取输入信息并将信息加入到总控器的请求队列中即可。总控器是一个singleton,一部分用于静态规划换乘路线并发送第一段请求或者发送增设电梯的请求,另一部分接收电梯的完成信号,发送下一段请求。(即一个乘客的请求会被分成至多三段,一段请求只能纵向或者横向移动)总控程序将请求发送到相应楼或者层的分派器中。分派器是一座楼(纵向)或者一层(横向)占有一个。分派器将请求分给其所在座(层)的某个电梯。输入处理在接受到结束信号即读取到null后会向总控器发送终止信号并结束自身线程。总控器再向分派器发送终止信号,以此类推,直到所有线程都退出。总控器、分派器、电梯都采用行为和策略分离的模式,其中电梯还将行为在此分离出去,自身只保留一些基本的方法,如更改所在楼层,载有的人数的方法,以及电梯的顶层行为(直接在run方法中实现了),这些方法由电梯行为类调用。具体细节见UML类图下的说明。

同步块与锁的选择

  作业架构中需要加锁的部分就是请求队列,本质上就是生产者消费者模型。总控器要维护两种队列,即请求队列和剩余路线的队列分别用于完成前述两部分的任务,因而至少需要两把锁,其他类一把锁就够了。因而总控器选择了可重入锁,而其他的类一律采用了sychronized代码块。这样代码块中全都是只完成一件事:向下一个阶段的队列中加入请求或者接受来自前一阶段的请求并将其从队列中删除。

调度器设计

  由于我的架构和公共请求队列的设计不同,调度器的设计也相对简单,无非是选择下一阶段的某个实例。总控器我采用了基准策略,因为最简单。而分派器采用了负载均衡的策略,环形分派,这点和基准策略类似,但是针对不同速度的电梯分配的数量不同,即速度越快的电梯分配的请求占比越高。至于调度器与程序中的现场的交互问题,我不清楚在我的架构中是什么含义,至少我的架构的线程间通信是很自然的。

UML类图及协作图

类图

  第一次作业中将类的字段和方法展示出来,主要用来分析各个类都在做什么,后两次作业则注重整体的关系,各个类基本没什么改变,增加的类也只是多加某个接口的实现类,特殊的类会额外说明。因而不再展示字段和方法。整体架构相关的前文已经给出,在此不再赘述,只关注实现的细节。所有的类图也都经过了整理,使得逻辑关系相近的类放在了一起。

第一次作业

  首先InputHandle和MainClass很简单,不必多说。其次是总控制器,它的任务之前已经说过了。另外值得一提的是第一次作业我为很多线程保留了join方法,这是受C++的影响,后续作业就将它们删掉了。SinglePersonSchedule类本质上就是包装了一下Arraylist用于存放一个乘客请求对应的拆分后的请求。IsinPerScheStrategy是上述拆分方法策略的接口,即将拆分策略单独分离出来了。而其实现类CommonStrategy并没有完成任何实质性的动作,因为第一次作业没有涉及到换乘。之后是分派器,可以看出也将其分派策略分离了出来,CycleDispatch就是一个简单的环形分派策略,其在第一次作业就被设计了出来。最后来到重点,电梯相关的类。电梯类本身知涉及关于其属性的getter和setter方法(有的方法不叫getXXXX或者setXXXX但本质不变,比如load方法本质上就是让PersonNum++)。IBehavior接口规定了电梯具体的行为,电梯的setter类方法也是由它的实现类(们)调用。具体实现类有上乘客(In),下乘客(Out),上下行(Move),开关们(Wait),空行为(Null),另外,打印输出信息也是有这些类来做。然后是IEleStrategy接口分离了电梯的调度策略,由其updateRoute方法来更新电梯行为,即IBehavior实现类实例的队列,也就是Elevator类中的behaviors字段。其余的方法则是IBehavior实现类间接调用的,用来标记乘客是否上到电梯上,是否到达目的地等信息。Look策略中的两个内部类中TaggedRequest是对PersonRequest的包装,对该请求增加一些信息,而RequestFloor则是包装了一个按照楼层排列的请求集合,这两者都是为了方便Look策略使用减少时间和代码复杂度的。最后是Elevator的工厂类,很容易理解,不再赘述。

第二次作业

  首先第二次作业引入了VoidRef等三个类,这是由于Java没有基本类型的指针,本质上就是包装了一个基本数据类型(Java的包装类达不到我想要的效果),与整体关系不大,可以先忽略。第二次作业的主要变化是将原来的Elevator类变成了抽象类,转而实例化NormalElevator和RoundElevator两个类。INOrR接口是为了时纵向和横向电梯复用代码而引入的接口,即将两者不同的地方单独抽出成几个方法,放在INOrR的实现类中。引入AbstractStrategy本质上也是为了使各个不同的策略实现类复用代码和变量,免得大量的变量通过参数传递。RequestPosition类单独从原来的内部类摘出来的原因和INOrR类似,加入了横向电梯后某些方法需要“定制”,而大部分方法不变,因而派生出很多子类,将这些类都作为内部类会导致外部类太过庞大,因而将其拿出来。此外增设了circle和SSTF策略。最后GenneralController类增加的DispatcherWrapper类是将原有的Dispatcher Arraylist包装起来,使得总控制器不用区分横向和纵向的分派器,而交由wrapper处理。

第三次作业

  第二次到第三次作业的变化就很少了,就只是增加了GeneralController和Dispatcher的策略的实现类,WeightDispatch是个未完成品,所以就不用关注了。新增加的SimpleTransferStrategy类采用了基准策略静态划分换乘路线,BalanceCycleDispatch类是在环形分派的基础上考虑了电梯的运行速度,速度快的电梯在一次环形分派中分得的请求更多。

协作图

三次作业的协作图都如下图所式

  图分别给出了输入请求,增加电梯和结束程序三种情况的流程。注意Elevator通知GeneralControllerPart2线程时实际实现要另开启新线程进行通知动作,否则会造成死锁。

bug分析

  第一次作业出现了死锁,这里特指bug修复前的版本。出现死锁的原因在于我为了模拟电梯运行和开门关门的时间并没有让电梯sleep,这是为了防止在关门时来请求无法应答的情况,因而设置了一个定时器,在需要模拟等待时间的时候设置计时器,计时器到时间后再反过来通知电梯。但是由于上述关门期间应答请求的情况,电梯可能需要在计时器计时过程中将其复位。而bug就出现在以为计时器复位了,实则没有,于是在再次设置的时候造成了死锁,此时计时器sleep的同时持有自己的锁,电梯也持有自己的锁,但是设置计时器需要拿计时器的锁,而计时器没有被复位,一定时间后会通知电梯,而通知电梯需要那电梯的锁。如是,便造成了死锁。由于当时不知道如何强制复位计时器线程,而进行了重构,重构后计时器不再存在,对运行时间的模拟改为让电梯执行带有timeout的wait方法。
  第三次作业纯纯的就是逻辑出现问题,分派器接受总控制器的请求分给不同的电梯,但默认来自总控制器的请求是可以满足的,总控制器对于某层是否可以做到换乘的判断逻辑出现的问题,从而导致出现了RTLE和CTLE。该逻辑错误在于某层是否可以横行换乘时没有区分电梯,举个例子,比如在2层有两台电梯6,7。其中6号可以在A、D停靠,7可以在B、C停靠,那么从C到D的请求应该是不能满足的,但一开始错误的逻辑没有区分电梯,而是认为A、B、C、D都可以停靠,从而认为2楼可以换乘,分给了分派器不能满足的请求。但是由于中测数据太弱,自己又没测,所以放任了这种低级bug的出现。修复办法很简单,重写中控器的判断逻辑即可。

hack策略

  针对互测,因为没有时间,所以只在第一次作业的时候交了一次数据,是随机生成的数据。
  针对自身程序测试,第一次作业的时候写了一个测评机,但是只能进行输出逻辑上的测试。后续就没有进行测试,因为需要权衡OO和OS的时间。

心得体会

  OO多线程是在OS前的,这不太合理,尤其是在不够清楚多线程的情况下写程序是非常不便的。线程安全问题不大,记得在访问共享资源前加锁就可以,但是无脑加锁很可能导致死锁,同时也会大幅降低效率。不过针对OO作业,基本的加解锁操作就可以应付,但是想要写的顺畅还要多了解一些底层机制,Java提供的方法不够灵活,很多独特的需求可以用更底层的PV操作来完成。而事实上OS学到信号量的时候第二单元已经快结束了,更甚者,OS课中对于死锁的理论分析至今都没有上。至于层次化设计我并没感到第二单元有什么层次,不如说是“职责划分”,各个类基本上没有细分的必要,即各个类都够“原子”了,整个程序更像是用这些类拼出来的。我的设计就是一个流水线式的结构,并且把策略与行为分离,这样各个类都是单一职责的。
  最后我想用一句话总结我对前两个单元的感想:讲的太少,做的太多。