我永远爱着OOP——第二单元总结
第二单元的电梯真是愉♂快呢,多线程编程作为java编程OOP中的重要组成部分,通过这一个单元的学习,我也是有了很多全新的认识
那么下面就先例行一下公事
三次作业分析
第五次作业
设计分析
实现的电梯是很简单的,没有复杂的逻辑,主要目的应该也是帮助同学们入手多线程编程,加上课上对设计模式有所点拨,所以整体的设计应该是不难的,编码量也不大,只要处理好锁的关系和wait
与notify
的时机,不要出现死锁,基本是不会有什么问题的
下面给出我的设计,也就是中规中矩的生产者-消费者
模式
这里我封装了自己的一个容器Requester
(感觉什么东西加上个"者"后缀就很厉害呢),其实就是一个请求队列,实现了一下迭代器接口(后来发现不好用)
然后就是电梯线程和电梯线程,调度器线程没有启用,这个设计主要是为以后的扩展做了一点准备,所以这里的Scheduler
是个静态的
然后差不多就是这样了,邻接资源就是那个队列,使用的时候锁一下就好了
一个需要注意的地方是怎么控制输入结束的全局持续通知
这里是借用了信号量的思想,也就是设置了一个相应的属性,然后每次循环的时候查看一下这个信号,别的就没什么了
下面是整个程序的一些度量(忽略这只手)
因为有各种各样的输出语句,所以Elevator
类的方法好像有点多了,不过我确定一定很短,很简单
你看
方法的复杂度都很低,看起来还是可以的
测试
对于多线程的程序我不是很清楚怎么做单元测试,单元测试也只能保证模块功能正常,但是我们都知道,真正的问题往往出现在线程的控制上的。
分享课上同学们分享的也都是各种黑盒测试(个人是比较鄙弃全部依靠黑盒测试的行为,盲目的随机胡测也许很有效,但是绝不优雅),所以也欢迎大家分享如何富有逻辑地进行测试。
第六次作业
第六次加上了捎带规则,加了点楼层,加了负层,0层神秘消失(误),整体来说笔者的设计是这样的。
三次设计的思路都是坚持做到单一职责,毕竟分割职责可以在做修改和扩展的时候放心大胆(我一向很大胆,其实是因为分隔职责后便于测试,良好的测试是修改的保险丝)。
所以就是
- 输入线程只管输入,是一个无情的指令接收机器
- 电梯线程只管执行,是一个无情的指令执行机器
- 调度线程负责调度,完成(
无情地)指令分发的任务
然后这样的设计就出现了
!
相比上次的设计,没太多变化,就是调度器动起来了,然后肩负起分配指令的任务
然后电梯中需要有一个队列维护自己持有的指令,线程变成了单步执行,每层看一看有没有上下开关门事件发生,逻辑不算复杂
调度器就是不断尝试将指令放入电梯中(后来笔者发现这里设计的不好,调度器空转太多了,加一个wait
和notify
机制会更好,或者是join
,不过想来想去,抢占式或许是最优解,这个后面再说)
然后下面是整个程序的度量
可以看到调度器的复杂度比较高,其实主要是笔者作死的一个设计
笔者为指令和电梯运行加了一个方向的概念,因此自己给自己多设置了一层逻辑,没什么意义,也就是有MainRequest
和Direction
两个量都表征运行方向,每次更新都要更新俩,判断的时候用哪个也说不清,确实是很糟糕的设计,笔者在后面也把这个量给废弃了
测试
关于这次的测试,笔者给出一些个人思考的可能出现问题的情况
- 对于LOOK玩家,在电梯折返,或者说换向的时候打断,可能是一个边缘逻辑
- 对于开关门和来指令的并发情况
- 对于错过的指令的测试
第七次作业
设计分析
据说这是OOP作业难度的顶峰
鬼畜多电梯、负载、速度、随机突发随机指令,这几个情境下设计一个好的算法确实不容易
所以与其苦心分析设计,不如"实践检验真理",也就是通过本地测试时间来决定算法的性能(真是个小机灵鬼)
所以在说我的设计之前,我想理性(瞎胡说)地分析一下这次调度算法的考虑
1. 关于接人和方向
(笔者很讨厌上楼的人在电梯下行的时候就上电梯),在本次作业中,出于载荷的考虑,也是加上同向捎带的判断,电梯的性能应该会好一些,但是实际检验发现,这个判断的效果并不明显,并且有的时候会出现负优化,个人分析原因是在于40条指令,三个电梯的总容量有20多,所以即使面对40指令并发,对于电梯们来说,工作量也不是很大。
正因如此,像我们考虑的情景——“很多反向请求的人上电梯,导致同向的请求上不去”,其实发生率也不高,所以在这种情景下,大吞吐反而是很优的。
2. 细节优化
对于这个评测方式,其实很多细节优化的效果也是不错的(只指分数,不保证绝对时间差),比如电梯的选择顺序,是快的优先还是随便来;是近的优先还是随便来。这种细节上的选择也会有不错的收益。
3. 抢占式很重要
如果让调度器主动分指令,电梯就要记着一会要接谁,即使这个人不在电梯里,出于安全考虑,电梯也必须为这个人留个位置,那么在这种情景下,电梯一般是一直装不满的。
本人的算法基本是硬调度,没有动态调度,调度规则都是硬编码的。(不像那些大佬们在电梯运行的时候动态通信)
程序的逻辑就是每次楼层改变和进出人后就去中间的Commander
那里pull
一下,看看有没有自己能拿走的,这样可以保证最大利用电梯容量
然后pull
的逻辑是在Commander
中实现的,Commander
获取电梯的当前状态,然后判断队列中有没有他能拿走的
下面是程序的度量
看起来Commander
好像不太高兴了,因为笔者不小心把指令分割的逻辑放到了Commander
里面,这个是违反单一职责原则的,我在下面简单做一下拆分
可以看到,加上Spliter
后,Commander
的复杂度明显下降,此时Commander
中只剩下和分配有关的逻辑
方法的复杂度还是很不错的
测试
这次测试笔者主要关注了下面这几个点:
- 载荷,要保证不能超载,对于电梯载荷上限的数据要进行测试
- 需要拆分的指令,对于需要拆分的指令,特别是可以多次拆分的指令,要重点测试
- 效率,最好在测试的时候单步输出一下每时刻每座电梯中的人数,确保电梯有较高的载重
设计原则分析
- SRP(单一责任原则):这次设计基本是符合单一原则的,唯一不足的地方是,电梯是主动去获取指令的,获取指令的逻辑也在电梯类里面,可以考虑专门做一个获取器,然后电梯每次调用这个类的对象
- OCP(开放封闭原则):这三次作业其实需求扩展体现的并不明显,重点是需求变化,完全地不变更原设计是不现实的
- LSP(里氏替换原则):这三次作业的类基本继承层数都是1,没有父子类的问题
- ISP(接口分离原则): 三次作业不涉及接口问题
- DIP(依赖倒置原则):三次作业中的类基本没有继承层次,所以也不涉及依赖倒置的问题
UML时序图分析
好了,我完事了
上面就是本系列作业的设计了,下面说一些我想说的
相比于上个系列作业,这个系列突出的需求增加更加明显,也更加友好,不存在说两次需求相互矛盾的情况,对于设计优秀的同学,每次作业的编码量肯定是不大的
然后吧,这个时间效率这个事情,我觉得确实是一个摸索的过程,群里有人在说用NN,问题不大,其实我上面说到的自己去尝试算法,然后调整算法,其实就是个手动NN的过程(误),真正要是在实际中解决这种鬼畜电梯的问题,就是要靠NN来实现根据过去推将来的,用NN来实现对未来的分析预测,从而调整自身调度策略,获得最优调度
下面想重点说一下这个监听者模式,老师上课提了很多次,下课也听同学们有讨论,所以说一点个人的见解(以下都是胡说,理性参考)
监听者模式的提出,是为了能够让一个个的listener
动态地加入监听和退出监听,同时,在Commander
新消息发布的时候,可以通知所有监听者,java
的Swing
库就大量使用了这种设计
针对本次作业,笔者最开始是考虑过用这个模式的,每当输入一个指令就通知一下电梯,然后电梯取指令,刚好很多电梯,然后可以都注册为listener
但是存在以下几个问题:
- 所谓通知,在这里的体现就是把指令放进去,但是,在指令来的时候,电梯可能正处于上锁状态(例如,进出处理中),这时我们的
Commander
试图尝试往每个电梯里放指令,就会遇到锁 - 其实上面那个还不是最重要的,毕竟只是要等一下下就好,问题是当某个请求无法被任何电梯处理的时候,这个请求就要被存起来,这样的话,其实就又退回了我们的生产消费模型了(毕竟在
Swing
中,如果一个消息没有监听器,这个消息是会被扔掉的,但是显然我们这里不能这样)
所以,这个模式虽然很美,但是放在本系列作业中也许有些不当之处,当然,这只是我浅薄的认识,如果大家有更好的实现方式,也欢迎在下面留言,一起讨论,一起学习