面向对象设计与构造第二单元总结博客
第二单元的三次编程作业结束了,这个单元的主题是多线程。以下是我对几次作业的一些总结。
三次作业的设计策略
第一次作业
我采用了scan算法,即让电梯在最底层和最顶层之间连续往返运行,在运行过程中响应处于在电梯运行方向相同(其实这次作业没有容量限制,不需要考虑是否同方向,同方向是因为考虑到之后的作业中可能会对电梯容量有限制)的各楼层上的请求。我觉得这个算法实现的逻辑较为简单,而且在性能方面也不错。
具体的实现方式是 这样的。采用生产消费者模式,将获取输入作为一个线程,将电梯作为一个线程,设计一个线程安全容器作为上述两个线程的共享变量来保存请求,同时在电梯中加一个容器保存已经进入电梯的人。输入线程每获取一个输入,就将请求放入线程安全容器,电梯按照scan算法往复运行,每到一层就判断电梯中是否有终点为该层的人或者共享容器中是否有起点为该层且同方向的人,以决定是否开门。当开门时,就把在该层等待电梯的请求从共享容器中移动到电梯内部的容器。
线程同步控制的方法就是使用线程安全容器来存放输入的请求,对容器的读写方法进行加锁。
第二次作业
第二次作业沿用了第一次的scan算法,只是在此基础上根据输入多加了电梯线程,以及进行了一些与电梯容量有关的设计。
第三次作业
第三次作业仍然适用scan算法,但是第三次作业需要额外多一个换乘的考虑。对此,我的实现方法是,如果一个请求必须要通过换乘才能完成,就先由一个电梯将该乘客送到中转站,之后乘客出电梯时再将这个请求放入共享队列来实现换乘。
第三次作业的可扩展性
从性能的角度看,我所采用的调度算法可以进行优化,如在电梯运行的方向上没有请求时,可以掉头(look算法)等。
从SOLID设计原则角度分析
SRP——单一职责原则
我的设计中除了主类外,有三种电梯类,电梯工厂类,输入类,容器类,控制类(控制何时结束电梯线程),以及一个静态类用来保存三种电梯的停靠站。
电梯类负责运行电梯接送乘客;工厂类负责建立最初的三个电梯和输入中新加入的电梯;输入类负责获取输入信息;容器类负责存放乘客信息;控制类负责控制电梯的结束;静态类中保存着一些与三类电梯停靠站的静态变量和静态方法。
我觉得各种类的职责没有重合,符合SRP原则。
OCP——开放封闭原则
在第三次作业中我为了实现换乘,修改了一些类的方法已完成新功能。如在电梯和容器的一些方法中,我增加了对乘客是不是需要换乘的判断。这些不太符合OCP原则。
LSP——替换原则
我的作业中只有三种电梯类直接继承了Thread类,没有其他的继承。
ISP--接口隔离原则
DIP——依赖倒置原则
我的设计中较为上层的类并没有依赖下层类的具体实现。符合这一原则。
基于度量分析程序结构
第一次作业
电梯的run方法中关于到达一层后是否开门的一些判断写的不够简洁,导致分支数略多。以后几次作业也有类似问题。Tray.get方法有一点冗余,是由于两个分支之间的代码是相似的,后来的作业对此进行了合并。
第二次作业
电梯的run方法的分支仍然有些复杂;此外,一些其他方法中有一些重复或 相似代码。
第三次作业
由于加入了换乘相关的代码,电梯中的循环复杂度有些提高。
自己程序的bug
第二次电梯作业中有一个强测测试点真实运行时间超时,可能的原因是线程安全出现了问题,但是这个bug极难复现,我观察代码也没有发现可能的线程安全隐患,目前还没有找到这个bug的原因。
发现别人的bug采取的策略
在前两次作业中我互测的策略就是随机生成一部分样例,交上去之后听天由命。结果效果一般,只在第一次作业中找到了别人的一个bug;在第三次作业中我发现三楼是一个比较特殊的楼层,于是在使用随机样例的基础上,我又编写了从其他所有楼层去三楼的样例以及从三楼去其他所有楼层的样例,最后一共测出了9个bug(未排除同质bug),有一个与三楼有关的测试样例测出了房间中3个人的bug。
对于线程安全问题,我在互测时没有过多考虑,当时考虑到线程安全问题的发生可能比较偶然,所以主要是考虑在调度逻辑方面去寻找他人的bug。当然也不排除我的测试样例偶然发现了别人的线程安全问题。
我觉得这次互测的策略与上一单元的主要差别在于,这一单元的输入并不是一次性的,在构造测试用例时要考虑输入的不同时间对此程序运行结果的影响;此外,由于多线程的程序有一定的偶然性,可能有些本地测试的结果与评测机测试的结果并不是完全绑定的,单个测试样例的hack结果有一定的运气成分。
心得体会
从线程安全角度来看:这单元的作业是我第一次接触多线程编程,刚开始学习多线程的时候,对于线程安全并不是理解的特别好,不清楚什么情况下需要加锁,这次作业的训练让我对于在纸面上学到的关于线程安全的理论有了更深的了解,理解了什么原子操作与非原子操作的区别,什么时候以及应该加锁时不加锁可能带来的影响。