OO第二单元总结
本单元代码的度量分析方面依然使用基本的plantUML以及statistic等插件,在涉及到算法策略等的部分将适当引入具体代码
设计策略
第五次作业
本次作业与上一届学长学姐的作业要求相比,增加了必须使用多线程和捎带的要求。在写本次作业的时候,我第一次接触到多线程的概念,用了很长的时间来思考哪些任务适合作为一个线程来处理,以及线程之间资源的共享与通信如何协调。考虑到电梯的运作和对于乘客请求的检测为贯穿任务始终的动态过程,故将请求读入类和电梯类均设为单独的线程。两者之间资源的共享与任务协调等童工共用一个调度器(Controller)来实现,调度器中存放请求队列,也获得电梯的实时信息,通过我们设计好的调度算法来实现对电梯的规划,进而完成目标。由于对资源的分配是使用做了较多思考,本次作业中我并没有出现死锁的情况,并且用while(!condition) wait()的方式避免了暴力轮询,对于线程安全,则需要对每一个设计到可能同时读写的变量进行加锁处理,Java内置的synchronized方法给我们提供了很大方便。本次作业设计中我遇到的一个比较大的困惑是如何判断电梯线程的结束时间。最后,考虑到电梯本身运行以及请求输入两个方面,用输入线程停止+请求队列为空+电梯达到目的地且无乘客的综合条件来进行判断。只有三者同时满足才能结束电梯线程,避免中途出现的请求队列暂时为空或电梯暂时休眠状态影响我们的判断。
第六次作业
本次作业的需求变为电梯数量不确定,需要根据具体任务要求确定。基于总体要求不变的前提,我选择依旧使用hw5的架构,对于单个电梯的调度,可以在提前分配好请求队列的前提下沿袭上一次作业的算法,而对于请求队列的分配,经过对性能和整体架构的平衡,我选择了优先分配当前乘客数最少的电梯,避免出现闲等或忙碌的状态。
public void UpdateRequestSet() {
ArrayList<PersonRequest> addedPassenger = new ArrayList();
if (!queue.isEmpty()) {
for (PersonRequest tmp : queue) {
int size = 999999;
int minindex = 0;
for (int i = 0;i < elevatorNum;i++) {
int tmpsize = passenger.get(i).size() + requestSet.get(i).size();
if (tmpsize < size) {
size = tmpsize;
minindex = i;
}
}
requestSet.get(minindex).add(tmp);
addedPassenger.add(tmp);
}
queue.removeAll(addedPassenger);
addedPassenger.clear();
}
}
第七次作业
本次作业中新加入的请求看似非常多,有动态增加电梯、电梯种类对于停靠楼层、载客数量以及运行速度的调整以及由此导致的换乘问题。对三种类型的电梯进行一些观察后可以发现,我们可以将所有乘客的换乘次数控制在一次以内,那么其实我们可以简单的将本次作业中的每位乘客的请求理解成一个或两个基本的不涉及换乘的请求捆绑在一起,进而只要我们在获取请求的时候自己包装一个新的请求类即可,并且利用总运行距离最短的标准来完成换乘策略。另外对于乘客数以及运行速度的限制,只需要在构建调度器和电梯时做小小的改动就够了,在此不再赘述。本次加入电梯为动态加入,我选择的策略和第六次作业一致,即每次加入新请求就按照当前的电梯情况进行分配,不会再分配,因此可能导致新建立电梯时间较晚,请求已经分配完毕的情况。然后,在强测结束后我才发现这个问题,导致了强测所有点80分的惨案。这也提醒我每次作业一定要自己多脑补一些可能的数据,不要做到正确性就摆出一副万事大吉的态度。本次作业中,我认为比较核心的一个方法就是换乘调度方法,即电梯中乘客离开后通知调度器,由调度器查看该乘客是否有第二次进入电梯的需要。(请忽略其中的调试信息)
public synchronized void MoveRequest(MultiRequest tmp) {
MultiRequest tmp1 = tmp;
ArrayList<ElementRequest> son = tmp1.getSonRequest();
if (son.size() == 2) {
tmp1.removeFisrt();
UpdateRequestSet(tmp1.getFirstTask().getType(),tmp1);
if (button) { //调试
System.out.println(
"move to " + tmp1.getFirstTask().getType() +
" Aim to " + tmp1.getFirstTask().getTo()
);
}
}
notifyAll();
}
第七次作业详细分析
在本单元三次作业中,我一直贯彻了输入类->调度器<->电梯的架构,我认为该架构比较稳定,且具有较强的可扩展性,而对于不同任务的性能优化,可以通过完善更改调度器中的TaskDispatch方法进行,在理论课的讨论中,同学们提出了很多种扩展任务的可能性,比如考虑家庭成员同时登上电梯和下电梯以及乘客多次进出电梯等,对于这些任务,完全可以通过对乘客请求类PersonRequest进行包装来完成。对于定时结束电梯线程这个可能增加的新任务来说,我们也可以通过调度器与电梯之间进行信息交互来完成,同时要修改乘客请求的分配机制,增加二次分配,但是对于整体架构的冲击不大。因此,我认为本单元作业使用的基本框架具有较强的可扩展性。
度量分析
第五次作业
类图如下
statistic分析如下
复杂性分析如下
第六次作业
类图如下
statistic分析如下
复杂性分析如下
第七次作业
类图如下
statistic分析如下
复杂性分析如下
Bug分析
第五次作业
本次作业任务要求较低,且明确清晰,因此在强测和互测阶段均未出现bug,在此不做分析
第六次作业
本次作业中出现了输入流安全方面的大问题,我在Main类和RequestReader类中使用了两个不同的输入流,因而导致一些乘客的请求被写入Main类中用于读取电梯数量n的输入流的缓冲区,导致任务不能完成。但是令我费解的是,在中测阶段并没有反应出该bug,但是强测阶段就惨烈的挂掉了19个点,使我非常难受。当然,痛定思痛,我认为在将来的学习过程中应该多看讨论区,多和同学交流问题,以及尝试着自己多去了解一些Java内部的运作方式。
第七次作业
本次作业问题也比较大,首先是我没有考虑到新加入电梯会比较晚的事实可能性,因此沿用上一次作业的一次性分配任务方法,导致任务配置极其不均匀,极大地拉低了电梯作业的性能,另外有一个线程安全方面的问题,就是电梯在卸载乘客的时候,在执行输出信息前就将乘客信息共享至调度器,导致显示上出现先进后出的问题,但实际上逻辑没有问题。
寻找bug的策略
在寻找自己的bug以及Hack其他同学的过程中,我主要注意去寻找临界条件,比如考虑到捎带的问题,有没有可能在任务规划时没有统一标准而出现电梯方向摇摆的问题呢?为此,我设计了一个乘客在某一层请求向上,而另一个乘客在其上一层请求向下的任务,成功发现自己任务目标设置方面存在着一些问题。那么在不了解其他同学的程序架构的前提下,我采取的策略是初步明确其使用的架构的基础上,分析其判断电梯终止、任务分配等方法中的逻辑,在有可能出现矛盾的地方进行大量的覆盖性的测试。比如在最后一次作业判断电梯何时结束时,覆盖一些比较极端的情况,比如在结束输入后很久再出现换乘等等。
心得体会
在本单元的学习过程中,我认为自己的收获是相当大的,从本单元第一次理论课学到一些多线程的基本知识,到各种试错自己莽出一个有一定鲁棒性和可扩展性以及能保证基本线程安全的电梯,这样的过程中,我对于多线程相关设计模式以及线程安全等独特问题都有了更深的理解。同时也让我认识到一个好的架构对于工程实现是多么的重要。一个良好的架构可以让我们在其基础上对于不同的任务进行灵活的改进,但另一方面,我们在面临新的任务要求时,要对于之前架构的方方面面进行新的考量,避免与新的要求发生冲突。在本单元实验以及理论课上,我更是了解到了许多优秀的设计模式,在不断的探索中,我越发感觉到对OO和Java理论的学习一定不能浅尝辄止,而应往深挖掘,发现最本质的东西。