面向对象的程序设计-类图统计程序的设计、优化与测试

面向对象的程序设计(2019)第四单元总结

I  此次作业的架构设计

  这两次作业都是基于 UML 类图解析的作业。第二次作业完全拓展第一次作业的格式,因此直接分析第二次作业的类图。

 

  程序核心类为 MyUMLInteraction 类,其中针对三种图的查询实现了三个类,并将对应的元素和指令分发给对应的子类实现。由于检查 UML 类图的合理性需要和 UML 的元素产生很多关联,所以实现中将内容检查环节嵌入 UMLModel 处理部分。对不同指令的查询功能,划分为单一节点查询类指令回溯型查询指令,前者可以简单遍历对应的边表实现,而后者可以利用可变参数的 DFS 结构灵活的获取结果。

  针对作业二,由于图中可以包含多个顺序图和状态图,所以 MyUmlState 和 MyUmlColla 类中针对不同的类图又各自形成一层抽象,来访问正确范围内的元素。通过指令的分发,使得各个模块只需要考虑自己模块的内容,便于 Debug 和模块化。对于状态机的后继状态查询算法,借鉴了类图中的查询算法得以实现。

  对于第二次作业新增的正确性检查指令,分别采取不同的策略检查。重名变量和联系的检查在创建数据结构时动态检查;循环继承的问题使用 Tarjan 的强连通分量检查算法,补充以自环的维护和检查得以实现;多重继承问题采用 DFS 记忆化搜索的方法,利用深度优先的特性将异常继承的关系反向传递到底。

  为了提高算法的运行效率,就要充分利用已有的计算数据,即在深度搜索的过程中使用缓存的方式减少计算。这也引入了对应的观察者类,用来记录和更新数据。

II 历史作业的架构设计

  四个单元的作业,可以说类的规模和复杂程度是逐渐减少的(可能这很奇怪)。但是本质上是对功能和需求的把握更加准确了,即可以用更简单的模型解决复杂的问题,进行更高层次的抽象。

A 多项式求导

  作为 Java 系列面向对象语言的第一课,上手还是有一定难度的。在三次作业中经历了两次重构,经历了设计不合理的诸多弊端。第一次作业很快速的完成了要求, 但是没有考虑到使用工厂模式,保留中间结构供优化等。第二次作业中便被迫进行大刀阔斧的重构,但是由于嵌套的树结构没有很好的建立,在第三次作业中还是重新构建了图结构,便于优化和求导。

  在寻求优化的过程中,经历了简单合并,启发式的寻找配方项,以及最后的按幂划分组,分组按树结构遍历缩短,最后合并的算法。虽然由于没有考虑到输入输出的格式差异导致失分,但是整体效果比较好。

  在这次作业中,涉世未深的我在开始写代码前没有系统的了解 Java 的内存管理和对象管理机制,导致在创建图节点的合并,修改和复制时遇到了不少的问题。在问题的驱使下我才开始查找相关资料了解学习。同时因为不会熟练的重载 Object 类的相关 equals(), hashcode() 函数,使得第二次作业的判断十分繁琐。在系统的了解时候,显著的提高了程序编码的效率。在进行压力测试时发现树结构的启发式搜索会导致超长表达式的超时,所以被迫在程序内部添加计时,利用时间强行结束过深层次的树结构,避免超时。

B 电梯调度系统

  吸收了上次的教训,我编码前仔细研究了 PPT 里面的内容细节,创造了分派器-调度器-电梯的结构。在第一次作业中增加了不少的代码量,但是第二三次作业变得无比轻松。面对第三次作业的复杂算法,在设计时在调度器中添加了统一的算法接口,同时调度器对每个电梯的运行时间和目标位置有预估,提供算法充分的空间进行计算。在算法的实现中,我采用了可变边权的 Dijkstra 算法,通过设计启发式函数调整每一个边权的取值,可以局部贪心使得每一个电梯得到最大化的利用。

  这次作业也充分考验了设计和复杂度之间的关系。我没有考虑到 Java 自带实现中的线程不安全因素,滥用了 ArrayList 导致强侧时候的惨剧。较为神奇的是在本地上万次测试的过程中都没有触发的潜在问题竟然在强侧时出现,让我非常惊讶。同时在设计多线程过程中,仅仅使用了 synchronize 块控制的方法进行同步互斥的处理,其实可以用 lock 等实现更清晰的逻辑。同时由于线程间的协作设计不合理,导致我需要在电梯结束任务后进行一定程度的轮询来结束其他电梯的工作,增加了 CPU 时间。

C 地铁调度系统

  这次作业的难度会相对较低,但是思维量仍旧比较大。JML 语言的灵活和抽象性,可以保证设计和实现过程相分离。这是我之前从未想过的绝佳优势。准确理解规格的基础上,将图结构与代码分离,实现自由的计算,缓存以及更新。面对不同的需求,我之前的复杂结构给编程带来了不小的困难,所以果断重构,利用通用的图算法加上重载内部函数,实现不同权值的计算。这次作业从很大程度上借鉴了前一次作业的结果,较为得心应手。

  这次作业最大的问题是在编码之前没有确认好对应的算法和时间复杂度。最初实现的拆点算法和加边算法都被证明时间会超出预期。究其原因,还是自己没有规划好相关的蓝图。在实现了理论可行的算法后,发现自己的时间仍旧比较长。在辗转多个文件的 Debug 后,终于确认是在运行 Dijkstra 算法的过程中忽略了已访问节点的移除,导致时间复杂度达不到预期结果。在编码前还应该多思考,明确蓝图和细节,才能写出高质量的代码。

D 类图解析系统

  到了这次作业,可以看出前两次作业都是和图密切相关的了。相关的主干逻辑和辅助类分离的思想也可以沿用。这次作业反而没有涉及太多互相交错的类,因为大多数逻辑都在内部的细节中得以实现。对于三种不同需求的查询,可以通过关联的形式,通过分派类进行分派,简化业务逻辑,降低模块间的耦合度。

  这次作业也借此机会让我了解到了类图的组成,时序图和状态图的设计及内部关系。我认为这一层抽象是凌驾于 JML 系统之上的,可以完善整个抽象体系的存在,对于我们对面向对象的设计思维有指导性的作用。

III 历史作业的测试设计

  老师说,再精巧的设计都离不开全面的测试。这十四次作业的测试,一次都不能少。前三次的测试程序都在之前的博客中详细提到,所以这里只做一个简要的概括。

A 多项式求导

  初出茅庐,编写的测试脚本比较年轻化,架构也较为简单。考虑到 Python 处理长表达式的劣势,所以选择使用 Mathematica 提供的 Wolfram Script 来验证结果。最初的实现中,采用概率的方式碰撞合法的表达式,这种方法概率很低,而且很难达到设计案例测试的目的。所以我使用了正则表达式生成合法的算式,辅以多个人为设计的压力测试案例,进行时间边界的测试。但是恰恰是这种测试架构,导致了我的输出不满足题目的表达式要求。因为盲目测试输出值的正确性,却忽略了输出结构组织的正确性。虽然可以正确计算出结果,但是括号的要求确实错误的。

B 电梯调度系统

  设计测试之前,首先要保证代码的逻辑正确性。通过对单个电梯不同的状态进行分析,确定了单个电梯行为的正确性。对于多个电梯的情形,考虑电梯状态的组合,不同情况下对电梯的并行情况进行分析,减少死锁等错误的发生。

  电梯调度测试系统的输入可以通过一定规则随机生成,压力测试集也较为容易的得到。因而设计了可以计算时间的 Python 脚本,从文件接受输入,执行/检查错误并将结果输出到文件中,实现闭环检查。

C 地铁调度系统

  这次作业在 JML 的加持下变得规范了许多。测试的检查也可以使用 JUnit 对 JML 所描述的每一条规则进行检查,保证结果完全符合预期。JML 的一些规格检查工具在这里没有办法正常运行,所以目前只能手动构造案例测试。为了验证程序的复杂度和时间,还是设计了互测,对运行时间进行计时。

  这次作业的测试开始变得有难度起来,难度点不在于输入的生成,而是在于结果的正确与否不便于检查。于是,结合大家的力量,决定开启对拍测试模式。修正上次实验的测试脚本,改为可以多人并行运行,计时、统计内存占用并进行比对。

D 类图解析系统

  到了这次作业,测试的文件可以通过给定 MDJ 文件的方式生成。随着测试系统的完善,决定提供一个通用的测试平台,供大家参与测试并可以得到结果。想到做到,利用 Github Pages 搭建网站,利用 Github 仓库托管所有测试的文件和待测试的 JAR 文件,可以动态的公布结果,实现如同角斗场一般的测试环境。项目地址:https://github.com/BXYMartin/OO-Public/

  在测试机的实现中,想要准确监控进程的 CPU 时间,内存占用等信息,还是花了不少心思的。利用 Python 的 Subprocess, resource 包,resource.getrusage() 方法获得运行时间,内存占用等信息统计输出。

  为了方便同学参与互测平台,在 Github 上采取 Merge Request 的方式更新测试文件和测试代码。测试结果通过 Git Submodule 的方式更新到本仓库,通过 Github Pages 动态显示在前端,避免使用服务器搭建网站的复杂和危险性。

  在经历版本迭代后,可以根据错误进行分类,Runtime Error 一类可以确定错误归属的错误会动态生成个人姓名的标签便于查找,对于结果出现冲突的,则标记为 Conflict 来等待解决。为了方式错误较多炸网站一类的事情发生,采用 Github API v3 更新网页,通过子模块 Token 的方式保证了网站库的安全,同时完善了更新机制。

  为了让 Conflict 冲突型任务更好的分发,还开发了踩人功能,对于不方便认领的任务,可以调查民意得到结果。

  在一个稳定运行平台和同学们的热情支持下,测试集得到了充分的丰富,测试集也得以正常运行。全过程中,有因为测试集产生的误判,也有真正找到的运行问题。和同学们一起讨论相关设计,一起寻找问题,抽象思维,共同进步,无疑是我在面向课程设计中最怀念的时光。

    

IV 课程收获

  从一开始在面向对象程序设计中一脸懵的自己,到现在可以熟练抽象建模,都是经过 OO 历练的成果。与其说 Java 是一门语言,不如说他是一种思维模式的代表。通过灵活的依赖,继承等关系,发展出依赖反转之类的高级使用方法(感谢苏苏的教导)。从一开始不太懂得继承和各种设计模式,到自己误打误撞的使用了一种却不自知,直到老师上课时谈到这才知道,到最后可以了解他们的用处。这点滴的收获都是在每周作业的精心设计之下训练出的能力。

  这次 OO 生涯的体验可以说非常好了。从一开始的代码风格指导书开始,每一次指导书都非常用心,最重要的是很少修改,这个真的很重要。助教组为了给我们提供一个统一的环境,包括网站、评测机和众说纷纭的论坛,组织了很多代码统一接口,处理输入输出和解析等等,代码量也是非常之大了。代码风格在十四次任务里逐渐从检查变成了习惯,控制代码复杂度,逻辑复杂度,行数和方法规模,规范驼峰命名法...这对于大规模项目真的非常重要,是一个很好的习惯。

  这次生涯对我的历练更多的确是在编写代码前的构思。对系统结构的设计,算法的选择,具体数据结构的选择,最终的结果预期等等,甚至养成了指导书发布第一天代码丝毫不动的习惯。血的教训告诉自己,没想好就编码绝对火葬场。结构模式设计失误会导致重构到吐血,细节的不小心也会酿成大祸。还有就是之前只存在于教科书里的算法,这次竟然这么灵活的在实际问题中使用起来,是一次真真实实的复习。

  最真实的收获是同学之前思维的碰撞。每个人设计测试案例都有自己的盲区,而同学们集思广益,共同分享想法和设计,真的是最有效的学习方式。设计互拍平台,让大家的想法得到集中,并且真正的应用到代码的检查上去,是我作为一个 OO 玩家最乐于看到的事情了吧。在这里感谢为测试集、测试源无私贡献的同学们(这里就不提名字了,谢谢大家!),是你们让在线互拍平台变成了可能,让我们可以一起进步。

V 改进建议

A 未来需求多给点提示

  感觉在前几次任务中,盲目猜测未来的发展方向会给代码带来一些冗余,指导书中给的代码提示可能也不够足以筛选一些比较偏的方向,我觉得划定一个大致的范围更有利于架构的设计。

B 更多的在课上分析作业的设计模式

  助教共享给我们的库中有很多非常优秀的设计模式,我认为在课上对这些设计模式进行总结,把书本中的知识应用于真实的项目中会更加有说服力,也会更有利于应用。

C 讨论区加个关键词/帖子修改功能

  随着帖子越来越多,有的时候会扎进评论里走不出来...不知道可不可以给帖子加个关键词之类的,便于查找。以及...帖子发错了不能删除比较难受,虽然能理解助教们是怕删帖无对症 Orz

D 更明确的要求

  第四单元作业感觉在要求上不是特别明确,导致我在前期处理了很多不必要处理的情况,反而增加了代码的复杂度。

E 研讨课...?

  作为高工的学生,看到计算机学院官网上写的公司大牛前往参加 OO 研讨课,分享课程设计和项目实战经验,整个人都懵了,为啥我们木有这种机会哇...TAT

posted @ 2019-06-21 16:29  BXYMartin  阅读(595)  评论(0编辑  收藏  举报