面向对象第二单元总结
第一次作业
-
Task:本次作业的任务是完成单部多线程傻瓜调度(FAFS)电梯的模拟。
-
设计策略:此次作业我一共写了5个类,其中Elevator和InputHandler是线程。调度器(Dispatcher)采用单例模式,内设一个私有变量requestArrayList,其主要功能相当于一个托盘,生产者(InputHandler)接收到一个请求,就调用Dispatcher中putRequest方法,将请求放入请求队列requestArrayList。Elevator可以通过getRequest方法从requestArrayList队列中取出一个请求并执行。StopElevator也是一个单例模式,当InputHandler监测到输入为null时,StopElevator中的变量stopFlag变为true。Elevator停止工作的条件是stopFlag变为true且requestArrayList为空。
类图如下:
- 优点:从类图和方法以及类之间的内聚情况来看,此次作业笔者的思路还是比较清晰的。各类之间分工明确,只做好自己该做的事情,有了一点面向对象编程的感觉。
InputHandler负责处理输入,Dispatcher和请求队列是一体的,Elevator负责从Dispatcher中索取请求,然后将乘客送往指定楼层。此次作业我没有将InputHandler和Elevator直接联系起来,而是在他们之间加了一层调度器,做到了输入和电梯的解耦,为第三次作业扩展电梯做了准备。
- 缺点:采用了轮询,没有使用wait()和notifyall(),导致CPU时间较长,强测某些数据CPU时间长达15.79s,资源浪费较大。
采用FAFS的调度策略,电梯利用率不高。
采用的是托盘式的生产者消费者模式,没有使用某些同学所谓的look策略,第二次和 第三次作业又都是在第一次作业的基础上直接扩展,因此降低了性能。
没有弄清sychronize的机制,直接锁方法,降低了性能。
UML图:
第二次作业
-
Task:完成单部多线程可捎带调度(ALS)电梯的模拟。
-
设计策略:大体沿用第一次作业的架构。
Dispatcher采用单例模式,但由于此次作业要求捎带,笔者理解的捎带是请求方向与主请求相同着方符合捎带条件,因此为了方便,Dispatcher中的请求队列扩展为上队列和下队列。当一个请求输入时,InputHandler调用Dispatcher的putRequest方法,将请求放入相应的请求队列。仍然采用消费者生产者模式,Dispatcher相当于一个托盘,InputHandler是生产者,往托盘里放东西,Elevator是消费者,从托盘里取东西。
由于第一次作业CPU时长较长,此次作业为了缩短CPU时间,采用了wait()和notifyall()的方式,电梯请求一个请求时,如果当前上队列和下队列中请求个数均为0,则wait()直到被一个新的输入唤醒。
对于电梯如何停止的问题,笔者的电梯中run方法里while循环的条件为调度器的上下请求队列中还有请求或者StopElevator中的停止标志尚未变为true(此处过于累赘,将在缺点中分析)。在InputHandler中,笔者将所有正确的请求以及null都加入了Dispatcher中的请求队列,其中null加入上队列。当Elevator调用Dispatcher中getUpRequest时,如果发现主请求为null,则break出while循环,结束电梯线程。
并且,由于笔者在调度器中分了上下队列,为了便于在取主请求时有个依据,因此笔者自己创建了一个类ResWithIndex用于存储请求,以上提到请求队列的ArrayList均是ResWithIndex类的。ResWithIndex的属性包含了index和personRequest。InputHandler每调用一次Dispatcher中的putRequest方法时,ResWithIndex中的index就++。因此当上下队列中均有人时,依据index的大小选出主请求。
类图:
方法和类的复杂度分析:
优点:个人感觉在设计架构上,此次作业写的还是比较清楚的,各个类各司其职,相互之间通过信息传递进行交流。Dispatche采用单例模式,简单清晰,省去了一些同学一开始将输入类设计为线程,存在bug无法解决最终重构的烦恼。此次作业对第一次代码的复用率很高,除了修改调度器中保存请求的请求队列,电梯执行指令的方式上加上捎带功能,以及改变电梯类的停止条件外,其余均没有重构。
缺点:此次作业的缺点十分明显。
UML图:
第三次作业
-
Task:完成多部多线程智能调度电梯的模拟。
-
设计策略:
由于三部电梯均有不可到达的楼层,因此对于某些请求,需要将其进行拆分.我采用在调度器中拆分请求的方式,对合并性质相同的楼层,对性质特殊的楼层用switch case的方法进行讨论。在将请求放入请求队列之前,就已分配好将用哪些电梯搭载该乘客。想复用第二次的电梯,由于有三部电梯,因此调度器中保存了6个队列,每个电梯各占一个上队列和下队列。此外,由于存在数据冒险的可能,在调度器中另设6个容器队列,用来暂时保存被拆分指令的第二部分指令,当有乘客出电梯时,检索容器队列中是否有其对应的第二部分请求。由于一级调度过于冗长,因此将switch case的各种情况放在了FloorCase类,Dispatcher和FloorCase相互配合完成对请求的拆分和分配。
虽然此次作业规定电梯有不能到达的楼层,笔者在设计之初,便把各请求分好,因此即使不对电梯做楼层是否可到达的处理,由于没有对应的请求,电梯也不会在其不可到达楼层停下来,因此,笔者在电梯中没有楼层处理部分,除了寻找请求的队列不同之外,笔者的电梯与第二次没什么不同,但在寻找主请求方面做了一点改动,如果在寻找主请求时,该层楼层存在请求,则其作为主请求。
类图:
方法和类的复杂度分析:
- 优点:
各类仅完成自己地工作,各司其职,最大程度地降低了耦合。除了对调度器做了一些改动之外,几乎没有重构,因此当许多同学还在为电梯发愁时,笔者早已完成了电梯。并且,三次电梯作业重构部分都不大,可见一开始就选中一个易于扩展的架构是多么重要(当然,这点基于不怎么考虑性能的情况,对于追求性能的大神,本菜鸡认为look算法才能最大的实现性能和架构的双赢,并且最大的体现调度器的“调度”这一功能)。
- 缺点:
在某些类的实现上过于复杂,不够简洁。笔者的第一版代码只有以及调度器,长达500+行,超出了checkstyle对于类的行数的限制,因此笔者不得不将调度器中的某些方法移至FloorCase中,电梯也过于复杂。
类图:
心得体会
更能体现面向对象的思想。类与类划分得比较明确,各方各负其责 而且第一单元只涉及一条线路,不存在线路不安全的问题,第二单元的线路安全是重要的训练地点。在升降机系列作业中,主要以假锁的方式解决线不安全的问题.