自序
竹外桃落,茶甜鱼肥。人间四月,物物皆对象;世事浮沉,何处不线程?
从新北区到大运村,从新主楼到沙河S7,电梯在我们的生活中可谓无处不在。谁人不曾在某个赖床的早八焦急等待电梯时口头或心中痛骂两句写电梯的程序员,然而直到这一单元的OO作业才知个中艰辛。在此跪谢之前乘坐过所有电梯的程序员不杀之恩。
电梯系列作业是OO课程的传家宝贝,是OO课程的精华之一。一场玄妙无比的多线程探秘之旅即将启航,真正意义上的面向对象程序设计从这里开始……
程序结构与设计策略
第五次作业
第五次作业首次要求采用多线程协作的方法完成单部电梯的调度控制任务。在具体的设计策略上,我采用了课上和指导书推荐的生产者-消费者模式,以Parser类为“生产者”从提供的输入包中读取乘客请求加入请求队列中,Queues类为缓冲区盛放请求队列,Elevator类为“消费者”运送乘客。此外为每个电梯(本次作业只有单部电梯)配备一个控制器Controller类根据请求队列和电梯的运行状态决定电梯的下一步行为。待读取完所有乘客请求后关闭Parser线程,将所有乘客运送完毕后关闭全部线程。本次作业的类图如下。
各类的行数、复杂度和方法复杂度等OO度量信息如下。经过第一单元的认真总结和反思,我在第二单元的作业中开始有意识地控制复杂度,使各类的职责尽量少而明确,各方法也尽量不出现过于复杂的逻辑或过深的循环层次。效果显著,比第一单元作业取得了长足的进步。整体的复杂度情况是比较乐观的,方法大多逻辑简单少见分支,除了Controller类的run方法外没有出现红色的警示,Controller类的run方法也是因为多线程协作的许多分支条件判断不得已而为之。
UML的协作图(sequence diagram)展示了线程之间的协作关系。由MainClass主线程创建各线程和对象,之后即停止主线程。其余按照上述生产者-消费者模式进行线程之间的交互即可。值得注意的是,我的设计与大多数同学的不同之处在于,我的Controller类和Elevator类实际上是同一个线程,扮演“消费者”的角色。这是因为我的Elevator电梯类仅负责基础的移动、开关门等操作,复杂的控制任务全部由Controller类完成,电梯只是Controller类创造的一个普通对象、如提线木偶一般任凭摆布罢了。
第六次作业
第六次作业在第五次作业的基础上进行迭代开发,主要实现了多部电梯调度、负楼层和载客量限制等新增功能。基于新增需求,我在第五次作业设计架构的基础上对原有的Elevator电梯类等做了修改,调度器扩展为两层,新增Distributor总调度器类,负责接受需求并将之分配给合适的电梯需求队列,分配依据是由当前每部电梯的等待队列和运行状态计算生成的权重(weight)给予平衡策略进行动态分配,同时兼顾路程的远近。此外还新增了Translator类,提供两个public方法支持实际楼层和数组下标的相互转换,这样就把-3~16层楼映射到0~18这19个下标数。其余部分按单部电梯调度即可,没有做太大的改动,功能较为稳定。以下是类图。
除了新增一些类和方法之外,整体的复杂度比上一次作业没有太大的波动,仍然得到了比较好的控制,美中不足在Controller类的run方法仍然稍显复杂。
每个类之间的协作关系也延续了上一次作业的设计。不同之处在于电梯进程由一个变为了多个,调度器由单层变为两层,Parsr类读取的新需求需经过Distributor的分配加入到电梯的缓冲区请求队列中,每个电梯的调度器需要实时向总调度器更新各自管理的电梯权重等状态的变化帮助总调度器实现需求分配,以及各处的楼层均需调用Translator类中的方法转换成对应的数组下标之后才能使用等等。
第七次作业
第七次作业延续前两次的设计架构,继续迭代开发,主要解决不同类型电梯的换乘需求和电梯的动态加入,调整和新增了一些类。为了实现换乘需求,将不同的电梯类型加入不同的队列进行管理,使用Dispatcher类解决是否需要换乘以及在何处换乘的问题,如需换乘则拆分乘客需求,待乘客的第一段行程到达后再投放第二段接续换乘的行程。给主类减负,同时为了避免Distributor类管理的事务过多过于冗杂,使用Service类和Parameters类分担Distributor类的数据和逻辑。其中Service类负责管理与一部电梯有关的类与线程信息,Parameters类则用于管理电梯当前的状态和参与分配优先级排序的权重等。因此Distributor类主要用于新需求和换乘需求暂存的分配、新电梯的加入与全局调度,也算是进行了数据结构与算法分离的初步尝试。另外,经指导书和同学提示,将官方包中的TimableOutput.println()封装在Output类中,确保线程安全。同时优化了其它类的部分代码,在满足新需求的同时实现功能与性能的平衡。这次作业的类图如下所示。
与第六次作业相比,第七次作业增加了一些类和代码。可喜的是平均类复杂度进一步下降,主类十分简洁,类的行为更加细分,实现了每个类各司其职、管理固定、明确且尽可能少的数据和功能。但Controller类的run方法仍然复杂,此外Elevator电梯类行数较多,超过了100行,究其原因是在根据电梯类型初始化电梯的运行速度、载客量限制等信息时写了很多行代码,现在回头看倘若结合第一单元的知识,将新建电梯的过程改用工厂模式、新建一个工厂类、电梯类实现继承和多态或许会更好。总之,在复杂度控制方面还有改进的空间。
由类协作图可以看出,类之间的协作关系也是在延续前两次作业的设计同时为新增需求服务。增加了4个类,但没有增加新的线程。可以实现加入新电梯、换乘等功能,同时在Output类中对输出接口进行了封装。
第七次作业架构设计的可扩展性
按照SOLID原则检查第七次作业的架构设计。
- SRP原则指每个类或方法都只有一个明确的职责。我经过三次的迭代开发已经基本实现,对结构较为复杂的类和方法做了一些数据结构和算法上的拆分,尽量小而精,并且在各类和方法的命名中都能顾名思义,明确了它们的职责。如果能对一些比较复杂的类和方法做进一步的拆分会更有裨益。
- OCP原则即开闭原则指无需修改已有实现(close),而是通过扩展来增加新功能(open)。与第一单元作业次次重构相比,我第二单元的作业是真正意义上的迭代开发,无需大规模推倒重构,在设计架构上具有很强的延续性,既省时省力也是极大的进步。但是在每次开发时,由于对新需求的预测不足,有时也因为偷懒图方便,没有留出扩展接口而是想当然地按照某种特殊情况来执行,因此在每次的作业开发中虽然没有修改大的类和方法的代码框架,还是免不了修改一定数量的代码适应新的需求。倘若继续迭代增加新的需求我相信还是免不了如此。因此,我在之后的设计中要注意分析代码的可扩展性,适当预测用户需求变化,不能偷懒否则之后会加倍偿还付出代价。
- LSP原则指任何父类出现的地方都可以使用子类来代替,并不会导致使用相应类的程序出现错误。本次作业中并没有出现类的继承,无法加以评判,暂不评述,多加注意即可。
- ISP原则指当实现接口类时,必须要实现其中定义的所有操作,否则不能创建对象。本次作业中并没有实现接口类,无法加以评判,暂不评述。如果违反IDEA会编译报错,多加注意即可。
- DIP原则即依赖倒置原则,由于本次作业中也没有涉及抽象关系,无法加以评判,暂不评述。
总而言之,从第一单元到第二单元,从第一次OO作业到最近的第七次作业,我的架构设计可扩展性是逐渐变好的,面向过程的陋习越来越少了,面向对象的韵味越来越浓了。但是想要设计完全符合SOLID原则仍然是一件很困难的事,我会在不断的练习中反复揣摩和实践,将这些设计原则逐渐转化为自己的程序员职业素养。
我的bug
第五次作业,中测、强测、互测均没有出现bug。
第六次作业,提交中测使出现了两个bug,其一是电梯会经过神秘的“0层”,很好处理;其二是在所有请求读取完毕后正在睡眠的电梯线程无法按时结束,我修改了有关部分的wait和notify代码逻辑,解决了这个问题。在强测、互测中均没有出现bug。
第七次作业,有些侥幸,中测、强测、互测均没有出现bug。
发现别人bug的策略
和第一单元的作业不同,第二单元作业的测试都强调“定时输入”而非一次性输入。这三次作业的测试有相似之处,首先需要实现按时间戳读取输入数据信息的功能,然后再将待测数据输入运行。然而,因为在检查输出正确性方面出现了困难,我没能实现自动评测,仅靠手动构造测试数据对别人的程序进行测试,在构造测试用例时尤其关注测试边界条件、同时投放大量需求等特殊情况。但由于笔者能力有限,加上房间里的bug数并不多,所以未能hack到别人的bug,自然也没有提交。
心得体会
第五次作业刚接触多线程设计时我几乎是一头雾水,花了很多精力搜索资料自学,仍然不太明白多线程之间的协作、线程安全等等。我第五次作业的设计非常保守,只是照着课件模仿wait和notify的写法,有一些多余的等待和睡眠,监控区设置的范围也比较大,为了避免死锁没有在synchronized关键字中再嵌套使用synchronized。踉踉跄跄勉强完成了第五次作业,也没有想太多电梯调度算法,虽然通过但是性能不佳。第六次作业我已经熟悉了多线程设计的逻辑,对第五次作业的架构进行了优化,而且系统地比较了网上提到的各种电梯调度算法,重点选择了LOOK算法改善原来的设计,大大缩短了运行时间。第七次作业我延续了第六次作业的设计,实现了数据结构和算法的分离,重点实现了换乘路线算法和根据各类电梯的载客量、运行时间等选择最短时间到达的电梯,在确保正确性和线程安全的前提下做了力所能及的优化,在强测中得到了满意的回报。总体感觉第二单元比第一单元轻松一些,能更从容地学习知识、思考优化算法。
幸运的是我在线程安全方面自始至终没有出现令同学们苦恼的死锁等问题,也没有出现电梯“吃人”、“造人”、“瞬移”等玄学bug,我的OO课程少了很多乐趣,也失去了锻炼的机会。但是我仍然不敢说我完全掌握了多线程设计,在第四次课上实验中我因为没能在规定的时间内找出bug而WA了。“三万里河东入海,五千仞岳上摩天”,IT行业的许多场景都是多线程、高并发的,因此我还需要在多线程设计方面加强努力。
庚子残春 于金陵