小黑123

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

OO第四单元总结 & 整体课程总结

第四单元总结

一、单元概括

​ 本单元的三次作业迭代,最终实现了解析类图、时序图、状态图三种 UML 图,并进行八条合法性检查后,支持诸多查询功能的分析程序。通过本单元的设计与构造,我们学习到了 UML 建模语言的基本知识,并完成了小型综合架构及算法的实现实践。由于三次作业属于无需重构的拓展递进,以下只对最终版本进行分析。


二、架构设计

1. 类图

  1. Class Diagram 部分

    画完发现免费版 StarUml 的水印影响观感(不全是因为犯懒嗷),以下用 IDEA 的生成工具代替。

  2. 顺序图部分

  3. 状态图部分

  4. 部分与整体


2. 架构分析

​ 由以上类图可清晰地见到,我的主解析器由三个单解析器聚合而成,单解析器分别负责类图 (UmlModelAnalyser) ,时序图 (UmlCollaborationAnalyser) 和状态图 (UmlStateChartAnalyser) 的解析工作,而主解析器只是将读入的 UmlElement 和查询指令依类别分发给三个单解析器,完成构造和查询功能。在实现时,我惊讶地发现,官方包 ElementType 枚举类中,已经将元素按三种图的类型分好了类,看来冥冥之中自有天意,人间正道是沧桑!

​ 整体流程如下:官方 Runner 启动解析程序,将元素列表传进 MyUmlGeneralInteraction 构造器,后者将元素分发给三个单解析器的构造器,全部构造完成并返回后由官方 Runner 类自动触发八条规则检查,通过后进行指令查询。主解析器转发请求给子解析器完成,并获得结果。

对于单解析器:

  • 由于查询操作为按名称查询,故需要一个以 name 做 key,对应查询元素为 value 的映射表,选择 HashMap 即可。
  • 由于内部运算逻辑统一地用 id 建立元素之间的联系,故需要一个以 id 做 key,对应元素为 value 的映射表,同样选择 HashMap 即可。为维护逻辑的直观,我选择不将所有元素统一放入一个 Map,而是一类元素一张表。

对于元素:选择适宜的容器存放自己拥有的信息即可。如 Class 存储方法、属性、父类、接口、关联对端等。一处小设计为,当添加元素进 nameMap 时,若已含有该 key,说明名称重复,如果查询时需要抛异常,可直接将该 key 的 value 改写为 null,表示该 name 重名。

一些特殊的自建类:

  • OperationManager:

    ​ 一个类可以有重名方法,在查询时,返回所有该名称的方法的可见性、参数列表等。为降低逻辑耦合和复杂度,创建该类以管理一个类中同名的所有方法,对 Class 提供查询接口

  • TransitionManager:

    ​ 与 OperationManager 是一样的考虑。两个状态间可能有多个迁移,一个迁移又可能有多个 Event。该类管理某两个状态间的所有迁移,对 State 提供查询接口


三、 关键算法设计

  • 单解析器的构造

    ​ 类图需要解析并存储的信息有 UmlClass, UmlInterface, UmlGeneralization, UmlInterfaceRealization, UmlAttribute, UmlOperation, UmlParameter, UmlAssociation, UmlAssociationEnd。需要注意的是,这些元素的添加往往有着先后顺序的要求,例如:要将一个 Operation 添加到一个 Class 中,首先要认识这个 Class 。这就意味着将 Operation 元素添加进 Class 的操作,一定要在解析过所有的 Class 后才能有效进行,而 Uml 元素的出现顺序是不保证的,故需要先对所有元素进行一轮识别 Class 的遍历,才能继续做添加 Operation 的操作。(有点啰嗦了sry)

    ​ 于是,通过对传入元素的多次递归,即可安全且正确地完成初步(静态)构造工作。每一次递归,可将互相无先后顺序要求的一批操作一起完成。简单分析,得到设计如下:

    ​ setUpStep1: 填充 classIdMap, operationIdMap, interfaceIdMap, associationEndIdMap。

    ​ setUpStep2: 应答 UmlAssociation,将关联两端的类互相存储进对方;

    ​ 应答 UmlGeneralization,将父类信息存储进子类;

    ​ 应答 UmlInterfaceRealization,将实现接口信息存储进类;

    ​ 应答 UmlAttribute,将属性添加进类或接口;

    ​ 应答 UmlParameter,将参数添加进方法;

    ​ 应答 UmlOperation,将方法存储进类或接口。

    ​ 至于上述所谓静态构造,是指只建立起元素间的直接关系,每个元素需要知道的更多信息(如祖先类、可达状态等),依靠查询时的动态构造。后文会有说明。

  • 类图部分

    • 父类递归

      ​ 在考虑继承的情况下,一个类需要知道其所有祖先的信息。由于 UML 为静态模型,即在查询过程中 UML 信息不会发生变化,故解析器存储的各元素信息同样为静态。因此,我采取的设计是空间换时间,通过冗余存储,使所有查询指令趋近常数级 O(1) 。具体地说,就是在 Class 类中设置属性 hasAdjustedClass,表示该对象有无进行过类递归,若无,则继续对父类调用递归函数,一路向上直到某类的父亲做过递归(或是无父亲时),递归结束,将父类的信息存储进本类,将本类的 hasAdjustedClass 置为 true 并返回。这样,若某个类的 hasAdjustedClass 为 true ,表示当前类的信息是 “完备“ 的,即该类已经存好了包括祖先在内的所有信息,可直接从本类获取信息并返回查询结果。值得一提的是,此时该类直到其顶级父类的一条路径上的所有类,都已经做好了递归调整,即全部 ”完备“ 。

    • 父接口递归

      ​ 与上思路基本相同,只是由于接口的多继承,简单的 ”递归“ 变成了有向图的深度优先遍历。

    • R002 循环继承

      ​ 分为两部分——类的检查和接口检查。

      ​ 对于类,遍历每个类元素并以之为起点进行类图遍历。由于其单继承特性,可以在类图遍历的同时记录其路径,并在发现环时,将复数个类节点同时标志为重复继承。具体地说,当访问的节点 A 在栈中时,表示碰到了环,开始退出遍历并弹栈。不难证得,在 A 之前弹出的节点一定在环上,即有循环继承。

      ​ 对于接口,遍历每个接口元素并以之为起点进行接口图深度优先遍历。由于其多继承特性,以 A 为起点时,考虑 B 是否在环上是困难的。安全起见,以 A 为起点时,只考虑判断 A 是否有多继承,即会不会在图遍历过程中重新遇到 A 。

    • R003 重复继承

      ​ 同样不难证明,重复继承只会出现在接口子图中。为充分发挥架构的威力,我并不在此处设计新的图算法,而是使用已有的 ”父接口递归“ 。Interface 中存储祖先的容器 allAncestor 为 HashSet,不会存放 equals 的对象。正是使用这个特点,可简便地实现重复继承的判断——一个接口,添加其所有父亲的祖先到自己当中之前,allAncestor 的大小记为 A,每个父亲的祖先的个数代数和记为 B,在 allAncestor.addAll() 之后,allAncestor 的大小记为 C。若 A + B > C,则说明该类发生了重复继承!另外,如果接口的某个祖先为重复继承,则该接口可直接判断为重复继承。将上述操作添加进接口递归函数中,在 R003 检查方法内对全部接口分别触发一次递归,即可直接获得结果。

    • R004 重复实现

      ​ 与 R003 的设计思路相同,触发递归并通过计数关系,获得是否重复实现的判断。但该判定算法需要注意,添加父类的实现接口时,只能添加该父类本身与其所有祖先类直接实现的接口!否则,在以下情况会出现问题

      ​ 当触发 Class2 的递归时,若 Class1 已经被递归过,会发生 Interface2 的重复添加。当然,这种细节处的 bug 需要结合代码详细分析,才能说清楚。每个人的实现方式不尽相同,以上只是我踩到的坑,可能没啥参考价值。

  • 状态图部分

    • 可达状态

      ​ 同样地,遍历的同时存储。在有向图遍历时添加优化,若某状态 A 的某直接后继 B 已进行过遍历,则直接将 B 的可达状态加入 A 的可达状态中,不对 B 调用递归遍历。

  • 协作图部分

    没得啥子要说的。

  • R001、R005 ~ R008

    ​ 这些规则较为简单,可在静态构造的过程中得到判断,而无需更多的额外步骤。


四、测试

​ 手画一些有针对性的 StarUml 图就基本够用了。结合自己的代码白盒画图,导入导出,帮助我修了一些大大小小的 bug。

比如乱七八糟状态图。

乱七八糟接口图

(刚意识到导出有水印,截图不就没有了。。。)


OO课程回顾与总结

面向对象太有意思了!这里的助教也耐心,课程组的老师也用心,同学们又个个都是人才,我超喜欢这里的!

​ 这学期是我编程能力提高最大的一学期。在有意义的代码量堆砌的过程中,我关于测试与性能、问题需求与程序框架的理解,都有了质的突变。这也是我生涯以来,敲的代码和实际问题距离最近的时候,可以说是在学 “接地气儿” 的技能了。下面从几个方面具体说说我在各个单元中的收获。

1. 架构与面向对象

​ 第一单元为 java 面向对象基础——多项式求导。那时的我勉强算是会用 java,但也只是懂得基本语法,至于面向对象的思维,是稀里糊涂一窍不通的。记得当时,由于不懂得使用面向对象的多态特性和接口的规约设计,导致各个函数与加法、乘法的求导方法规格不统一,使用起来也要做很多冗余杂乱的 instanceof 判断和向下类型转换 ,以调用子类特有的方法。总之,现在回看起来,那时的代码就是垃圾山,毫无优美的架构可言。搞笑的是,那还是我在第二次作业时重构过的代码。啊,好二啊。

​ 第二单元为多线程——电梯问题。老师讲过,高并发性是现代软件的基本属性。刚接触时觉得听起来如此高深,实际实践起来发现——还真挺高深的。不过就基本内容而言,掌握线程状态切换的时机、临界资源互斥访问的方法、wait/notify 编程实现线程同步配合的基本准则,就足够进行编程尝试。结合操作系统中关于进程切换与信号量通信的内容,我在多线程部分的基础知识掌握得还算自认为挺不错。

​ 另外,我觉得重要的一点,还在于对象与引用的理解加深 。在之前的 C 语言程序设计中,变量在声明时就显式规定了其为变量本体,或是指向另一个变量的指针,抑或是指针的指针,所以使用起来不用思考很多。而且,其声明即构造。但是 java 不一样,除基本数据类型外,一切皆引用Object a = b; 不是开了两块内存,而是做了一处共享。这样显然的道理,当时的我并不熟悉。多线程编程对于共享对象的思考,是协调并发设计的核心所在,于是在共享对象的寻找过程中,我逐渐形成了习惯——随时思考对象有无共享。目前的知识告诉我,对象只有在调用构造时,才会被创建出 “新” (即 new,顾名思义),而没有调用构造时,使用的一定是同一个变量。也正因此,第四单元的 R003 将元素加入HashSet 之时,才会被没有重写的 equals 方法判断为真——其地址相同,为同一对象实体。实际上,正是因为忽视对象共享,我在第一单元时才会被一处 bug 搞得一时间怀疑人生。第二单元我的架构还是比较清晰的,难点在于线程同步,因此不再赘述简单的生产者——消费者模式。

​ 第三单元为 JML 规格——人际关系网络。本单元的官方代码和 JML 要求,已基本把架构搭好。该单元的学习中,我学到了无二义性的交流语言的力量。其严谨性的需要,使得 JML 等规格语言像公理系统一样,大家共同认可一套基本语法和语义,并使用这类语言符号的组合,以精准描述规约内容。它的作用,在理论上,为验证程序正确性提供了名为形式化的手段,即数学验证;在实践上,也协调了框架设计与细节实现之间的安全性、一致性,即倡导了 “契约精神” 。JML 在对安全性与严谨性有极端严格的要求时,使用性价比是最高的。

​ 第四单元为 UML 图——三类图的解析。经过一学期的训练,我的依照实际问题考虑性能,并搭建合适架构的能力达到了生涯新高(因为以前从来没有,哈哈)。本文的第一部分可以看到我的架构类图,十分层次分明、对称,看起来有种难以言表的舒服和美观——可能这就是好的架构的一种表现?也有可能是我审美水平太低了,其实不咋地但看着也当成了好东西。这单元的作业与第一单元有些相像,都是单线程的从无到有设计框架并构造出来的实践。同第一单元相比,本单元多了一些算法设计,但写起来心里反而顺畅了不少。究其原因,就在于好的架构。架构漂亮,写起来就会头脑清醒,知道自己正处在整体的什么位置,将要为整体做出什么样的贡献;而架构糟糕,就如我的第一单元,会让人写着写着晕头转向,不知道我是谁,我在哪,我该干啥。可见,设计的重要性!


2. 测试理解与实践

​ OO 来之前,测试于我而言就是代码能跑了,带进去俩数试试,交上去之后测试点过了,就万事大吉;OO 走之后,我终于明白,测试 yyds!

​ 第一单元,我还停留在 “带进去俩数试试” 的阶段,导致结果惨不忍睹——我低估了自己犯愚蠢错误的能力。但凡做一些有针对性的测试,也不至于惨败成那样啊hhhh。也是那时开始,我知道了中测有多不可靠,还是得多做测试,才能减小错误保留在代码里的可能性。提到针对性,可能在互测时疯狂加括号 hack 爆栈的兄弟,算得上是我的 ”针对性测试初体验 “ 了吧,啊哈哈。

​ 第二单元,感谢讨论区同学的测试方法分享,让我在没学相关 java 语法的时候,就能使用定时投放输入的功能。这一单元开始,我对测试的重视程度提高了起来,通过中测后,仍然抱着 ”我的代码没测试过的话,就是不可信的。既然是我写的代码,那极有可能有问题“ 的信念做足测试。最后的结果比第一单元好了很多,只不过仍有部分死等问题没能测出来,是小小的遗憾。

​ 第三单元,我开始学习使用那无敌的 Junit 单元测试工具。不用不知道,一用吓一跳——是真的好用。单元测试,就是通过编写针对性的测试代码,把各个方法进行尽可能大覆盖范围的测试。这是我第一次将测试工作正式编码化,体验还是很不错的,绿色的对勾看着很爽,但是红色的叉叉往往更让人兴奋——毕竟在提交之前找到 bug 了是件好事。事实上,经过几次的死活找不到 bug 以至于抓耳挠腮空洞无望之后,才发现,找到 bug 这件事已经可以让人无比喜悦了。

​ 第四单元,StarUml 画图进行测试,依然是做针对性的测试,成果还可以接受。只是最后一次作业的 R004 ,由于使用的是不同常规的自己瞎琢磨的算法,导致一处细节问题没有考虑到。其实这个情况不是很难测到,但确实是疏忽了。小小的遗憾,不过没关系,我的心境已经不再是 ”追求完美“ 了。不完美,但也挺不错。


3. 课程收获

​ 如上所述,我确实是收获了好多。包括编码能力、架构设计、面向对象思想等专业技能,也包括时间紧张条件下完成任务的抗压能力。但更重要的,我想是本领超群让人佩服的助教们,教学超级高水平、用心可敬的老师们,和奋斗在同一阵线的所有同学们。如果没有和我共同探讨、交流架构和测试的设计思路的我的好朋友们,没有讨论区和群里分享 debug 经验的同志们,就不会有我今天的应该还算不太差的成绩。感谢你们,感谢学习氛围积极向上的 23 & 6 !能和你们在一起学习,一起进步,是我的幸运!


4. 一些小建议

  1. 刚开始的第一单元,对于面向对象启蒙的同学们,难度确实是有些大。即使成功啃下来了,也极有可能是像我一样丑陋地、狼狈地啃下来,代码也只是能运行,很难有漂亮的架构设计可言,毕竟对于面向对象思想和高代码量作业的理解程度都处于不成熟的时期。草草啃完,收获是挺大,但如果能在有一些架构设计经验的时候再来做,收获可能会更多。可以适当增大假期 pre 对于架构设计和面向对象思想的讲解和训练难度,以更好地适应第一单元。
  2. 第三单元的 JML 部分,课下作业均为依照 JML 实现代码,可以增加一些按照给定代码写出正确 JML 的作业任务。在课上实验部分确实有此类练习,但客观来讲,课上实验的难度可能不及课下作业的五分之一,对于 JML 的两方面练习量不均衡。当然,考虑到 JML 的难以测试,需要再想一些测试的好方法,比如同学分享,对他人 JML 提出挑战等。
  3. 第四单元的 UML 图,感觉重点有些偏离,倒是更偏重 ”解析“ 而非 ”UML“。在画类图的过程中,我发现,即使第四单元的作业架构和算法设计得很不错(这确实也是一项颇有意义的训练,但似乎和 UML 关系不大。。更像是借 UML 的场景设计程序,而非学习 UML),但依然不能熟练地画好类图。希望在第四单元的训练中,可以加入同学们动手画图的环节。当然,可能也是难以测试的,需要再想办法。

此致,

​ 敬礼!

posted on 2021-06-26 20:10  小黑123  阅读(105)  评论(0编辑  收藏  举报