OO第四次博客作业
完成第四单元的UML类图的解析之后,OO课程就告一段落了,感到有些轻松又有些不舍。本次博客作业将对第四单元的设计进行分析,同时会进一步总结四个单元OO学习的体会与收获。
一、第四单元架构设计
我在第四单元作业前对于架构设计思考了很久,个人认为这次作业的设计在层次划分上做的还是比较理想的,可维护性和可扩展性都还不错。除了最后一次作业允许接口拥有属性导致我对部分方法进行了修改之外,后两次作业的迭代都没有修改原有代码,比较好地实践了开闭原则。
由于需要解析三种UML图,我分别设置三个类ClassGraphInteraction、StateGraphInteraction和SequenceGraphInteraction放入来存储对应的数据,之后由主控类MyGenaralInteraction调用这三个类来完成对应的查询方法(这三个类分别实现了UmlClassModelInteraction、UmlCollaborationInteraction和UmlStateChartInteraction接口)。在构造这三个类的对象时,我先将原来的elements分类,之后分别传入三个类的构造方法中。构造方法中需要对各种类型的UMLElement对象分批次进行解析。

第十三次作业中,对于操作较为复杂的class、interface、operation和association我都单独设计了类来封装相关的属性和操作,其中class类和interface类的操作有较大的相似性,因此我将它们设置为抽象类Node的子类,将公共操作定义在Node中。对于Interface的查询操作我使用了递归查询的方法,子类会向上调用父类的查询方法。由于接口多继承可能出现指数级别的复杂度,我对输出数据allInterfaces进行了缓存。

第十四次作业中,我同样设计了StateMachine类、State类、Interaction类和Lifeline类来存储对应的UmlElement。在新增的方法中,比较复杂的方法是计算状态图后继状态个数的方法,由于状态转换会出现循环的情况,不同于接口的查询,因此无法使用深搜+缓存的方法,因此我使用了广度优先搜索的方法,每次查询时进行重新搜索。由于我没有将初始状态和终止状态和普通状态进行统一,因此在这个方法中还需要额外判断一下终止节点是否是后继状态。
第十五次作业中,新增了初始化后的检查环节,检查方法中比较复杂的时检查循环继承和重复继承接口的方法,这两个方法我都使用了遍历节点进行BFS的方法,如果搜到了自身则有循环继承的现象,如果搜到了已经访问过的点则有重复继承接口现象,时间复杂度为O((V+E)V)。在R004检查是否重复实现接口的方法中,我将类直接实现的接口、父类实现的全部接口、直接实现的接口继承的全部接口依次放入HashSet中,如果放入的元素出现重复则说明存在重复实现接口的现象。
以下是方法复杂度和类复杂度,其中方法复杂度中已经省略MyGenaralInteraction类和其他类中的简单方法。
从复杂度中可以看出,几个构造方法的复杂度比较高,在涉及图的几个方法复杂度也比较高。ClassGraphInteraction类的复杂度较高,可以选择将类图解析进行进一步的拆分,例如将对操作的查询、对属性的查询放在两个不同的类当中。


这个单元的整体体验还不错,由于课程组完成了对输入输出的解析,我们可以把全部的精力花在架构的设计和UML图的理解上。我还阅读了课程组下发代码中关于输入输出解析的部分,感觉很有收获。输入部分比较精妙的地方在于InputArgumentParser类,这个类主要完成的工作是:首先通过parse方法将输入的数据分割形成参数列表,之后利用反射调用对应类的valueOf方法,将解析的任务下放给对应的类。整体的交互过程是由AppRunner类完成的,交互的过程是一个有限状态机,在五个状态之间进行转换。在model部分输入完毕后将读取的UmlElement传入构造器中并进入指令交互阶段。正常输出信息和异常信息全部由OutputInformation类进行统一处理。相比之前我自己处理输入输出时的面向过程和大量硬编码,这些代码处理输入的时候很好地兼顾了OO的设计原则,层次化的处理使得代码可扩展性很好。
二、架构设计及OO方法理解
1. 表达式求导

采用了Poly、Item、Base三个层次,其中Base用于存放因子,Item用于存放项,Poly用于存放表达式。Poly、Item和Base类全部实现derivative接口,均可调用求导方法并返回一个Poly类的对象。抽象类Base有5个子类,分别是Sin、Cos、Pow、Constant和Expr(内置Poly对象,用于存储表达式因子)。
解析表达式时使用了正则表达式的方法,在使用之前将全部最外层的括号都替换成了@符号,之后逐层提取相应的元素。创建对象时使用了工厂方法模式,对应5个Base子类的工厂ConstantFactory、SinFactory、CosFactory、PowFactory、ExprFactory全部实现BaseFactory接口,均可通过getBase方法返回对应的Base对象。在WRONG FORMAT的判断中,在Main函数中加入try-catch块,如果解析过程中表达式不合法,则抛出异常并由Main函数捕获,打印出错误信息。
这个单元对OO方法的理解还处于摸索阶段,一开始仅仅将类当作结构体来使用,对于迭代开发的意识也比较弱,导致重构成为常态。之后慢慢开始将封装、继承和多态融入程序的设计之中,开始注意程序的层次划分、可扩展性,体会到高内聚低耦合的
2. 电梯作业

本次作业大致的设计思路契合了Worker Thread模式。由于需要进行乘客请求的分派,我增加了一个Scheduler类,这个类可以主动获取电梯的运行状态,通过比对乘客信息和电梯的运行状态来选择适合的电梯分派请求。分派后请求进入各个电梯的请求队列,由电梯自身来进行调度。Input类的职能类似于Client,负责读入并提交请求信息到Channel类中。电梯类的职能类似于Worker,进行单个电梯的调度和运行控制。
为了处理换乘问题我通过继承PersonRequest增加了Person类,新增endFloor属性用于存放乘客的最终目的地,原有的toFloor属性用于存放暂时的目的地。在这个类中我穷举所有无法直达的楼层来确定对象的toFloor属性。
这个单元老师还重点对OO的一些设计原则进行了介绍,通过检查程序是否符合SOLID原则可以将设计中的缺陷更好地展示出来便于改进。同时多线程编程的条件下对于程序架构的要求更高,需要良好的设计来保证线程安全,因此就有必要使用一些设计模式,例如生产者-消费者模式、单例模式等。
3. 人际网络查询

这个单元我在架构设计考虑的不多,把主要的精力放在了选择数据结构和算法、保证程序的正确性和性能上。我设计了三个类分别继承自官方接口,并在MyNetwork中增加了一个内部类来处理最短路径之外,除此之外其他方法全部由MyNetwork,导致MyNetwork类中许多方法非常臃肿。
我认为比较合理的设计是对MyNetwork类进行一定的拆分,设置一个内部类,将与图有关的数据封装在其中。对于比较复杂的方法,例如最短路径查询、强连通查询和并查集操作,也可以设计内部类来存放相关的数据。使用内部类使得每个方法更加清晰,同时也不存在访问权限的问题。
这个单元引入了规格化的设计,通过JML使得我们书写代码的时候针对性更强了,通过规格达成的约束,使得方法的实现者和调用者之间相对都是透明的。书写各个方法时我们无需关心调用者是何时调用、如何调用,只需要保证功能的正确性即可,这样的明确分工也使得开发的过程更加容易。
三、测试理解与实践
第一单元表达式求导的作业中,由于Python中已有表达式求导的库和正则表达式生成的库,因此我编写了自动生成数据和结果比对的程序并利用批处理程序完成整个对拍的过程。个人认为评测机的搭建也是一个相当有挑战性的任务,Python相关库的使用、脚本的书写和调试等等需要从网上检索有效的资料来完成。
第二单元的电梯作业我也写了一个简单的评测机,利用Python的subprocess模块完成定时输出,之后进行正确性的判断。
前两个单元使用的是利用自动评测机这种“暴力”的方法进行成百上千次的评测,而第三单元的测试使用的工具就比较多样了,并且开始转向了白盒测试。除了和前两单元一样使用了自动评测机,我还利用了JUnit这种强大的单元测试工具,这样的测试粒度更细,而且可以检查测试的覆盖率。最后在这单元结束时还使用了JMLUnitNG进行测试,虽然感觉这个工具有点鸡肋。
第四单元由于自动生成数据较为麻烦,我只编写了自动运行和结果比对的脚本,在作业的迭代过程中完成回归测试。 软件测试是一个十分复杂的内容,作业的代码量在千行左右时想要在几天的时间内进行覆盖性测试就十分困难了,因此更加可取的方法还是利用规格规范代码的功能,或是进行一些粒度较小的单元测试。而合理的架构设计会使程序出错的概率大大降低,同时对进行测试也很有帮助。
四、课程收获总结
整个OO课程感觉收获自己的收获和进步都是挺明显的,尤其是在前两单元中,学习的内容比较陌生,作业与课程讲授的内容联系紧密甚至更加超前,难度也比较大,在每周学习和完成作业的过程中收获的提升也尤为明显。后两单元学习的知识理论性比较强,系统地介绍了规格化设计和模型化设计,但是和作业之间的关系较小。
第一单元的前两次作业感觉还是保留了较多面向过程的思维,第三次作业我借鉴了参考设计,设计出的架构就比较符合OO的思想了,作业中也使用了推荐的工厂模式来创建对象。这一单元的进步还是很大的,尤其对Java的基础语法和OO的基本知识有了很大的收获。这单元过后也开始意识到设计程序时要先从整体架构入手,思考后开始写能够提高效率。
第二单元设计的重点在于线程安全,主要使用Java的synchronized关键字来完成同步。课程还重点介绍了一些多线程设计中常用的设计模式,在作业中使用了生产者-消费者模式和Worker Thread模式,实验中还练习了观察者模式,研讨课中也接触了读写锁模式、单例模式等。
第三单元的重点在于理解规格并书写符合规格的代码。这一单元比较关键的地方在于要兼顾规格与程序的性能,在熟悉语法之后阅读JML的规格并不困难,而选择合适的数据结构和算法来完成对应的类就比较复杂了。这单元对性能的高要求也促使我阅读了jdk的部分容器的源代码,包括ArrayList、HashMap和PriorityQueue。同时实验中也提到了JVM的垃圾回收机制,我在空闲的时间也了解了虚拟机的一些额外的知识,虚拟机屏蔽了大量的底层细节,使得Java的使用变得更便捷。
第四单元要求对UML图进行解析,在老师的讲解和对照mdj结构后感觉UML图的理解并不困难,而设计一个良好的架构进行解析就显得尤为重要。还有就是课上重点介绍的模型化设计感觉是一个很重要的技能,虽然现在感觉自己的理解还很有限。
五、改进建议
- 在电梯作业中的,希望课程组能够提供一下可以进行定时输出的脚本。由于实时交互几乎不可能人工模拟,如果没有定时投放数据的脚本很难进行本地测试。而我在写定时输出的脚本时花的时间似乎都超过了花在第一次电梯作业的时间(可能是对Python不太熟悉的缘故)。如果课程组提供这个脚本可以使同学将更多的精力花在多线程的学习上。
- 关于第三单元的整体设计,课程组为了介绍规格化设计使用了JML语言,但是这种语言的相关工具链大都年久失修bug重重,无论是JML语法检查还是基于JML进行测试都比较麻烦。不知道课程组有没有办法选择一种其他的方式来进行训练。还有个人认为方法名的暗示效果过于明显,如果想训练理解规格的能力感觉使用function1、function2这种方式效果更好。
- 还有一些关于互测环节的建议,感觉互测环节对同学的周末生活是一个挺大的折磨,建议课程组允许全部作业有效的同学进入互测,这样就不用在周日中午的时候经历一波煎熬了。还有就是希望能够隐藏互测期间自己被砍的记录,被砍之后很影响心情。
六、心得与体会
感觉线上学习OO课程跟线下相比差别似乎不大,仍然可以体会到与助教和老师的交流,以及每周一次作业的巨大压力。有一说一感觉这门课的整体设计都特别用心感觉是经过反复推敲的,四个单元的课程和作业都是逐层推进的,没有太多重复的内容,让我们不断地学习到新的知识,每次的实验课都可以学到很有用的技能(虽然限时的训练难度波动较大,有些紧张比较让人反感)。暑假期间我也想继续学习一下老师上课时提到的坑,比如JVM虚拟机、多线程的进阶知识等等。想起老师开学初提到OO是一个修炼内功的过程,课程结束后需要学习思考的东西还有很多。

浙公网安备 33010602011771号