OO第二单元总结
OO第二单元总结
一、设计策略
第一次作业
需求分析
第一次作业是实现一个多线程的单部电梯调度,乘客可以向电梯给出想要到达的指定楼层,电梯上限为6人。共有三种模式,Random、Night和Morning。电梯需要在以ALS调度算法为基准的时间内将所有乘客送到指定楼层。
大致思路
第一次作业采用了上课老师推荐的生产者消费者模式。创建了一个 WaitQuene 类用来存放乘客,其设置为线程安全类。输入线程将获得的输入经处理后put到 WaitQuene ,调度器从WaitQuene中获取乘客,进行处理后对电梯做出调度。调度算法主要为look算法。具体实现过程见UML图分析即之后的内容。
UML类图

UML协作图

如UML图所示,本次作业创建了七个类。Person类用来表示乘客,该类保存了乘客的需求,并且把该类设计成了不可变类,保证了对该类操作的线程安全。Elevator类用来表示电梯,其内部实现了电梯的各种方法,包括开关门、上下行和进出乘客。PersonGroup类用来保存各层楼中的乘客请求。Scheduler类用来表示调度器,其通过读取WaitQuene中的乘客请求并根据look算法来控制电梯的行为。Input类用来读取请求并将请求保存进WaitQuene中。如下图所示。

对于该程序的框架,对我来说应该算是比较好的。在该框架下,每个类都完成其各自的功能,各司其职,为下次作业的编写留下了较好的框架,使得我后面的作业写的都比第一次作业快很多。
首先在MainClass中创建各个类,其中创建了两个线程,Scheduler和Input。在一开始的时候读入对应的模式传递给Scheduler,以让Scheduler选取对应的算法。
同步块和锁
为了保证线程安全,对于waitQuene这个共享对象,在WaitQuene类里的所有的方法都上了锁。其中为了避免轮询,对get()方法加上了wait(),对add()和close()方法加上了notifyAll(),代码如下。
public synchronized void add(Person person) {
waitQuene.add(person);
notifyAll();
}
public synchronized void close() {
isEnd = true;
notifyAll();
}
public synchronized Person get() throws InterruptedException {
while (waitQuene.isEmpty() && !isEnd) {
this.wait();
}
if (isEnd && waitQuene.isEmpty()) {
return null;
}
Person person = waitQuene.remove(0);
return person;
}
public synchronized ArrayList<Person> getList() {
return waitQuene;
}
public synchronized void remove(Person person) {
waitQuene.remove(person);
}
public synchronized boolean toEnd() {
return isEnd && waitQuene.isEmpty();
}
调度器设计
Input通过put()向waitQuene存入请求。调度器通过get()和getList()获取在waitQuene中的请求信息。
对于我采用的电梯调度算法,Random模式下为look算法,即若在电梯运行方向上有相同方向的请求且其所在楼层数为电梯运行方向上的楼层,则若电梯人数不到限制数,则可以将其捎带进来,并将电梯的目标楼层设置为当前目标楼层和新加入的乘客的目标楼层中的最远值(若电梯向上运行则为最大值,向下则为最小值),每层楼都判断一次是否有人下电梯,有就开门让乘客下电梯。当到达目标楼层后,电梯反向继续运转。
Night模式下,乘客一次性全部到达,都是从高楼到一楼。Scheduler先sleep一小段时间以保证get的时候所有乘客都已全部读入WaitQuene中。然后在所有乘客请求中选出最高的楼层作为目标楼层,让电梯直接上去,然后往下从高楼到低楼接乘客,满员了就直接下到一楼,重复以上步骤,直至送完所有人。
Morning模式下,所有乘客都是从一楼到高楼,乘客到达间隔不会超过两秒。该模式下的算法基本沿用了Random模式下的算法。对于乘客到达间隔不会超过两秒的限制,我没有很好的想法。在该模式下,电梯先从一楼接到尽可能多的乘客,然后将进电梯的所有请求中的最高楼作为目标楼层,在上升过程中有人到达请求楼层就开门放出乘客。到达目标楼层后电梯直接返回一楼,之后继续上面的操作。
Lines Counter

Metrics

从Metrics可以看到Scheduler和Elevator飘红,这可能是因为Elevator类和Scheduler类中的方法很多,分别达到了18个和16个。逻辑较为复杂,代码量较为庞大,尤其是look算法的编写,由于情况较多,加上编码能力并不是很好,写的较长。如果能把功能分离,且通过继承一些接口实现,也许能减少一些复杂度。
第二次作业
需求分析
本次作业在第一次作业的基础上,增加了两部电梯,并且可以中途加电梯,最多加到5部。需要在210s内把所有乘客送到指定楼层。
大致思路
在第一次作业的基础上,我增加了一个MainScheduler类,用来分配乘客到各个电梯的WaitQuene中。Person类改名为MyRequests,其中新添了增加电梯的请求指令。整体如下图所示。具体实现见UML图分析。

UML类图

UML协作图

如大致思路中的整体图和UML图所示,沿用了第一次作业的框架,并增加了一个MainScheduler类,整体实现中有两处地方用到了生产者消费者模型。
首先是Input和MainScheduler,Input作为生产者而MainScheduler作为消费者,WaitQuene为传递请求的媒介。
第二处是MainScheduler和Scheduler,MainScheduler作为生产者,Scheduler作为消费者,媒介仍是WaitQuene。不过每个Elevator对应的Scheduler的WaitQuene是不一样的。
MainScheduler同时作为生产者和消费者,通过从Input产生的请求送入到的WaitQuene中读取请求,并将请求经过算法分配到某个电梯对应的WaitQuene中。每个电梯各自的调度就和第一次作业一样了,只不过WaitQuene里乘客的来源不一样而已。若MainScheduler读取到的是增加电梯的请求,那么就增加相应的WaitQuene、Elevator、Scheduler等,完成新电梯的创建。
同步块和锁沿用了第一次作业只在WaitQuene编写的方式,没有做出更改,仅仅增加了加电梯的请求。
调度器设计
对于电梯各自的调度算法,基本沿用了第一次作业的设计,没有做出多大改动,仅仅稍微优化了一下,减少了一些代码量,对Elevator增加了相应的编号,修改了一下构造方法和输出内容。
对于分配的算法,Input将请求传递给主WaitQuene,MainScheduler从主WaitQuene中获取请求,并将请求分配给各自电梯的WaitQuene中。三种模式下都采用了哪个人少分给哪个的算法,当一个请求要分配时,选择所有电梯中WaitQuene 人数最少的,一样的话就按顺序分配。
Lines Counter

Metrics

可以看出,第一次作业飘红的地方还是飘红了,新增加的MainScheduler也跟着飘红了。可见我的电梯类的功能和调度类算法的实现的复杂度还是较高,不过由于害怕正确性出错不敢大改架构,只能任由它飘红了。
第三次作业
需求分析
本次作业相对于第二次作业,电梯有了限制,分为了ABC三类电梯,A类电梯可到达任意楼层,但每移动一层需要0.6s,B类电梯只能到达奇数楼层,移动一层需要0.4s,C类电梯只能到达1-3和18-20层,移动一层0.2s,剩下的条件和第二次作业相同。
大致思路
本次作业我的想法是先修改一下Elevator、MyRequests等以适应第三次作业的要求,即添加上电梯的种类参数并修改对应的和种类有关的代码,然后再修改MainScheduler中的分配策略,其它不做出修改以保证正确性。
UML类图

UML协作图

如UML图所示,本次作业的框架和第二次完全相同。仅仅是增加了电梯的种类参数和修改对应代码,并重写分配策略。
调度器设计
调度器设计和第二次作业基本相同。对于分配策略,仍然是三种模式下都是一样的分配算法。首先是判断请求是否符合C类电梯的规则,若符合则将该请求分配进对应电梯的WaitQuene中,若不符合则判断该请求是否符合B类电梯的规则,若符合则将该请求分配进对应电梯的WaitQuene中。都不是则直接分配进A类电梯的WaitQuene中。这样的分配策略显低级,而且在所有请求先于增加电梯的请求到达时,新增的电梯就没有用了。但至少正确性有了把握,即使性能很低,甚至可能超时,但至少能拿到大部分的分。(其实就是不想逃离自己的舒适区,不敢挑战,这样是很不好的,很难得到真正的提升,我反思)不过从强测结果来看分数似乎还挺高,有点奇怪
Lines Counter

Metrics

由于做出的改动很少,问题和第二次作业一样。
二、可扩展性分析
三次作业的迭代中,我的修改幅度都不是很大,各个类都各司其职,相互之间的约束基本没有,应该算是有比较好的可扩展性。
不过也有一些没做好的地方。比如在电梯类中实现了太多的功能,特别是把上下电梯的本应属于人的操作放到了电梯类里,不太符合单一功能原则。而且如果需要添加新的功能,我基本都得需要更改写好的类的代码来实现,虽然改的不多,但是却难以通过新建接口之类的方法以不修改现有代码为基础方式进行功能的增加,这不符合开闭原则。这样看来,其实我的作业可扩展性还是不够好,在之后的编程过程中,应该更加注重可拓展性,最好可以利用好继承和接口,这次作业中没有用到,可能也间接导致了可扩展性的降低。
三、Bug分析
本单元作业在第一次作业时被强测出了11个点的bug,其余均无bug。
这11个点的bug分为两类bug,其中一类只涉及一个点,即在Random判断楼层时仅仅判断了大于目标楼层和小于目标楼层,当在某种情况下等于目标楼层且没有新目标时,会陷入死循环,导致超时。
第二类bug就是一个比较粗心的错误了,Morning和Night模式的所有点都错了,原因是在判断最高楼层时,假设当前时间有7个请求,会选择最高的一层,在下一次循环时,由于电梯最多只有6个人,多出来的一个人就被我忽略了,这样这个人的请求就无法处理,从而导致了严重的bug,错了10个点,血亏。
对于hack策略,与第一单元的评测机不同,我采用的是手工构造、黑盒测试(不会写评测机)的方法,不过由于构造的数据比较随机,没有专门针对,导致一个bug都没找到。
四、心得体会
线程安全
通过本次作业,我第一次接触到了多线程编程,通过对电梯调度的设计逐渐熟悉和入门了简单的多线程编程,还是挺有收获的。多线程编程其中一个关键的地方在于如何保证线程安全。早在第一单元在将不可变类的时候就听到了线程安全这个名词,当时还不知道什么意思,现在大概清楚了。在作业中也把乘客设计成了不可变类。不可变类只能读不能写,则在多线程下无论什么样的顺序进行操作得到的都是相同的,保证了线程安全。其次是对于锁的运用,只有十分清晰自己各个线程的协作关系和流程,才能明白在哪wait,在哪nofify才能保证线程安全而且不死锁。只有仔细分析,才能确保万无一失。
层次化设计
关于层次化的设计在两个单元的作业中体现得都有所体现。在这一单元中,经历了第一单元的训练,已经明白了先设计好在写代码的重要性。首先是写画出整体的构架,通过生产者消费者模式的想法确定自己整体的编码策略。然后确定各个类及其作用和关系,确定好后在具体实现各个类的功能,最后进行类似连线的操作,按照这样的我理解的层次化设计思路来写本单元的作业,对我来说完成地还是较为顺利的。
完成本单元后,我深刻体会到了一个良好的设计对一个大规模的项目有多么重要。这对我以后编程有了更好的指导,对我编程的效率也有了很好的提升,也让自己的理解更加深刻,体会到了课程组的精心设计,感谢课程组付出的努力。

浙公网安备 33010602011771号