2020面向对象设计与构造 第二单元 博客总结

面向对象设计与构造 第二单元 总结

一、程序结构分析

1. 第五次作业:单部ALS电梯调度

(1)类图与时序图

(2)设计策略

初次接触Java多线程,在编程的过程中,不仅需要考虑功能正确性,还需要额外考虑线程安全性,两者需要兼顾才可以保证程序能够运行得到预期结果。

① 重要类介绍
  • Input:处理输入并存放进请求队列,读取到null结束。
  • RequestQueue:继承自阻塞队列LinkedBlockingQueue,暂时存放从标准输入获取的请求,等待后续线程取走请求。
  • Controller:调度器,负责从RequestQueue中取走请求,并将请求委派给电梯的进入队列。
  • EnterQueue:电梯的进入队列,按照楼层存放未进入电梯的乘客。
  • Elevator:电梯,响应进入队列与离开队列的请求。
  • ExitQueue:电梯的离开队列,按照电梯内乘客的目标楼层存放乘客。
② 线程设计

从类图与时序图可以看出,笔者采用的是生产者与消费者的设计模式,本质更像是流水线,MainClass线程负责启动其他线程,Input线程负责获取输入,每获取到请求后,通知Controller线程取走请求,然后Controller线程再将请求安排到Elevator线程的进入队列EnterQueue中。

形象化表述便是:Input -> RequestQueue <- Controller -> EnterQueue <- Elevator RequestQueueEnterQueue为共享对象,其内部方法需要上锁。

③ 电梯运行算法

由于第五次作业只有一部电梯,笔者经过网上调研,了解了一些算法,最后采用改良版look算法实现电梯。

  • 电梯闲置:
    • EnterQueue非空,电梯响应同方向最近的乘客请求,如同方向没有请求,则反向。
    • EnterQueue为空,电梯静止,等待唤醒。
  • 电梯处于运行状态:
    • 同方向存在楼层的ExitQueue非空或EnterQueue(包含当前楼层同向请求)非空,电梯继续向同方向运行。
    • 同方向所有楼层的ExitQueue为空且EnterQueue(包含当前楼层同向请求)为空,电梯调转方向。
    • 电梯到达某一层:
      • 该层的ExitQueue非空,电梯开门。
      • 该层的ExitQueue为空,但是EnterQueue存在发出同方向请求的乘客,电梯开门。
      • 该层的EXitQueue为空,且EnterQueue不存在发出同方向请求的乘客,电梯不停靠,继续移动。
  • 输入结束:Input线程通知Controller结束,Controller线程通知Elevator结束,流水传递结束信号。
  • 简而言之,电梯在一次开门中就处理完毕当前楼层所有事务,不管当前楼层要上电梯的乘客移动方向是否与电梯方向一致,发现同方向没有进入和离开的请求时,电梯再调头。

(3)量化分析

本次架构设计的复杂度还是非常理想的,只有电梯类判断调转方向的look()方法复杂度过高,因为其中包含过多条件逻辑。

method ev(G) iv(G) v(G)
Elevator.look() 8 9 12
Total 66 87 98
Average 1.57 2.07 2.33
class OCavg WMC
Controller 2.0 4.0
Elevator 2.25 27.0
ElevatorQueue 1.25 10.0
EnterQueue 1.86 13.0
ExitQueue 1.67 10.0
Input 2.0 4.0
MainClass 1.0 1.0
RequestQueue 1.75 7.0
Total 76.0
Average 1.80 9.5

2. 第六次作业:多部智能电梯调度

(1)类图与时序图

(2)设计策略

本次作业在上一次的基础上,增加了多部电梯与载客量,这也意味着需要考虑新增的请求分配给哪一部电梯的问题。由于上一次架构的可扩展性很强,因此完成本次作业的大部分时间都用在了对于性能优化的思考上。

①新增类介绍
  • ElevatorSimulator:电梯模拟器,用于模拟某一部电梯完成所有请求所需要的时间,需要对电梯当前的状态进行克隆,因此实现了电梯队列的克隆方法。
②线程设计

这一次的线程设计与上一次完全一样,但是发现了上一次设计中的一个线程安全Bug,将在程序Bug分析部分说明。

③电梯运行算法
  • 2-5部电梯:沿用了上一次的改进look算法,由于电梯满载时,同方向的EnterQueue请求即使到达相应楼层也不能进行响应,因此满载后增加了方向的判断。
    • 电梯满载,且同方向存在某楼层ExitQueue非空,不改变方向。
    • 电梯满载,且同方向所有楼层ExitQueue为空,此时再向同方向运行已没有意义,调转方向。
  • 1部电梯:由于加入载客量的限制,当一部电梯请求很多时,开门响应反方向乘客请求显然会浪费电梯内部空间,因此使用纯look算法来运行,每次开门只接待同方向乘客。
④请求分配算法

增加电梯数目,意味着新增的请求可以有多种选择,笔者思考了一段时间,最终选择贪心模拟方法来实现优化,一旦乘客被分配出去,便不会再改变要乘坐的电梯。

贪心模拟的流程:

  • 对每个电梯当前的状态进行克隆,生成电梯模拟器ElevatorSimulator。模拟器继承自Elevator,取消了电梯运行睡眠的时间。
  • 将新增请求添加到模拟器的EnterQueue中,开始模拟电梯运行。
  • 返回该部电梯运行所需时间。
  • 选择时间最短的电梯,安排该请求到其EnterQueue

该算法较为复杂, 涉及到对线程类的操作,且贪心类算法很容易忽略掉最优解,克隆的电梯状态可能与请求到来时的状态存在误差,影响分配结果。

(3)量化分析

由于增加了更多的条件判断逻辑,复杂度有了明显升高,体现在Elevator类的look()see()等电梯方向判断的方法上,但也在可以接受的范围内。

method ev(G) iv(G) v(G)
EnterQueue.enterElevator(int,int) 6 5 6
Elevator.see() 8 7 11
Elevator.look() 9 11 14
Total 110 145 168
Average 1.72 2.27 2.62
class OCavg WMC
Controller 3.5 7.0
Elevator 2.43 51.0
ElevatorQueue 1.4 14.0
ElevatorSimulator 1.6 8.0
EnterQueue 2.6 26.0
ExitQueue 1.875 15.0
Input 1.67 5.0
MainClass 4.0 4.0
RequestQueue 1.75 7.0
Total 137.0
Average 2.14 15.22

3. 第七次作业:多类动态智能电梯调度

(1)类图与时序图

(2)设计策略

在上次作业的基础上,对电梯分类,不同电梯可以停靠的楼层不同,需要考虑换乘策略;支持动态增加电梯,笔者直接在Controller线程中创建新的电梯线程。

本次作业的设计历经坎坷,在设计时内心一度很崩溃。因为调度的思维被第六次作业完全禁锢住,导致实现过程非常复杂且繁琐,甚至误判轮询问题为算法时间复杂度过高,屡次为降低CPU时间而更改算法,连续奋战多天才完成。

这里也不得不承认,笔者写程序的能力仍然比较差,即便设计架构后进行着笔也经常在非常多细小简单的地方碰壁,遇到设计时没有考虑到的问题。面对越发复杂的程序架构,全局性的把控做得非常不好(头脑一片混乱不知所措),仍然需要付出更多时间练习。

①新增类介绍
  • Person:乘客,由于这一次存在换乘,不能直接使用PersonRequest来表示出发地和目的地,因此单独新增了一个类,这个改变也使得笔者几乎重构了代码,将所有方法的参数从PersonRequest改为Person
  • ChangeQueue:换乘队列,存放待换乘的乘客,当乘客离开第一部电梯时,将其激活到EnterQueue中。该类方法均上锁。
  • Output:由于指导书中写明,输出存在线程安全问题,使用该类将其上锁包装。
②电梯运行算法

这一次使用的是纯look算法,因为初始三部电梯可达楼层交集较少,与第六次作业的单部电梯较为类似。

关于换乘:

  • 若电梯EnterQueueExitQueue非空,则忽视换乘请求,优先处理电梯当前任务,且每到达一个楼层激活当前楼层已经能够换乘的乘客。
  • 若电梯EnterQueueExitQueue为空,则电梯运行至待换乘的楼层等待。(这个想法是导致笔者程序出现了轮询情况的元凶,后面会谈)
③请求分配算法 I

延续第六次作业,笔者认为第七次作业也可以采用模拟的方法,由于存在换乘情况,一部电梯无法真实模拟出情况,笔者准备采用电梯群贪心模拟的方法,具体流程如下:

  • 克隆每个电梯状态。
  • 将新增请求分配给所有可行的电梯,分别进行性能(运行时间 + 乘客总等待时间)的模拟,起始时间从0开始计算。
  • 对于需要换乘的请求,每次模拟需要拆分分配给两部电梯,第二部电梯的模拟需要依赖于第一部电梯乘客离开电梯的时间。

该方案实现到最后时发现了两点问题:

  • EnterQueueChangeQueue无法共享“克隆人”。
  • 算法时间复杂度疑似过高。
④请求分配算法 II

既然不能对群体进行模拟,笔者决定只对需要使用的电梯进行贪心模拟,具体实现与第六次作业基本相同,模拟过程忽视了换乘队列的所有请求,因此该方案贪心得到的甚至连当前时间下的最优解都不是,导致性能大打折扣。

关于换乘楼层:笔者决定固定1,5,15三个换乘点,静态换乘。

⑤请求分配算法 III

由于上面的方法通过中测时,仍然存在CPU时间较高的测试点,笔者又改出了对电梯状态进行加权来分配请求的想法,但是加权参数调节十分困难,效果甚至不如算法 II,因此最后放弃了该算法。

(3)量化分析

由于这次作业差点无效,因此最终完成得非常仓促,舍弃了架构,复杂度直线上升(换乘楼层判断方法最高,因为if和else太多)。

method ev(G) iv(G) v(G)
RequestQueue.respondRequest() 5 6 7
Controller.addPerson(PersonRequest) 1 6 8
EnterQueue.waiting(ExitQueue,ChangeQueue,int) 3 10 11
Elevator.run() 3 11 12
Controller.Controller(RequestQueue,ArrayList) 1 8 13
Elevator.predictLook() 9 10 13
Elevator.look() 10 12 16
ElevatorSimulator.simulate(Person,long,boolean) 1 15 19
Controller.judgeChangeFloor(int,int) 12 1 33
Total 165 229 295
Average 1.72 2.38 3.07
class OCavg WMC
ChangeQueue 2.0 20.0
Controller 5.71 40.0
Elevator 2.5 60.0
ElevatorQueue 2.17 26.0
ElevatorSimulator 3.67 11.0
EnterQueue 2.09 23.0
ExitQueue 1.78 16.0
Input 2.0 4.0
MainClass 1.0 1.0
Output 1.0 1.0
Person 1.11 10.0
RequestQueue 2.67 8.0
Triple 1.0 4.0
Total 224.0
Average 2.33 17.23

4. SOLID原则分析

(1)单一职责原则

一个类应该只有一个发生变化的原因。

这个原则实现得比较好,如果电梯类能够减小负载,会更佳。在第七次作业中,调度器没有拆分,职责过于复杂,有违该原则。

(2)开闭原则

一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。

笔者在迭代开发的过程中经常忽视该原则,对已有的类方法进行修改,甚至对成员数据结构进行修改,终究是方法普适性不够,还需要在开发中增加对需求的预测。

(3)里氏替换原则

所有引用基类的地方必须能透明地使用其子类的对象。

笔者这次迭代出现继承关系的只有电梯队列ElevatorQueue,但是并没有直接实例化基类的对象。这里其实可以将基类的方法使用protected保护起来。

(4)接口隔离原则

1. 客户端不应该依赖它不需要的接口。

2. 类间的依赖关系应该建立在最小的接口上。

本单元作业没有单独设计接口,故略去分析。

(5)依赖倒置原则

1. 上层模块不应该依赖底层模块,它们都应该依赖于抽象。

2. 抽象不应该依赖于细节,细节应该依赖于抽象。

本单元没有明显的抽象关系,故略去分析。

(6)总结

没有程序能够完全实现SOLID原则,甚至有些设计模式偏离了SOLID原则。具体原则的遵循需要根据需求灵活改变、优劣权衡后得出。

二、程序Bug分析

1. 第五次作业

  • 自测:只发现了算法上的Bug,某些情况下电梯会两层楼间死循环。
  • 中测:第一次提交就出现了RTLE,经过检查依然是电梯掉头判断的Bug。幸运的是,没有出现线程安全Bug,时间也都十分合理。
  • 强测和互测:没有出现Bug,但由于强测时评测机的负载很高,导致出现很大时间差,两个点性能不幸爆零,在本地复现后(Bug修复环节也交了,但是听说会更改一部分测试点,笔者没有进行更细致的关注,就不作横向对比了)发现运行时间有加快数秒的也有减慢数秒的,甚是玄学。

2. 第六次作业

  • 自测:发现了第五次线程安全的Bug,在null与最后的请求相隔一段时间到来时,电梯无法被唤醒,运气的是评测机无法测试出该Bug,但这个Bug确实给笔者足够的警示,来不断检查自己的线程安全问题。
  • 中测:一次通过,算是比较顺利。
  • 强测和互测:没有出现Bug,且强测性能得分很理想。

3. 第七次作业

第七次作业几乎全是Bug,经历了好一番修改才修好Bug,优化算法也没有再考虑其他的方向。

  • 使用第一版调度算法时,由于无法共享“克隆人”的问题,贪心模拟根本无法结束。
  • 使用第二版调度算法时,中测CPU出现了3秒的情况,笔者误以为是算法复杂度过高,又改了算法。
  • 使用第三版调度算法时,中测CPU出现了7秒的情况,笔者才意识到一定是出现了轮询问题,经过仔细地检查仍然没有发现轮询所在地。后经过dalao推荐,使用VisualVM对jar包进行监视,总算是找到了线程安全Bug所在。当调度器通知各个电梯结束时,由于电梯的wait依赖于结束信号的无效,因此结束信号到来,还有换乘任务的电梯开始了暴力轮询。
  • 强测和互测:没有出现Bug,而且性能分居然很高,笔者认为自己的架构设计属实配不上分数。

三、互测发现其他人Bug策略

由于定时投放以及检查时间、输出正确性都很困难,笔者开发了评测系统,使用的都是比较基础的JavaPython知识,没有做过多的功能性与数据可视化完善,评测系统博客链接:2020面向对象设计与构造 第二单元 评测系统开发

1. 第五次互测

互测期间,笔者专注于评测系统的维护,一直在测试评测系统正确性,只是随便构造了一些数据,发现了房间内成员的线程安全Bug。

同房内还存在如下Bug:

  • 两个程序存在CPU轮询情况。
  • 一个程序响应第一个请求时瞬移了电梯,即优化掉了0.4秒的时间,只要第一条请求在0.4秒之前到来,就必然出现电梯移动过快的Bug。

2. 第六次互测

评测机负责随机数据测试,笔者则观察了同房间成员的架构,没有发现CPU轮询的行为。

经过评测机的对比检验,笔者程序的性能比较经得起考验。

房间内只有一个程序被发现Bug:

  • 某种情况下,会抛弃掉一名可怜的乘客,不拉TA到目的地,即算法问题。

3. 第七次互测

房间内出现了非常面向对象的程序,与笔者自己的架构形成了鲜明对比,笔者花了很久时间研读该程序。

同样经过评测机的对比检验,笔者程序的性能并不理想,常在房间内垫底,不过也在意料之中。

利用评测机发现了两个Bug:

  • 一个程序存在A类和B类电梯载客量错误的Bug,是研读指导书不够细致,忘记修改所有电梯的载客量所致。
  • 一个程序存在超时问题,且这个Bug在随机数据的轰炸下仅仅出现了两次,笔者没有查明具体原因。

四、总结

经过又一单元的磨练,笔者有了很多收获。

  • 多线程程序可以极大地提高运行效率,但是线程安全问题的捕获依然是现今计算机领域的难题。
  • 生产者消费者模式,包括后来提到的观察者模式等,与计组的流水线有异曲同工之妙。
  • 开发评测机也顺便学会了很多Python相关的知识,了解了新的工具VisualVM,收获了不少乐趣。

但是笔者仍旧要反思自己。

  • 思考的角度太单一,受到禁锢之后便无法跳脱出来,希望以后可以拓宽思维,不要局限在一个地方,愈陷愈深。
  • 随便舍弃架构的行为属实不可取,在编程前的架构设计中,既要关注细节,更要把控全局。
  • 我们是来学习的,而不是来赚取分数的。

对OO这门课体验很好,希望自己再接再厉。

posted @ 2020-04-15 18:15  Forever518  阅读(284)  评论(0编辑  收藏  举报