2019面向对象课程序设计第二单元总结
前言
本次作业开始采用了多线程的设计模式。在之前的编程学习中,我们所编写的程序都是单线程的,不需要考虑线程安全性的问题,而在这个单元,由于调度器和电梯需要同时运行并进行交互,即调度器需要给电梯派发请求,或者电梯从调度器中获取请求,我们就需要构建多个线程来实现我们的需求。因此,我们就需要考虑线程安全和线程交互问题。例如,我的设计是调度器给电梯请求还是电梯从调度器中获得请求?当暂时没有请求,而两个线程又不能退出,如果不采用轮询的方法,我该怎么办?这里不仅有着线程安全的问题,还有着一个新的概念——锁,为了保证每一个对象的同步方法或同步语句块只有一个线程在访问,就需要锁来进行维护。对于线程安全和锁这个概念及其应用,还需要阅读更多的资料去学习。
1.需求分析
本次作业的总体目标是构建一个或多个载人电梯,电梯应当有合理的运行逻辑:必须开门才能上下人、关门以后不能上下人、必须严格按照乘客的需求上下人、不能跳层、不能超载,只能在指定楼层停靠等。并且能在给定的时间内完成所有合法的乘客请求,即把所有乘客从指定起始楼层带往终点楼层。
2.作业设计策略的分析与总结和程序结构分析
这次作业的复杂度分析这部分我依然采用了IDEA的Metrics插件的Complexity mertics,对于这个插件有一些术语在这里需要解释一下:
-
ev(G): 一个方法或者类的结构化复杂度,值越高复杂度越高。
-
iv(G): 一个方法及其调用的其他方法的紧密程度,值越高则紧密程度越高。
-
v(G): 一个方法或类的循环复杂度,值越高则循环复杂度越高。在类中,有OCavg和WMC两个指标,分别代表类的方法的平均循环复杂度和总循环复杂度。
由于本单元中我没有用到继承和接口,因此在solid分析中将不考虑LSP,ISP,DIP这三个分项。
2.1 第五次作业(FAFS电梯)
2.1.1 设计策略
第一次作业比较简单,因为只是采取了FIFO策略,一次只带一名乘客,因此只需要设计一个调度器,调度器负责维护一个队列来保存乘客的请求,并且由于完全按照FIFO的策略,所以我们可以用java.concurrent类中的LinkedBlockingQueue,其中,调度器其实是一个静态类,但可以优化成单例模式。
private static LinkedBlockingQueue<Object> queue
= new LinkedBlockingQueue<>(31);
再设计一个电梯,负责从调度器中取用请求,并将乘客带往目的地。
在没有请求但是输入没有结束的时候,LinkedBlockingQueue的take方法自带阻塞,在请求输入结束以后,我会给队列中加上一个String对象,当电梯接收到这个String对象的时候,就会自动退出线程,运行结束:
public static PersonRequest requestService() {
try {
Object m = queue.take();
if (m instanceof String) {
return null;
} else {
return (PersonRequest) m;
}
} catch (InterruptedException e) {
return null;
}
}
2.1.2 复杂度分析(class and method)
类图:
Main类中只有一个程序的入口点,负责接收输入并把它添加到队列中。
RequestQueue类中只有两个方法,一个负责添加请求,另一个负责给电梯请求。
Elevator类中有几个特征方法,开门,关门,以及前往楼层,人的进出等。
复杂度分析:
类复杂度
方法复杂度
可以看出,本次作业的结构复杂度、循环负责度和依赖度都比较低,也许是需求比较简单的缘故。
优缺点分析:
个人觉得这次作业的设计还是比较好的,因为每个方法的各项复杂度都很低,并且每个类职能很专一,都只做自己该做的事情,比较好地体现了oo的思想。
2.1.3 时序图分析
Main负责给queue请求,而queue和elevator交互,电梯去拿请求,调度器则负责发送终止信号。
2.1.4 soild法则分析
SRP方面,这次作业还是实现的比较好的,每个类职责单一明确。
OCP方面,也实现的比较好,因为在下一次作业里只需修改Scheduler的内容和Elevator的内容而无需重写。
2.2 第六次作业(ALS电梯)
2.2.1 设计策略
第六次作业其实也相当于第五次作业的优化版,有点不同的是,在处理捎带请求的时候可能需要遍历队列,因此我不再采用LinkedBlockingQueue,而是自己通过Arraylist和同步语句块来实现线程安全。并且采用了单例模式,确保队列只有一个并确保线程安全:
private static Scheduler instance = new Scheduler();
private ArrayList<Object> requestQueue = new ArrayList<>();
private Scheduler() {
}
public static Scheduler getInstance() {
return instance;
}
在调度器里实现一个捎带方法,即通过特定的条件来判断队列里是否有符合条件的捎带请求,如果有的话则加入到电梯的服务队列中,并且根据到达楼层进行排序。
private void arriving(final PersonRequest m) {
int way;
if (nowFloor < m.getFromFloor()) {//设置方向
way = 0;
} else {
way = 1;
}
while (nowFloor != m.getFromFloor()) {
upOrDown(way);
}
if (m.getFromFloor() < m.getToFloor()) {
way = 0;
} else {
way = 1;
}//先到达起始楼层
while (service.size() != 0) {//每到达一层即判断有无捎带请求
service.addAll(Scheduler.getInstance().hasSameRequest(
service.get(0), nowFloor));//添加捎带请求
checkService();//排序
if (needOpen() && !doorStatus) {
openDoor();
service.addAll(Scheduler.getInstance().hasSameRequest(
service.get(0), nowFloor));
checkService();
}
if (doorStatus) {
letPersonIn();
closeDoor();
}
upOrDown(way);//上下楼
if (needOpen()) {
openDoor();
letPersonOut();
}
}
closeDoor();
}
当service队列为空时,即主请求也服务完毕,则再从调度器队列中取下一个请求。直到调度器队列也为空。
2.2.2 复杂度分析
类图:
其中,Main方法还是只是负责接收输入,把输入给调度器。
调度器是单例的,负责检查是否有捎带请求和给电梯请求。
电梯依然是基本功能,只是在此基础上添加了服务列表,以及checkService(排序函数)
复杂度分析:
类复杂度
方法复杂度
优缺点分析:
可以看出,本次作业的设计各项复杂度也较低,因为只是在第一个电梯的基础上做了一些优化而已。同样每个类的职能也很专一,只负责自己改负责的部分。但其实为了更抽象一些,可以把Main的输入请求作为一个线程放到调度器里面,这样更能体现oo的思想。
2.2.3 时序图分析
和第一次作业类似。
2.2.4 solid法则分析
SRP方面,由于和第一次作业类似,所以实现较好。
OCP方面,由于第三次作业不太适合电梯去调度器拿请求,所以调度器就需要重构了,改为调度器给电梯发送请求,没有实现扩充实现新功能。
2.3 第七次作业
2.3.1设计策略
第三次作业和前两次作业相比变得更复杂了,主要原因是有3部电梯,并且每部电梯都限制载客量,都有自己的可停靠楼层,这就需要乘客自行换乘才能实现,由于三个电梯只是属性不同,所以我依旧只有三个类:调度器、电梯、Main类,这次,Main类只负责启动线程,而把输入服务交给了调度器实现,调度器每接收到一个请求,就根据请求的样例来分析发送给哪个电梯。如果发现这个请求无法由一个电梯完成,则将其拆成两个请求,发送至可以完成第一个请求的电梯的服务队列和转发队列中,当电梯到达转发队列的起始楼层并且乘客已经下电梯后,就把转发请求给与对应的电梯。
每个电梯的四个队列,调度器不再维护总队列,只负责分发请求,队列由电梯自己维护:
private ArrayList<PersonRequest> transfer = new ArrayList<>();
private ArrayList<PersonRequest> service = new ArrayList<>();
private ArrayList<PersonRequest> queue = new ArrayList<>();
private ArrayList<Integer> out = new ArrayList<>(); //who is already out.
为了让电梯间有交互,并且让调度器知道其他电梯的信息,调度器拥有三个电梯对象,这样电梯间的交互就可以借助调度器来完成了。
private static Scheduler instance = new Scheduler();
private Elevator elevatorA;
private Elevator elevatorB;
private Elevator elevatorC;
其他设计方法和第一第二次作业类似。
2.3.2 复杂度分析
类图
Main:负责启动线程。
TimableOutput:自行设计的线程安全输出方法。
Scheduler:负责保存电梯信息和初始化电梯,提供静态基本方法,主要方法为addRequest,其他方法基本上都是为了实现这个方法的方法。
Elevator: 基本功能和上次类似,只是给了其他电梯添加转发需求的权力,因此有了addTransferService等方法transferPerson为检查是否有转发需求的方法。
复杂度分析
类复杂度
方法复杂度
由于需求比较复杂,因此可以看到循环复杂度和依赖度都比较高,主要原因是需要多次遍历队列以及需要其他线程的信息。由于hashmap线程不安全,所以我还是采用了Arraylist,并自己添加同步方法
优缺点分析:算法性能较差,循环复杂度高,但是每个类依然保持自己处理自己的事情,不过常用的静态方法可以再新增一个类,而不用写到调度器里面,个人觉得这次作业还是符合oo思想的。
2.3.3 时序图分析
main类负责启动线程,调度器负责发送请求,电梯之间发送转发请求进行交互。
2.3.4 solid法则分析
SRP方面,每个类依然有着自己明确的职责,实现的较好。
OCP方面,也实现的较好,如果电梯类型不同只需修改构造器,调度器也无需重新构造。如果有新需求,只需增加,无需重写。
3.自己程序的bug
本次作业中尽管我的设计已经分工明确,在前两次的强测和互测中没有被发现bug,但是在第三次作业中,因为一点粗心,即在发送转发请求的过程中没有考虑乘客是否已经下了电梯,错了很多点,也被hack了很多下,但是还是在一次合并修复内把所有bug修完了。只需要在电梯类中增加一个out队列,记录下已经下电梯的人,然后在转发之前先找到队列中是否有这个人再将其转发即可,一个很微小的错误,而且也是逻辑上的错误,还是自己没有测试充分、设计时没有考虑清楚导致。
4.发现别人bug采取的策略
编写自动化测试程序来对别人的程序进行测试,如果发现错误则进行bug定位,找到其出错的位置,再考虑逻辑上还有没有其他的bug,由于第七次作业我被分到c组,因此有效性较好,能根据错误快速定位bug所在位置,再根据其设计结构构造相似的样例。
在线程安全方面,主要测试是否有“电梯提前下班”和“接收不到输入“的问题,只需构造相应的测试样例即可。
在第一单元的测试中,我主要是通过找其输入处理和输出处理部分的bug,也就是字符串处理问题的bug,输出量小,较为简单,而本单元的测试输出量大,就算有了输出,自己肉眼看也很难看出输出是否正确,因为输出通常有几百行,所以不得不编写测试程序来测试,辅助找bug。
5.心得体会
5.1 线程安全
由于多线程中采用的是并发执行的方式,所以可能会导致线程不安全,本单元电梯系列有点像生产者-消费者模式。因此如何构建线程安全的程序呢?可以考虑以下几点:
- 对于不可变属性,用final关键字修饰。
- 对于严格只允许一个进程访问的对象,使用单例模式。
- 保证操作的原子性,使用Atomic类的变量。
- 对于要处理临界资源的方法或代码块,用同步语句修饰。
- 要弄清楚wait() notify()方法应该放置于什么位置。
5.2 设计原则
从oo的角度来说,设计还是要符合oo的思想,每个类都严格地只管自己该管的事情,方法也是如此。同时,要灵活运用java的内置类,里面的好多数据结构一定是优于我们自己写的程序的。比如这次就可以使用LinkedBlockingQueue,还有Lock与ReentrantLock的使用,比同步语句块更灵活。ReentrantLock+Condition的组合可以实现唤醒特定的线程。其次,在设计的时候就要考虑尽可能多的方面,不然就会和我一样强测拿到一个很低的分数。最重要的还是要秉承一类一组功能的原则,这样在设计架构的时候可以保证很好的正确性。
5.3反思与总结
本单元的作业自我评价不是很满意,尤其是第三次作业的致命bug,使我的强测拿了很低的分数,以后在写完程序后不仅要进行逻辑性的检查,还需要多加测试。尽管oo进度已经过半,但是还要不断努力,继续加油。