【面向对象】第二单元作业总结
单元总述
这个单元分为三次作业,一步一步对电梯模型进行了完善,内容从最开始的先来先服务到多线程的待优化的电梯。这是我第一次接触多线程编程。总结下来来看,个人感觉对多线程的理解还是比较透彻的。并且比较好的方面是,在前几次作业中写的一些模块在后面的代码中都得到了复用。下面从几个角度对这几次作业进行一个总结。
多线程编程分析
首先在第一次电梯作业中,我采用了上课讲的生产者消费者模型,以一个队列作为存放需求的托盘,输入队列作为生产者向需求队列中不断加入需求,电梯作为消费者不断从需求队列中取出需求并进行实行。在这次作业中我把共享的控制锁加在了需求队列身上,因此保证了线程的安全性。
在第二次电梯作业中,采取了和第一次作业相似的结构。只是在电梯作为消费者进行取需求时采用了更好的策略,加上了可捎带的功能,除了上面再第一次作业中提到的之外还维持了一个电梯中的需求序列用来存储进入到电梯之中的需求,因此在保证线程安全的前提下完成了电梯捎带的问题。
在第三次作业中,只是在上一次作业的框架之下加了一个总体的调度器用来将命令分解成多部电梯的指令,然后再将这些指令按照时间的顺序加入到每一步电梯的需求队列之中。这样只需要保证新加的总调度器的线程安全性就可以保证整个系统的线程安全性。为了实现总调度器的线程安全性,我们同样采取了设置一个队列并进行维持的方法,注意这个时候不仅仅是输入队列,还有一些需要转乘的乘客也需要加入到总队列之中,所以尤其要注意随机唤醒一个消费者进程的时候需要保证队列不为空。
在三次作业中都采取了wait-notify的方式来进行控制。所以需要注意的一点是如何标记程序的结束(即输入完成并且所有需求已经满足),三次作业在结束的判断上都花费了一些功夫。
基于度量的分析
第五次作业
- 总体结构分析
在本次作业中根据需求很自然地分成了生产者(输入)、消费者(电梯)、托盘(需求队列)和输出四大类。然后每一个类根据具体的属性再进行详细的设计。
- 复杂度分析
可以看到每一个类的复杂度控制的还是相当不错的。
每一个方法也不是很复杂。
- 依赖度分析
可以看到在这次的作业中不存在相互依赖的关系,直接依赖也比较少,类的设计还是比较合理的。
第六次作业
- 总体结构分析
可以看到由于电梯与调度器放在了一起,所以电梯这个class还是略微显得有些臃肿。
- 复杂度分析
在复杂度分析中,果不其然的是电梯这个类有些臃肿。
但是具体到每个方法而言,复杂度还是控制在一定范围之内的。
- 依赖度分析
和上一次作业相同,由于采用了相似的结构,因此依赖度也相似,都不是很高且没有相互依赖。
第七次作业
- 总体结构分析
可以看到我们增加了一个调度器类,并且对需求进行了拆包重组,从而更加社和本次的任务(可以将一个需求分解成为两个需求)
- 复杂度分析
可以看到由于调度器设计的比较复杂,因此这里的复杂度略微地偏高。
但对于每个具体的方法而言,每个方法的复杂度还是控制在一定范围之内的。
- 依赖度分析
同样的,本次作业中也没有出现相互依赖的情况,并且依赖性的控制也比较符合预期。
关于SOLID原则
(这一部分参考了https://blog.csdn.net/vichou_fa/article/details/52523617关于SOLID规则的描述)
SOLID原则就是指:
SRP | The Single Responsibility Principle | 单一责任原则 |
OCP | The Open Closed Principle | 开放封闭原则 |
LSP | The Liskov Substitution Principle | 里氏替换原则 |
ISP | The Interface Segregation Principle | 接口分离原则 |
DIP | The Dependency Inversion Principle | 依赖倒置原则 |
1.SRP
当需要修改某个类的时候原因有且只有一个。换句话说就是让一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。 类被修改的几率很大,因此应该专注于单一的功能。如果你把多个功能放在同一个类中,功能之间就形成了关联,改变其中一个功能,有可能中止另一个功能,这时就需要新一轮的测试来避免可能出现的问题,非常耗时耗力。
在本次作业中我尽量遵守了这条原则,但是由于第一次设计中将电梯和调度器放在了一起,导致后期每次需要更改调度器策略的时候都要在这个类中加加减减,因此其实两部分分开的话就会避免这个问题。
2.OCP
软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。
这条在这三次作业中还是十分注意了的,尤其在接触了SOLID原则之后。在第三次作业时我没有选择去修改Elevator类,而是新扩展成了BasicElevator类,在之前的基础之上增添了第三次作业需要的功能。但是这条原则貌似存在的问题是需要将所有的field都设置成可以扩展的而不是private的,所以在每一次设计时都要想到后面这个类很可能被继承扩展。
3.LSP
当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。同样的接口模块之间,可以在不知道服务模块代码的情况下,进行替换。
就目前的作业而言确实是这样的,子模块所有的接口和父模块都可以替换。但是由于本次作业中继承的比较少,还不涉及到这个问题,但在以后的作业中会注意这个问题。
4.ISP
不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。 客户模块不应该依赖大的接口,应该裁减为小的接口给客户模块使用,以减少依赖性。如Java中一个类实现多个接口,不同的接口给不用的客户模块使用,而不是提供给客户模块一个大的接口。
本次作业中并没有使用接口模块,但在以后的作业中会注意这个问题。
5.DIP
1. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
2. 抽象不应该依赖于细节,细节应该依赖于抽象
本次作业中并没有产生高层依赖底层的现象(由于层次还是比较简单),但在以后的作业中会注意这个问题。
Bug分析
第五次作业
本次作业的难点其实就在于如何写一个安全的线程。由于我们设计的比较清晰,因此并没有出现Bug。
第六次作业
本次作业的难点在于如何写捎带的算法。在前期刚刚完成代码的时候出现过几次程序不能正常结束的问题,但通过一些测试之后发现了这个问题并得到了解决。因此也没有在公测之中出现Bug。
第七次作业
本次作业的难在于如何调度三部电梯。说实话因为想要更好的去优化这个算法,反而导致了在其中一部分换乘时的逻辑过于复杂,从而没有满足C电梯的条件。反过头来想一想其实在设计的时候就应该避免这种过于复杂的调度算法的出现,不能把一大堆逻辑挤在一起,这样只会导致在debug时有疏漏的地方。在下次设计的时候会非常注意这点。
心得体会
- 线程安全
在多线程程序中,错误的复现实在是一件非常困难的事情。输入的先后顺序、执行的先后顺序都有可能导致同一个bug不会出现第二次。所以可能我们能做的只是在设计的时候尽可能减少bug发生的可能性。在我的整个设计过程中我体会到的是尽量减少共享变量的个数、尽量少去存储读取共享变量,尽量将每个线程的执行过程与共享的数据分隔开。这样做的好处可能就是尽可能地简化程序的逻辑,然每一个线程尽可能的独立。此外对锁的运用也是随着对多线程编程的深入了解也逐渐熟练。从最开始的每进行一次读写就要将整个类锁死到最后的只设计几个监控的锁,在涉及需要锁的时候只对这几个加锁。其实这个过程是可能增加Bug出现的可能性的,但是可以提升程序执行的效率。
- 设计模式
三次作业使用的都是生产者-消费者模型。虽然在课堂上也介绍了有关订阅-发布模型等等,但是由于为了程序代码的复用并没有选择更换设计模式。在以后的coding过程中可能会考虑使用更多的设计模式。