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

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

第二单元的内容主要是应用多线程,来模拟电梯的运行,包括后面第二次、第三次作业逐渐扩展要求,以实现多部、类型不同的电梯同时运行的要求。回首第二单元进行总结~(也许平常但凡电梯运行的不好我们都会抱怨,这下子自己写一写电梯才发现原来还是很不容易的哈哈哈)

第五次作业

  • 架构分析

    • 作业要求:单部可捎带电梯,并指明Night,Morning,Random三种不同的模式。
    • 这次作业我只设计了两个线程,即负责接收输入的请求指令的 InputThread 线程,以及电梯线程 Elevator。二者有且仅有一个共享变量,即请求队列 Waitqueue。InputThread 线程负责将接受的请求指令放入 Waitqueue,而 Elevator 在运行中自己扫描 Waitqueue 并拿取符合条件的请求,这里定义了一个类 Processingqueue 来表示电梯的待捎带队列。
    • 首先明确电梯采用 ALS 策略,运行的方向由主请求决定。当某个请求的运行方向与当前方向相同,那么到它的出发楼层时,将这位乘客捎带。具体业务流程如下:
      • 采用ALS策略,电梯移动的方向由主请求决定。
      • 那么如何确定主请求呢?即Elevator 中的 setmainrequest() 方法,针对不同的模式,采用不同的确定策略,这里是电梯设计的核心,我没有将调度的部分单独分离为一个调度器,而是在每个 Elevator 中增加了一个 final 字段Schedule,这个策略类实现自一个统一的接口,根据不同模式覆写不同方法。
      • 对于Random 模式,如果乘客队列不为空,则选取最早入队请求(即队首请求)为主请求,如果乘客队列为空,则从请求队列中选取队首请求。如果二者都为空,则等待 InputThread 线程向 Waitqueue 中加入请求。
      • 对于Night和Morning 模式,分别选取出发和到达楼层最高的请求作为主请求。
      • 每走过一层,查询当前是否有可捎带乘客,这个判定是否可捎带的方式覆写 schedule 的 setprocessing()方法,可以针对不同形式采取不同判定标准,而且后续也可以扩展性的改动。
      • 电梯每运行过一层,都问问自己的乘客队列和候乘队列里有没有人上下,若有则人员上下电梯,然后继续运行,直到 输入结束,所有任务处理完毕,退出线程。
    • 在本次作业中只有一个共享变量 Waitqueue ,至于电梯何时拿取请求,拿取什么请求,拿取请求后如何处理,都交由电梯线程负责。这样做的好处是便于理清线程间的关系,而且性能分也不错;不好处是调度的灵活性上不足,可扩展性不好。
    • 本次作业采取生产者——消费者模式,InputThread相当于生产者,Waitqueue 相当于“托盘”,Elevator 相当于消费者,拿取请求的部分交由电梯线程来负责。
  • 同步块设计

    • InputThread 只写不读,Elevator 既写又读。因为只有一个共享变量,所以我将 Waitqueue 设计为一个线程安全类,在相关方法添加 synchronized 关键字,这样就避免了InputThread 和 Elevator 中出现大段的 synchronized 控制块,也有利于理清线程安全控制和业务处理的界限,不用把二者混为一谈。

第六次作业

  • 架构分析

    • 作业要求:多部可捎带电梯,相当于第一次作业中单部电梯×n。
    • 这次作业我依旧只采用了两个线程,负责输入的InputThread,多部电梯只需要构造同一个类的不同实例即可。在这次作业中依旧只有 Waitqueue 这一个共享变量,多电梯即为“一对多”的情况。每部电梯轮番获得“托盘”的权限并获取请求,其中每一部电梯的行为同第一次作业完全相同。
    • 本次作业依旧采取生产者——消费者的模式,只不过有多个消费者轮番从“托盘”中获取请求,这样做的好处是容易理清线程安全的问题,我为图方便未设计宏观的调度器,而是采取多部电梯“自由竞争”的方式,后续反思总结时,我认为这样做虽然性能分也还不错,但在调度的灵活性上非常不好,有可能会出现“不患寡而患不均”的情况,严重则会出现明显的性能不均衡的情况,具体的内容后文阐述。
  • 同步块设计

    • 在InputThread 线程与多部电梯间仅有一个共享变量 Waitqueue,但每一部电梯调用 setprocessing()方法,从Waitqueue 中批量拿取新请求时,仅将Waitqueue 对应方法设为synchronized 还不够,因此在电梯setprocessing()方法中,每次需要遍历整个Waitqueue 寻找符合捎带的请求时,需要用 synchronized 控制这一部分的代码。

第七次作业

  • 架构分析

    • 作业要求:多部电梯型号不同,分A、B、C三类,即它们的可达楼层、容量、速度参数等不同,模式还是三种之一:Night,Morning,Random。
    • 电梯的工作方式我还是沿用五、六次的方法,这里不再重复阐述,主要谈谈有关换乘的处理:
      • 换乘的主要目的在于A、B、C三种电梯负责的楼层有所不同,A型电梯可到达全部楼层,B型电梯只到达奇数层,C型电梯可以到达1-3和18-20,如果不做换乘处理,仅根据电梯型号来分配对口的请求,那么很可能出现A型电梯任务过重,而B、C电梯空等的情况,这说明对于部分请求,不能仅仅将它分配至某一个电梯“一梯到底”,可以将最初一个完整的请求拆分成几部分,让B和C帮A承担部分任务。
      • 这里如果按照生产者——消费者模式来划分的话,原本InputThread 是生产者,不断产生新请求,Elevator 是消费者,不断处理请求,如果现在某个请求分为两段运输,第一段的电梯运输结束后,第二部分的请求需要回到请求队列,这样的话电梯这个“消费者”也会产生请求,变成一个“生产者”,随之而来的线程交互就会很复杂。
      • 这个问题最开始困扰我很久,不知如何解决,后来我采取的方式是在请求到来加入电梯之前,就已经分析出它该不该被拆分,应该被拆分成哪几段,然后将第一段请求给对应电梯。这里我新增了一个共享变量 Recordmenu,专门负责拆分的请求的投放问题。当第一段请求已经结束,就将第二段请求投入,这说明我最终采取的方式,不是令电梯运输过程中产生新请求,而是以分段投入的方式,将一个请求拆分成两个请求,以模拟换乘的行为。
    • 这样的好处很明显,就是线程间的交互不用很复杂,为自己理清思路提供了很大的方便,后续我思考时认为自己第三次作业设计的不好,因为InputThread 线程承担了一部分调度分配的任务,这其实并不符合面向对象的设计原则,将这部分宏观分配的任务单独分离出一个 Dispatcher 是更合理的架构设计,在完成作业的过程中我选择了比较稳妥的方式,但这样并不是很符合设计原则的架构。
  • 同步块设计

    • 第三次作业中InputThread 线程和Elevator 线程间有两个共享变量,一个是请求队列 Waitqueue,还有一个是拆分请求的记录表 Recordmenu,这个记录表用于记录拆分请求处理的进度,当电梯将第一段请求完成之后,在Recordmenu 中将对应记录置1,此时InputThread 就将第二段请求投入,最终模拟了换乘行为。在扫描和改动Recordmenu时加synchronized 控制代码块。
    • 结合第六次作业的bug,采用层次化的请求队列,即InputThread 将请求放入第一层Waitqueue,在根据类型分配到三种电梯各自的 Waitqueue中,为保证性能,不同型号的电梯的Waitqueue 并列,同一型号电梯共享一个 Waitqueue 自由竞争。
  • 扩展性分析

    • 在不同模式的选择上,电梯保留更换策略类的空间,可以灵活地添加其他模式。
    • 在换乘策略上,我本次作业只应用了A和B的换乘,即只由B替A分担任务,后续如果要扩展更多的换乘策略,也可以按不同情况来拆分请求,Recordmenu 也记录了每个第二段请求该去到哪个电梯里,在换乘上具有一定的可扩展性。
    • 如前文所述,在第三次作业中,我的架构设计中最终只有两个线程,其实是InputThread 线程承担了一部分二次分配的任务,这其实并不是很好的设计,但当时迫于ddl 也只能选取了一种稳妥的方式。后来反思时,我认为应当将这一部分内容,单独分离成一个Dispatcher线程,随时关注等待队列Waitqueue 以及 记录表 Recordmenu,这样在调度的灵活性上将大大增加,也不必使InputThread 过于臃肿。
  • UML类图和协作图

BUG 分析

  • 自己的bug

    • 第五次作业强测互测均未出现bug,但在中测时Night一直rtle,后来发现是 setmainrequest() 中设置的如果请求队列里还有满足捎带的,就一直等待,但是此时请求队列如果为空,电梯也会一直等待,这样会造成电梯在某一层停住不动了。
    • 第六次作业强测出现一个bug,是rlte的问题,原因在于我最开始采取的是自由竞争的方式,运行时没法控制一个请求由哪一步电梯抢到,在一定情况下会出现某一部电梯成为“恶霸”抢走所有请求,其他电梯空等的行为,即分配不均的问题。解决的方式是采用层次化的请求队列,即最开始InputThread 只将到来请求放在第一层Waitqueue 中,然后按顺序将请求分发在各自电梯的二层Waitqueue中,这样就保证了每一部电梯接近均衡分配的问题。
    • 第七次作业强测互测均未出现bug。
  • 他人bug

    • 因为没有搭建测评机,所以我采用的是手动构造样例的方式。这里阐述一下我的测试思路:
      • 首先是基本功能测试(当然能进互测的话,这部分课程组应该已经检验过了)。单独构造针对A、B、C的样例,再混杂构造。
      • 然后从1-20,再从20-1,检验在长距离运行过程中会不会出错。
      • 运行途中,手动增加多个可捎带请求,检测电梯会不会超载。
      • 频繁在高层移动,或者是频繁在底层移动,检测高低层是否出错。
      • 反复在某两层之间移动,检测是否出错。

心得体会

  • 线程安全

    • 首先理解多线程花费了我很多功夫,比起第一单元正则表达式,多线程涉及到运行过程的实际问题,所以刚开始总不明白多线程究竟是怎样工作的,后来在分析共享变量时,我不得不手画一些流程图,利用简单易懂的方式,哪一个线程读写,读写哪个,读写完然后哪一个线程读写,这样简单粗暴的方式可以网络整个代码运作流程,方便自己理解。
    • 其次是设计线程安全类是一种不错的方法,这样可以避免在线程的流程代码中出现大片的 synchronized 代码块,这样东一块西一块很容易出现错误。但线程安全类的设计在有关方法加synchronized 的选择上刚开始疏于考虑,在第六次作业时盲目的为所有Waitqueue 方法加上 synchronized 结果出现了同一个人进出多次的问题,后来发现在第六次作业中多个电梯间也共享请求队列,疏漏了这一点。
    • 第三是错误的不可复现性,我的体会是采用朴实的办法,有错一定要先摘录下来,分析错误特征,是人进出电梯时机错误?还是电梯无法运?或是不正常的开关门,先记下来也很方便后面重复回炉分析,应对错误一闪而过不可复现令人苦恼的问题。
  • 层次化设计

    • 在第六次和第七次作业后续完善中,我采取了多梯运行——两层请求队列,先设定总的请求队列,InputThread 只需要每次不断填充增加就可以了,在将请求按类型分配到第二层的 Waitqueue 中,这样减少了线程间的交互,也增强了可扩展性。

这一单元我认为比第一单元明显要难,而且在编写过程中遇到了很多挫折,所幸最后自己还是坚持完成了三次作业。除此以外,我还有一个很大的感受就是要敢于去尝试更合理的架构设计,我在这一单元作业的编写过程中,选择了迭代改动较小,沿用自己之前设计的方式来通过,尽管最后获得分数还不错,但我认为自己不应该抱有多余的担忧,比如担心采用新的架构最终debug失败无法完成作业怎么办?学习oo应该更多的学习面向对象的思维,学习更好地设计架构的思维和能力,这样才能更好地掌握面向对象的思维能力,期待自己在后面的学习过程中再接再厉!

posted @ 2021-04-25 16:11  Wuhaotian  阅读(63)  评论(1编辑  收藏  举报