OO第二单元总结
第二单元结束了,比起第一单元对OOP的懵懵懂懂,第二单元才开始真正思考了什么是面向对象,什么是好的架构,什么是好的设计。查了很多资料,看了很多大佬的博客,回顾了前面的课程,重新审视自己本单元的设计。
分析需求
本单元要求模拟调度电梯运行,实现上下楼载客等功能。而电梯的数量和功能也在不断的增加和多样化。从作业1的单部电梯搭载全部乘客并且不限制电梯的载客量,到作业2的多部电梯搭载乘客,每部电梯有最大载客量,再到最后作业3的电梯不仅有最大载客量,还有不同的种类,不同的载客量,不同的上下楼时间,还有增加电梯。这样的增加需求也算是迭代开发一个很好的例子了。
然后就分析具体需求,本次主要分两种情况。一个是乘客,一个是电梯。乘客提出请求,表示自己出发楼层和目的楼层,电梯接受请求,处理乘客请求,将乘客从出发地送往目的地。因此我们需要接受乘客请求,处理乘客请求。
具体到每个作业。作业1需要接受乘客请求,分派乘客,此处分派乘客就是需要送往一个电梯。作业2在作业1的情况下,需要决定分配乘客到哪个电梯。作业3在作业2的基础上还要决定乘客是否需要换乘,增加电梯后乘客整体情况需要这么变。
设计阶段
作业1
本次作业中,先设计一个输入线程,用于实时接受乘客请求。因为只有一个电梯,本次作业并未设置单独的调度器,输入线程将请求放入请求队列,电梯直接从请求队列中拿出请求。单个电梯采用LOOK算法。
本次作业较简单,架构并不复杂。主要采用了生产者-消费者模式。
UML类图如下:
作业2
本次作业引入了多部电梯。在助教的点拨下,我加入了调度器,通过调度器分派乘客请求给各个电梯,各个电梯对自己单独的请求队列处理,每个电梯交互较小,耦合度较小。同时,为了实现性能的优化,第2次作业电梯我也采用了换乘模式,需要换乘时,电梯再将乘客请求放回请求队列,因此电梯从第一次作业的纯碎的消费者变成生产者和消费者。同时request
和Scheduler
类借鉴了Worker-Thread
模式。
老师课上讲到,不要在一个线程中直接引用另一个线程,但我在Scheduler
线程直接引用了Elevator
类,不是一个良好的设计(应该对每个电梯加一个请求队列的类,实现调度器和电梯的交互)。
总的来说,第一次作业到第二次作业架构变化并不大,即是因为第一次作业预留了一些迭代开发的接口,更多的是作业跨度并不大。
UML类图如下:
作业3
本次作业较之前还是有较大区别,主要区别还是体现在增加电梯和电梯停靠楼层太混乱。但在第二次作业中,我已经涉及到了换乘问题,因此第三次作业需要改动的只有换乘策略和选择电梯的策略。
UML类图如下:
时序图如下:
实现阶段
本单元中,最重要的是线程问题。既要避免轮询,又要保证不死锁,及时唤醒和结束线程。
为了便于修改request
,将request
信息复制到新建的Person
类。
第二次作业的分派主要是按照楼层分派,将电梯限制到连续楼层,然后调度器分派即按照楼层分派即可。
第三次作业分派先看是否是只有特殊楼层(只有一种电梯能运),还是换乘站,换乘站根据谁请求队列内人少,符合电梯运行规则。
但第三次作业由于电梯换乘比较麻烦,因此许多特殊楼层需要特殊判断,为了实现更好的性能,Scheduler
类需要了解一些Elevator
的信息,因此耦合度较高。
同时在三次作业中,特别是第三次作业,Scheduler
类越来越长,甚至超过了checkstyle
的限制,因此又加了Choice
类,来帮助实现选择。
设计分析
SOLID设计原则
SRP原则
每个类或方法都只有一个明确的职责
本次作业中,每个类都有自己明确的任务,但部分类承担的任务过重,还有待改善。
OCP原则
无需修改已有实现(close),而是通过扩展来增加新功能(open)
在前面两次作业中,实现的较好,这也使得迭代拓展变得较容易,但第三次作业中,为了追求性能,导致耦合度变高了许多,拓展也变得难了许多。
LSP原则
任何父类出现的地方都可以使用子类来代替,并不会导致使用相应类的 程序出现错误。
只有在第三次作业中涉及到电梯继承,由于每个电梯行为差异较大,父类很少有实际的有用的行为。
ISP原则
任何父类出现的地方都可以使用子类来代替,并不会导致使用相应类的 程序出现错误。
在电梯类中,我是用的继承,但仔细想想,不同电梯其实具体行为差异较大,用接口来抽象层次比继承感觉要好一些。
DIP原则
高层模块不应该依赖于底层模块,两者都应该依赖于其抽象
本单元作业中层次化设计较少,更多的只是数据交换,以后应该将一些行为抽象化为层次,来到达更好的架构。
测试阶段与Bug
本次作业出现的Bug较少。本次作业可能出现的bug包括,电梯运行行为出错(包括开关门,上下客等),死锁,轮询,电梯无法及时唤醒和停止,换乘时未将乘客送往目的地等,总的来说本次作业的bug除了多线程不稳定外,还是比较好避免的。
前两次作业电梯行为比较死板,主要集中测试特殊情况,比如单层楼上许多人的情况,这种情况也在互测时hack到了人。
第三次作业更多的是集中特殊楼层的起始与到达,比如换乘站1楼,15楼,还有及其特殊的3楼等,这也让我在互测中hack到了人。
自我反思
本单元我对自己的设计并不是很满意,不仅是对自己电梯的实现和性能,更多的是架构和层次设计感觉没有让自己满意。经过了这么久的学习,让我对什么是好的代码,设计,架构有了新的理解。
从单行代码讲起,要实现让人易懂,包括代码的规范和逻辑的复杂度。尽量少枚举数字等特征,转而用更抽象的行为描述,每行代码要清爽整洁,这样会对迭代开发,或者合作开发时带来巨大帮助。而我这次虽然较一单元更注重了这方面,但仍做的不够理想。
然后是方法和类,方法要尽量做到简洁,不要变成“面条代码”,更多地以抽象层次描述。类,也是,避免出现God类和Idiot类,一个类不能太过复杂,不能太过简单,类之间应该有明确的分工,这样才能更好的实现高内聚低耦合思想。前两次作业还做的较好,为了追求性能,第三次作业就开始类之间耦合度明显上升。不是一个很好的设计。
最后是架构,一个良好的设计是要兼顾性能和正确性的。但有时候由于自己水平有限,很难两者兼顾,很难在追求性能时保证各个类之间的耦合度很低。虽然看起来对于某次作业或者工程来讲,性能高可能是更好的(毕竟能得更多的分),但是长久来看,为了开发一个更大的项目,为了迭代开发,追求性能带来的耦合度的上升很难满足OCP设计原则,维护代码难度大大上升,也就是很难在不修改或者修改很少代码的情况下实现迭代开发,必须要修改大量代码甚至是重构的情况下实现,如果项目较小还有可能实现,但工程一大,这样几乎是天方夜谭。同时,我的架构中也缺少抽象层次,更多的是“你干什么,就给你分派这个工作”,工作一多,管理自然就很困难。如果能将行为抽象出来,相近的行为统一管理,既好维护,也为迭代开发提供了很好的基础。
因此我尽量在以后的作业或者其他场合开发代码,要做到从开始花尽可能多的时间去思考架构,不要认为最开始作业简单,就随便采用一个“只是能用的架构”,导致耦合度较高,迭代开发困难;同时也应该将行为层次化,将数据层次化,将结构层次化。(感觉说的有点空,只能慢慢感受了。。。)
最后,感谢课程组的老师助教们的付出,感谢大佬们的分享。希望自己在下个单元能学习老师和大佬们的意见,真正地实现一个好的架构。