OO第二单元小结
一、设计策略总结
HW1:
第一次作业设计比较简单,采用了生产者——消费者模式:主线程作为生产者获取输入请求并放入调度器中;调度器作为单独的一个类,并不新开进程,而是归入主线程中;电梯作为消费者,单独开设一个线程,并从调度器中获取请求。
HW2:
第二次作业新增了多部电梯的需求,但其实本质上是一样的,因为每部电梯是独立的,可以看作是HW1的情况。所以,在HW1的基础上,我只是将原来的调度器拆分成了一个主调度器和对应电梯个数的分调度器。主调度器获取主线程放入的请求,通过分析几个电梯目前的状态来制定策略,分配给相应的分调度器。分调度器和HW1中的调度器功能基本一致,只是多了一个向主调度器反馈电梯目前状态的功能。而电梯则新增在每次更新相应楼层或人数时更新分调度器相关信息的功能,与分调度器构成观察者模式。
HW3:
有了HW2的设计,HW3的设计其实没有太大改动。只是我意识到了必须从请求获取更多信息以方便分派,所以我将课程组提供的PersonRequest类与其是否需要换乘,换乘楼层,以及需要进入的电梯等信息封装成了一个Person类。每次进入主调度器后,主调度器调用Strategy类进行Person类的包装,并分派给相应的分调度器。若该请求到达楼层为目的楼层,则直接简单地排出。若为换乘楼层,则将其排出,并调用主调度器的redispatch方法,将其重新分配至新的分调度器。而若来的请求为新增电梯请求时,则在主调度器类新建一个电梯和相应的分调度器。
二、架构设计的可扩展性分析
以下利用SOLID设计原则对本人的HW3架构的可扩展性进行分析:
SRP:每个类或方法都只有一个明确的职责
本次设计的分调度器Table类,其职责有两个:一是获取和分配请求,二是获取电梯信息并反馈给主调度器。因此,根据SRP,这种设计是不好的、冗余的,可以考虑从中剥离出一个Message类专门获取和反馈电梯信息。
OCP:无需修改已有实现,而是通过扩展来增加新功能
本次Elevator类的设计是很明显不符合这一原则的,因为我的调度算法和该类的run方法耦合在了一起。这就导致若以后有其他牵扯到调度算法的需求时,要对这个类进行大幅改动。因此,考虑实现一个Elevator接口,由满足不同需求的电梯去实现这个接口,这样设计就有了可扩展性。
LSP:任何父类出现的地方都可以使用子类来代替,并不会导致使用相应类的程序出现错误
因为本次设计架构未涉及到继承关系,故在此不对这一原则进行分析。
ISP:通过接口封装一组高内聚操作来建立行为抽象层次
其实,个人以为,接口封装这种操作遵循两个原则——“多多益善”和“宁缺毋滥”。“多多益善”指尽可能去思考架构中每个类的未来扩展可能性,并将其核心的方法封装成接口。以我的HW3为例,我这次的代码中没有接口的设计,这很明显是不符合这一原则的。同时,“宁缺毋滥”的意思就是我们创建接口时要准确提炼出一组高内聚的操作。一组高内聚操作组成的接口起到了未雨绸缪的作用,而封装不到位的接口则相当于埋雷。
DIP:程序要依赖于抽象接口,不要依赖于具体实现
个人感觉第一单元所说的多态即为DIP的一种实现方法。上文中提到过,本次HW3的Elevator类可以依照这种原则进行改进,抽象出一个Elevator接口,然后调度器与该接口进行交互,这样可以满足未来多种不同功能电梯的需求。
三、基于度量的程序结构分析
HW1:
UML类图:
HW1的要求比较基础,故简单地通过主类、调度器类、电梯类三个类来实现,这样的形成的架构十分清晰。
经典OO度量:
可能是由于HW1架构较为简单,导出的这些度量值并未有太大的问题。不过可见本人Elevator类的LCOM(内聚缺乏度)相对较高,可以考虑进一步去除一些与电梯工作相关性较低的方法,增加内聚度。
UML时序图:
HW2:
UML类图:
如第一节设计策略分析时提到的,HW2的类仅仅是在HW1的基础上将一个Table类分离成了一个主调度器Tray类和一些分调度器Table类。这种类的设计的一个很大优势是符合OCP原则,工程量小且能保证正确性。但同时,我发现这一设计的缺点是我的类数太少,这样可能导致一些类的内聚度很低。因此,可以考虑在此基础上剥离出一些新的类,如Elevator类的调度算法类等。
经典OO度量:
在分析OO度量数据时发现Tray的LCOM较高,这也是前文分析中所提到的情况,故在此不过多赘述。
UML时序图:
HW3:
UML类图:
关于HW3类的设计则依然是一脉相承的,这次我吸取了之前的教训,新扩充了Strategy和Person类以使每个类的功能更加专一。不过在经过code smell分析后发现还是存在一些设计方面的隐患——Elevator类和Channel类之间存在循环依赖,希望以后确定类之间关系时能避免这种闭环情况。
经典OO度量:
HW3的度量结果还是令我较为满意的,每个类的LOC(代码行数)都不算太大,不存在冗余类的情况。同时,除了从HW1一直延续的Elevator类,其余各类的LCOM也都保持了一个较低值,具有较高的内聚度。
UML时序图:
四、程序bug分析
由于本人第二单元三次作业都本着牺牲性能追求正确性的原则,故有幸在三次作业的强测和互测都未被发现bug。
五、Hack策略分析
本人本单元的三次作业都采用自动化测试的方法进行hack,每次测试都采用随机生成的数据输入,再对输出结果的正确性进行检测。可能是因为这种随机化的测试样例不够强,本人本单元的hack有效性较低,三次作业分别成功hack了4次、0次、2次。而对于线程安全相关问题的检测,本人通过数据的投放时间间隔,增加电梯与调度器交互的频率,并使请求的投放数超过电梯最大载客数,观察在这种情况下结果是否会报错。
总的来说,相比本人第一单元傻傻的收集测试样例手动测试,第二单元采用自动化测试这种方法还是很方便的。但同时个人感觉,与第一单元只需要生成大量随机多项式就能进行hack不同,第二单元大量随机的请求强度并不是太高。因此,在随机数据的基础上,我们还应该面对线程安全这一问题进行特定的样例构造。(比如一些大佬构造的造成死锁的样例
六、心得体会
线程安全:
线程安全是多线程中的重点和坑点,很侥幸的是,本人在Unit2的三次作业中并未碰到严重的线程安全问题。以下本人想提出一些自己的拙见:
第一,我觉得我们应该在设计阶段就将线程安全问题完全解决,而不是在一边码代码的时候一边思考这一步要不要考虑线程安全,这样是十分容易出错的。
第二,我们可以通过画简单流程图的方式确定各个流程的wait和notify顺序安排,将流程之间的协作具体化,这样能尽可能地避免发生死锁情况。
设计原则:
个人以为SOLID设计原则十分具有指导意义,正是因为本单元我的HW1到HW3的大部分设计都遵循了这些设计原则,作业的正确性才得到了很好的保障。其实从Unit1到Unit2,虽然发生了从单线程到多线程的巨大跨越,但其中设计优先这一理念却一脉相承。如果我们能在设计阶段就反复将自己的架构与这些设计原则进行核对,不断优化,那么我想,代码的正确性和扩展性便是水到渠成的。
一点感受:
通过Unit2的学习,我真正进入到了多线程的世界,虽然问题的复杂度有了一个飞跃,但这复杂却是一份贴近生活的真实。通过造电梯(其实本人电梯的性能相当于楼梯),我感受到了面向对象思想在实际生活中的应用,进而加深了对其的理解。大千世界,对象繁多,如何统筹规划各个对象,协调它们之间的运作,是将伴随我们一生的问题。希望能通过OO的学习,和大家一起进步,不断提升自己解决问题的能力。