2020面向对象设计与构造 第四单元 & 课程 博客总结
面向对象设计与构造 第四单元 & 课程 总结
第四单元架构分析
第四单元是笔者感觉自己写的最为OO的一单元,使用代理模式将已有的类进行封装,添加需要的属性与功能。
UML类图
这一单元的java文件太多,没有手绘类图,而IDEA导出的类图清晰度欠佳,于是列举了最后一次作业的文件树来辅助展示架构。
`-- homework
|-- MainClass.java
`-- uml
|-- MyUmlClassManager.java
|-- MyUmlGeneralInteraction.java
|-- MyUmlInteractionManager.java
|-- MyUmlStateMachineManager.java
|-- checkrule
| |-- DuplicateGeneralizationException.java
| |-- DuplicateRealizationException.java
| |-- FinalStateOutgoingException.java
| |-- InitialStateMoreThanOneOutgoingException.java
| |-- InterfaceAttributeNotPublicException.java
| |-- LoopGeneralizationException.java
| |-- MyUmlRuleException.java
| `-- MyUmlRuleExceptionType.java
|-- collection
| |-- MyCollection.java
| |-- interaction
| | |-- InteractionCollection.java
| | |-- LifelineCollection.java
| | `-- MessageCollection.java
| |-- statemachine
| | |-- StateCollection.java
| | |-- StateMachineCollection.java
| | `-- TransitionCollection.java
| `-- umlclass
| |-- AttributeCollection.java
| |-- ClassCollection.java
| |-- InterfaceCollection.java
| `-- OperationCollection.java
|-- element
| |-- MyUmlClass.java
| |-- MyUmlInteraction.java
| |-- MyUmlInterface.java
| |-- MyUmlStateMachine.java
| |-- interaction
| | |-- MyUmlEndpoint.java
| | |-- MyUmlLifeline.java
| | `-- MyUmlMessage.java
| |-- statemachine
| | |-- MyUmlRegion.java
| | `-- common
| | |-- MyUmlFinalState.java
| | |-- MyUmlPseudostate.java
| | |-- MyUmlState.java
| | |-- MyUmlTransition.java
| | `-- transition
| | |-- MyUmlEvent.java
| | `-- MyUmlOpaqueBehavior.java
| `-- umlclass
| |-- MyUmlAttribute.java
| |-- MyUmlOperation.java
| |-- parameter
| | `-- MyUmlParameter.java
| `-- relation
| |-- MyUmlAssociation.java
| |-- MyUmlGeneralization.java
| |-- MyUmlInterfaceRealization.java
| `-- end
| `-- MyUmlAssociationEnd.java
`-- framework
|-- Aggregable.java
|-- Directable.java
|-- Filable.java
|-- Messagable.java
|-- MessageSortable.java
|-- MyUmlElement.java
|-- MyUmlStates.java
|-- Typable.java
`-- Visiable.java
架构设计
代理模式
使用抽象类MyUmlElement
,去掉了原本的属性parentId
。其余所有UML图的元素继承自该类,并具有相应的UmlElement
属性。面对需求对相应元素的属性和方法进行添加,因此具有较好的可扩展性。
接口设计
抽象出了元素的共性,如UmlClass
和UmlInterface
,都是顶层的java文件,实现Filable
接口。而具有Visibility
属性的元素使其实现Visiable
接口,便于添加对所有具有可见性元素的询问。
容器设计
由于所有的查询都基本是基于name
字段,且很可能出现同名情况,因此设计泛型容器,以name
作为Key,Set
作为Value实现对所有同名元素的访问。采用迭代器的模式,避免外部对容器内的元素进行修改。
异常设计
官方的异常类不能按照发现一处抛出一处的方式最后进行合并,遂自建异常类,在顶层的Manager
中捕捉并记录发现的异常类型。其中\(R005\)到\(R008\)都可以采用边解析边抛出异常的方式,而\(R001\)到\(R004\)则只能在顶层管理类中进行检查,每发现一处错误就抛出相应的MyUmlClass
或MyUmlInterface
,最后转换为官方异常抛出。
笔者认为这一部分的处理不是很理想,改动的地方较多。
顶层管理
三种图建立三个管理类,分级进行元素解析,并提供查询部分元素的接口方法供官方接口的实现类调用。
应用的算法
本单元对时间性能要求不是很严格,秉持着求稳原则,图操作全部使用BFS,但由于查询的结束条件不尽相同,最后没有专门建立BFS类来统一处理。
- 循环继承:本质是有向图是否存在环路的问题,逐点BFS,搜索回自己即说明该点在环路上。BFS兼记录路径的方法风险较高,因此没有使用。关于类的循环,由于一个类最多有一个父类,可以使用栈,将访问的类依次入栈,如果访问到已标记的结点便出栈到该位置整合后抛出自定义异常。
- 重复继承:逐点BFS,如果通过两条不完全一样的路径可达同一点说明存在重复继承,也就是每次访问的结点不能存在于已访问结点集,判重问题。
- 重复实现:先将类和其所有父类入队,然后处理方式同重复继承。
量化分析
本单元虽然有较多的类,但很多代理与容器并没有什么实际价值,只是为了扩展的方便性而设计,复杂度确实非常理想。
高复杂度主要出现在条件逻辑较多的方法和BFS。
method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Total | 357 | 431 | 492 |
Average | 1.32 | 1.60 | 1.83 |
class | OCavg | WMC |
---|---|---|
Total | 476.0 | |
Average | 1.77 | 10.13 |
对面向对象方法理解的演进
经历了一学期的风风雨雨,从Java入门,到表达式的求导,从多线程的安全问题,到电梯评测脚本搭建,从Tarjan图论算法,到UML系统学习……笔者一度认为本学期的学习重点更侧重于构造和设计,但面向对象的思想,其实已经在无形中影响起了笔者的编程习惯。
第一单元
本单元作业的主题是实现表达式的求导,另外要附加对输入进行格式检查的功能。第一次作业经历了好几次的重构,后两次作业在架构的基础上完成得比较顺利。
这个单元是笔者看来最为困难的一个单元,难点如下:
- 笔者本身对于Java不够熟悉,只经历过寒假作业的训练。
- 没有良好的架构设计思路,绞尽脑汁思索如何写得OO。
- 难以驾驭数百行的程序,先前写得最多的代码量也都控制在200行以内,而是还是C语言。
笔者总结了初次接触OO的两点不成熟理解:
- 尽可能地将类封装起来,使其成为完全独立的个体。
- 多利用Java比C和C++语言多出来的优势,减小编程的难度。
而问题其实也很明显,编程的思维完全被禁锢,进而导致了部分过于简单的问题解决得十分复杂且低效。
比如第一次作业只涉及到简单的幂函数求导,其实百行左右就能解决问题,但是笔者为了应对传闻中的三角函数与嵌套因子,设计了多个类来完成,这样对程序的可扩展性是一种提高与把控,但对于如此简单的需求本身而言,是完全没有必要的累赘。
再比如之前的寒假作业,要入门正则表达式,笔者从Task0开始就直接将全部字符读入后用正则进行处理,实际上前几个Task用C语言的字符串简单处理就能实现。
因此反思,在程序设计时要从需求入手,杜绝小题大做,面对骤增的需求量时,再采取重构等策略应对。
第二单元
本单元主题是多线程程序设计,使用多线程模拟多部目的选层电梯的调度。这一单元更侧重于高并发、减少线程安全问题,而对OO理念涉及的不多,不过面向对象仍然是至关重要的一环。
- 编程时要谨小慎微,避免出现死锁与暴力轮询。但谁也免不了出现线程安全Bug,因此密集的数据测试也是主角之一。
- 线程安全问题固然很重要,但是编程时仍要兼顾架构的稳定性。
第七次作业写得很自闭,因为先前直接使用了官方提供的PersonRequest
类,导致内部不能存储换乘的信息,要重新造轮子,修改方法的参数类型。在设计对象时,可以使用代理模式,将已有的对象信息作为新对象的一个属性,在后续需求增添时可以直接向新的对象中增添属性或方法实现高的可扩展性。
另外在设计对象时要尽可能减少对于内部属性的改变,因为一个改变可能会影响到引用了该对象代码段执行的正确性。
第三单元
本单元主题是契约式编程,重点在于依照JML规格实现相应接口的方法,没有过多地体现面向对象思想。
因此这一单元对OO的理解是没有什么加深的,反倒是复习了数据结构中比较经典的基础BFS、DFS、Dijkstra算法,简单的记忆化搜索,学习了Tarjan点双连通分量的实现,了解了很多容器间的查询、遍历、插入、删除的效率关系,对时间性能有了很好的把控能力。
第四单元
本单元主题是UML模型化设计,更偏向于理论的介绍,但这单元的作业使用OO的方法来完成感到信手拈来。
-
常用设计模式必然有其精妙绝伦之处,在设计时要多多纳入考虑。
-
多学习Java语言的特性对于简化编程的难度有很大帮助,本单元的三次作业可以使用泛型、函数式编程等。
经历了最后一单元,笔者对OO也有了更深刻的认识。面向对象程序并不是要完全地实现“模块化隔离”,而是综合了功能需求、可扩展性、鲁棒性等种种因素后诞生的最接近理想的产物。
面向对象将数据与方法封装为一个整体,是一种理念和方向。我们不可能写出没有任何耦合的代码,不可能将多个子类的共同特性一次抽象出来,更不可能做到精准预测未来的所有需求而从来不对代码进行重构,甚至Java自身的轮子也时常存在不完美的地方。
因此,身为一名编程者,我们只能尽力向理想化的面向对象前进、靠拢,这也正是面向对象编程无尽的魅力所在。
对测试方法的理解与实践的演进
笔者前文也提到,面向对象设计与构造这门课程,能够给求学者带来的绝不止面向对象。更多的乐趣其实都是在对作业不断的探索与测试中获得的。
笔者本学期作业一共炸了三次,其中两次都存在测试做的不足的原因。
第一单元
第一次作业由于难度本身不高,涉及到的边界情况较少,笔者在重构完成后没有进行什么测试便提交且安稳通过强测。
带着这种心态,第二次作业便翻了车。经历了好一番优化后,对于三角函数的化简出现了合并系数失真的问题。同时还发现了对指导书要求理解疏漏,WRONG FORMAT!
误判的Bug。也就是一次作业爆了两个Bug!
还好翻车翻得早,不然后面的单元笔者恐怕还懒惰于启动评测机的开发与使用。
第三次作业笔者使用了(白嫖了)评测机生成样例并验证自己的与互测屋内成员的结果,强测稳住了阵脚,并且在互测取得了不错的收益。
总得来说,这单元带给笔者的收获就是,一定要多对程序进行测试,有很多时候充分测试都不一定能够发现一些隐秘的Bug,而不测试的后果更可想而知。
第二单元
第二单元的公测和互测没有被测出线程安全问题与正确性问题,但这也并不代表程序不存在相关的Bug。
为了检验电梯运行情况的正确性,笔者着手迭代开发了评测机,对于电梯的运行逻辑进行分析验证,接触了Python、Linux的一些方便的功能,并下载使用了工具VisualVM进行CPU时间的监视。因此这单元可谓是测试最为充分的一单元,在互测中也连续取得了收获。
这单元让笔者着实体会到了主动开发评测脚本的乐趣。
第三单元
第三单元笔者继续使用评测机(这次数据生成难度高,但笔者又双叒叕摸鱼了,白嫖了dalao的成果),使用黑盒对拍的方式检查Bug,因此三次作业也没有被发现任何Bug。
关于JUnit在这一单元用的不多,但在学期结束后笔者的感想就是,今后开发Java项目时少不了对其的使用,因为黑盒对拍这种方式只能在存在两个或以上实现了相同需求的程序时发挥功效,而且几个人一起测试可能最后结果都是错误的。
另外本单元也学会了构造极端数据对程序鲁棒性进行测试,在互测中使用提前准备好的数据全屋一起多人运动,连续三次有了hack收获。
第四单元
第四单元架构设计得非常舒适,但是第一次还是爆了个CPU_TIME_LIMIT_EXCEED
。
随机数据思路
本单元构造数据难度较大,笔者尝试了随机生成类图相关数据,功能性的覆盖效果还算理想,可是没有测出什么致命的Bug。
要保证数据的合法性,就不能生成循环继承的数据。
- 类的继承树:使用并查集,每一次从没有父类的类中随机取出一个,如果该类没有继承关系则随机一个类继承,如果该类已经是其他类的父类,则需要保证该类不能继承其集合中的子类。
- 接口的继承图:笔者采用了五级制,在生成接口时随机为接口分配一个继承等级,低等级的接口只能继承比其高等级的接口,如果随机时发现没有接口满足条件,就生成一个方法填充。
此外设计了几种随机数据的模式——纯随机、多方法多属性、多继承关系、多关联关系。实际应用时发现了一位dalao早期版本的一个统计实现接口的Bug。笔者观察了随机数据的结果,发现生成的继承图有一定的覆盖性价值,可见随机数据构造得还没那么失败。
第三次作业将第一次作业避免循环继承的处理去掉,也能保证出现\(R001\)、\(R002\)、\(R003\)、\(R004\)的错误类型,并且发现了其他dalao早期版本的Bug。
测试疏漏
这一单元极端数据的构造难度远大于第三单元,而且笔者第一次作业前前后后搞了一天的随机数据,然后就摸了,再然后就是强测的菱形图出现了超时情况……
至于超时的原因,笔者发现沿用的第三单元BFS模板本身就有Bug,在出队列时才打访问标记,这会导致队列中已经有的元素反复入队的情况发生。将打标记这一行挪到了入队时就轻松解决了超时的问题。
对Bug和测试的理解
经历了一学期的自测、公测与互测,笔者认为程序的Bug可以分为这样几类:
- 算法实现Bug:这种Bug即常见的低级失误,思考的逻辑与使用的算法不存在问题但是实现中可能由于疏忽
(手残之类的2333)导致程序出现Bug,如第九次作业互测时发现的查询Bug,笔者第二次作业出现的更新系数错误等。可以用JUnit、黑盒对拍、评测脚本等检查,形式验证的方式不太适用。 - 程序逻辑Bug:思考的解决逻辑与算法本身存在一定问题,大多数超时、异常等情况也应属于此类。如笔者第十三次作业的BFS超时Bug等。可以使用JUnit,对拍、评测脚本等方式进行覆盖性的检查,但需要更强的数据支持,要增加边界情况。
- 需求偏差Bug:程序的逻辑以及算法实现都没有问题,但最初理解需求出现偏差,或者需求描述不清晰导致实现结果与需求方预期不符。这种Bug最难察觉同时又最容易出现,整个第四单元基本都是围绕模糊的需求展开的,而笔者第二次作业也是没有仔细阅读指导书的需求从而翻车。JUnit和评测脚本方式都不再适用,因为测试逻辑也顺着错误的需求,得到的是正确的假象。只能够通过形式验证的方式,与需求方不断交互确认来降低Bug出现的概率。
其实有时的Bug都是一些很细节的地方没有考虑周到,谨慎测试后依然会翻车,这大概与个人的编程思维和习惯有很大的关联。笔者习惯在编写时就考虑周全,一边写一边形式验证,编写效率低但是通过本地测试的效率很高。有些dalao写程序效率非常高,且没有细节处的Bug,不进行过多的测试就能做到无伤,是笔者无论如何都学不来的。只能说,面对做了测试后的结果时,自己能够无悔并欣然接受。
总之,Bug是门大学问,测试是门更大的学问。
对课程的建议
作业
寒假作业
- 今年相比去年,寒假作业内容更丰富,让没有Java基础的同学快速入门,可以考虑增加第三弹迭代,介绍Java语更多的特性,使同学们对Java更加熟练,加快到第一单元的过度。
作业内容
-
前两个单元都有相应的主题,但是到第三单元JML,笔者更倾向将其归类为算法,JML这个工具实在冷门,导致了这个单元本身的契约式编程十分不明确,相关工具链也过于鸡肋,希望可以加以改善。
-
第四单元UML规范其实在第一单元绘制类图时就已经涉及,笔者没有采用自动导出的方式,在绘制类图时查阅了很多资料才将类图完善。不知道能否将其前移,置于一个合适的位置。
中测
第三单元的中测部分应当适当加强,我明白课程组的本意是引导同学们进行自主的测试,但在笔者看来,仍然有一部分同学依赖于中测结果,且这部分同学在强测失足后依然不对测试进行过多的探索,也许本单元中测加入隐藏稍强数据点的约束可以使更多同学培养自主测试的习惯。
强测
第二单元
- 在经济条件允许的情况下对多线程的程序进行两次或更多次测试,来检验线程安全的问题。
测试机制
- 强测的数据可以考虑从多个随机的角度进行生成,比如第三单元的第一次作业,笔者使用的随机生成器,基本每一个数据都能hack到房内一个成员的同质Bug。多种随机的方案既保证了强测数据的覆盖性,又能确保强测梯度,很多Bug都只会在特定的策略下浮现。
- 本地测试环境与评测机的环境存在一定差异,希望开放CPU时间测试窗口,为了防止滥用可以设置使用次数或者使用CD。或者,考虑提供官方评测机的主频范围,使同学们可以在本地对程序的时间性能有个相对稳妥的把控。
性能分
- 性能分的评价机制造成了及其严重的内卷,自己的得分会受到其他所有人性能的影响。希望能够依据数据点的多条分数线来判定性能值,不然就会出现两种极端情况,一种是不断进行优化,另一种则直接放弃优化,我想这样强烈的竞争应该不是课程组所希望看到的。
互测
互测的体验非常好,首先极大地培养了测试能力,另外也可以学习到优秀的架构设计。
- 不知道最后总评如何计算互测成绩,在笔者看来,第一单元的互测意义其实最大,为什么要删除
Wrong Format
的互测数据,是笔者所不能理解的。这一单元强测本身就难以覆盖全面,所以大量的Bug都是依赖互测发现的,为了防止部分狼人对格式下手过重,可以考虑限制提交格式错误的数据数量。 - 第三单元的互测限制了指令数,导致一些Bug无法成功hack,希望适当扩大限制。
- 直接把互测房间的等级给出,让同学对自己强测的结果有一个感性的认识,并依据等级使用不同的hack策略。况且Bug修复环节本来也会根据自己hack得分来直接推断出互测房间等级,隐藏这个真的没有什么必要。
实验
- 合理安排实验的难度,明确实验的目的,尽可能使实验的操作更加具有局限性和客观性。垃圾回收那次实验难度过高,要求使用JUnit寻找可能隐藏在任何地方的Bug,这几乎在两小时内是无从下手的,导致了一次教学事故。
- 及时反馈实验的结果,便于反思与提高。一学期下来也不知道自己哪次实验做得过关哪次做得不好,其价值完全体现不出来。
研讨课
- 这学期应该是受到了疫情的影响,希望今后的研讨课可以请在职一线的工程师来传授一些工作中开发项目的技巧与经验。
- 很多单元研讨课难以确定一个合适的主题,笔者认为可以由老师和助教事先商讨出几个课外补充的知识点,让研讨分享的同学以授课的方式传达给大家。
线上学习的体会
这学期必定是最特殊的,但身为计算机专业的一份子,不论身在何地,都可以写代码,OO也理所当然成为了受到影响最小的课程。
课程收获与总结
Java语言方面
- 了解Java基本语法,知道继承、接口、封装、多态机制。
- 会使用Java已有的轮子,越用越香。
- 设计简单的异常并且处理,在第四单元得以运用。
- 设计多线程程序,绞尽脑汁避免线程安全问题,但多线程是门水很深的领域,仍有很长的路要走。
- 泛型、Lambda表达式、函数式编程等概念也有所了解与应用,Java的一些特性为简化编程提供了很大帮助。
- 上手IDEA,接着学会使用了JetBrains公司的其他IDE。
评测方面
- 使用Python、C++、Java、Linux Shell等工具帮助评测。
- 尝试随机数据生成,造的数据覆盖性强不强也是一门大学问。
- 阅读他人代码能力有一定提升(与隔壁OS的磨练不无关系)。
- 至少会用Git三步走将代码push到远程,切换分支拉取分支等操作也在隔壁练得炉火纯青。
设计方面
- 代码风格在IDEA的CheckStyle帮助下有了巨大的进步,回望一年前的数据结构代码可谓是ShiShan。
- 驾驭高代码量的能力有了巨大提升,第四单元最后一次作业2910行代码。
- 架构设计理念有了非常大的进步,从前期只有面向过程的思路到最后一单元的面向对象设计方案,笔者认为至少设计出了让自己满意的架构。好的架构并不一定没有缺陷,不过无论从debug角度还是从可扩展性角度,好的架构都会让编程者感到舒适万分。
- 对于常见的设计模式如工厂模式、生产者消费者模式、观察者模式、适配器模式和代理模式等有了了解和应用。
- 博客园前端维护用到了一些CSS的知识,尚需多加练习。
写在最后
笔者认为,OO不光磨练技术,还磨练心性。一周一次大作业本身其实应当是一种快乐,而不应当是一种负担。对于每周日中午12点的互测、每周一晚上10点的强测结果,笔者也从焦虑变得释然。
每一次作业能够相比上一次作业对Java、对评测、对架构甚至对自己有了更多的认识,又何尝不是一次进步?
笔者不认同及格万岁的想法,任何事情都应该先去尝试,即便最后结果可能并不令人满意。况且这也是在为今后的生活打下坚实的基础,未来应对工作中的项目开发,没有了所谓强测、互测、性能分这些花里胡哨的新颖规则,有的仅仅是单调的流水作业、模糊不清的需求描述,以及DDL的无情push。
届时,我们大概都会怀念起,2020年的春天,在隔离的家中,那种见到强测满分的成就、互测hack到人的喜悦、讨论区帖子加精的快感、老师们在研讨课中的劝诫、助教们彻夜修锅的辛勤……那都是6系一门小名为OO的课程,留给我们的宝贵回忆。
感谢课程组一学期的远程付出,给予了笔者如此优秀的课程体验,望OO课程能够越来越好。