2021 OO 第四单元总结博客
2021 OO 第四单元总结博客
需求回顾
本单元需要完成的任务是实现一个UML类图分析器,能够处理类图、状态图、时序图,并对模型的有效性进行检查。
这一单元并不是很难,但总体而言对细节要求比较高。
一 本单元架构设计
1.1 Homework13
本次作业需要实现对类图的处理。
我们首先需要解决的问题是如何从各个Element中将各个属性解析出来并建立有机联系。它们的关系是这样的:
UmlClassUmlAttributeUmlOperationUmlParameter
UmlInterface
因此为实现层层存放,方便添加属性和方法,我分别对顶层类非底层类型建立自己的类,即:MyClass、MyOperation、MyInterface.
由于本单元作业数据输入方式非常特殊,当读到END_OF_MODEL时一切信息都已尘埃落定,我们把这些信息搞到手了,不需要考虑数据的动态变化,只需要根据指令查就完了。所以我新开了一个MyPretreatment类,用于处理和将信息组织进各个类(进行两次遍历,第一次遍历将元素按就九大类型分类存储;第二次按顺序自底层向上遍历,构建信息树),并将存有所有类和接口的HashMap返回给MyUmlInteraction类,在该类中实现各种方法。在方法实现中,有两点值得一提:
-
将重复信息存入HashSet以检测异常
在第一次遍历的信息分类存放时,我把
Class以id为Key值存成了HashMap;在第二次遍历时遍历该HashMap,并组建以Name为索引的哈希字典。每次加入一个新的Class时我都会检测重名,一旦重名,就会把这个名字加入重名的HashSet中:public void handleClass() { for (MyClass myClass : this.classes.values()) { this.names.put(myClass.getMyClassName(), 0); if (!this.className.containsKey(myClass.getMyClassName())) { //如果叫这个名字的类还没有来过 this.className.put(myClass.getMyClassName(), myClass); //存入按名字索引的字典中 } else { this.dupClass.put(myClass.getMyClassName()); //存入重名哈希集合中 } } }这样做的好处是,我们只需要根据
dupClass.contains(name)就能判断是否发生异常了。同样的思想可以用于检测属性是否重名。
-
设置脏位,使需递归调用的复杂方法仅执行一遍
本单元作业的数据输入形式决定了两行相同的查询指令一定能得到相同的结果,因此如果这是一个复杂度较高的查询,我们可以把第一次的结果存下来,使第二次只有IO时间。以接口父类查询指令为例,每个
MyInterface中都有这样一个属性:private HashSet<String> interfaceName;用于存放自己父类的所有名字。每次查询接口父类的指令到来时,我们只需检测
interfaceName这一属性是否为空,如果不空则说明已经进行过查找,可直接返回;如果为空,则表示我们需要进行查找操作。public HashSet<String> getInterfaceName() { if (this.interfaceName.size() == 0) { //进行一波查找操作 } return this.interfaceName; }
1.2 Homework14
本单元在上次作业的基础上加入了顺序图和状态图的相关需求。总体的架构可以直接沿用上一次的架构,只是这次各Element之间的关系变得更加复杂:
UmlClassUmlAttributeUmlOperationUmlParameter
UmlInterfaceUmlInteractionUmlLifeline
UmlStateMachineUmlState,UmlFinalState,UmlPesudostateUmlTransitionUmlEvent
仍然对顶层类非底层类型建立自己的类,即新增:MyInteraction、MyLifeline、MyMachine、MyState、MyTrasition类。第二次作业的组织结构和第一次完全一致,只是增添了很多更细节的处理,在此不过多赘述。
1.3 Homework15
第三次作业中新增了模型有效性检查的需求。本次对于之前的架构也没有任何更改,只需在END_OF_MODEL之后进行八种模型检查,如各有效性都符合,则直接调用第二次作业中的各方法实现查找即可。
本次架构唯一修正的地方是因为MyUmlGeneralInteraction方法过长,对以下部分进行了封装:
- 封装了所有的异常检查,将重复出现的各异常检查封装为
MyTools类中的函数 - 封装了参数类型的有效性检查,将其变为
MyTools类中的函数 - 封装了将参数类型的字符串转换,将其变为
MyTools类中的函数
为了进一步降低MyUmlGeneralInteraction类行数的负担,统一采用在MyPretreatment类中进行检查的方式,MyUmlGeneralInteraction中仅负责I/O操作,以R001为例:
@Override
public void checkForUml001() throws UmlRule001Exception {
HashSet<AttributeClassInformation> cacheR001 = this.myPretreatment.checkR001();
if (cacheR001.size() != 0) {
throw new UmlRule001Exception(cacheR001);
}
}
在方法实现上,值得一提的是,我最初被R002卡了不久的时间,我不知道该如何输出环上的所有接口,即如何在dfs中有效地记录路径,在构造这个算法的过程中我持续遇到各种奇奇怪怪的bug,比如,如果它属于多个环怎么办,环中环怎么办....最后发现自己的想法完全走偏了,我们只需要遍历所有接口和类,判断当前这个类或接口是否循环继承,如果出现了循环继承就把它加入到待输出的HashSet中就好了。
二 四个单元中架构设计及OO方法理解的演进
1.1 Unit One
第一单元实现的是表达式求导。我的架构主要是通过继承与接口,实现了自底向上的求导。各因子分为常数因子、幂因子、表达式因子、正弦函数因子和余弦函数因子,统一继承Factor抽象类。其余包括表达式类、多项式类、因子类,及通过工厂模式解析原始输入的工厂类。
本单元对OO思想的指导是飞跃性质的,第二次作业的煎熬使我发现,难住我的正是我的思维方式,因此我一直在思考的是,我到底通过怎样的过程才能把这一次的代码实现。而我真正需要思考的是,我需要什么类,每个类中需要放什么方法和属性,类与类之间需要构造什么样的关系才能把方法实现。经历这些思考之后,我对面向对象的思维方式和继承有了比较深入的理解。
(碎碎念一句:第一单元还是有遗憾的,我应该学一学递归下降的,而不是抱残守缺地使用又臭又长的正则表达式,好家伙,那个正则展开之后自己能占IDEA的半屏...)
1.2 Unit Two
来到了第二单元,传说中难度巅峰的电梯单元,虽然没有被死锁折磨过,但我时常被CTLE折磨。本单元感觉架构比较简单,重点在于电梯运行算法和多线程交互。由调度器将各请求分配给各个电梯(即将总请求队列totalQueue中的各请求分给各电梯的waitqueue),每一个线程电梯通过自己等待队列中人员的变化进行运输。在第三次作业尝试实现换乘的过程中,出了1mol多线程bug,各种人还没从这个电梯出去,早就已经待在另一个电梯的灵异事件。通过解决这些问题我对多线程”锁“的概念有了更多理解。
1.3 Unit Three
第三单元是jml约束下的社会关系网络。本单元不太需要架构,只需要按照课程组给出的jml写成自己的java代码。这一单元我出现了许多性能问题,甚至第一次作业来一堆qnr我的程序就能1、2秒才蹦出来一个结果,慢到离谱。于是在修锅的过程中对各个容器的使用有了更多认识。这一单元同时也是个比较强调算法的单元,我动不动就在CTLE的边缘疯狂试探。不过借此契机学会了变量维护这一思想,顺带学了一下并查集、Dijsktra堆优化,在性能和算法上还是有张进的,这些都在第四单元继续帮助了我。
看到这里可能会觉得,你这而三单元也不咋有面向对象的收获啊。没错,确实没有什么显式收获,但隐式收获是很大的,正是由于前期的这些练习,第四单元的架构才能那么顺利。而且,在这个时期发生了一件小事使我感叹于OO对我的影响。
就是在写一次的数学建模作业时,需要用到编程计算一个量不小的数据表格,我这种建模万年Python党竟然觉得Python难用,迅速地设计出了一个通过面向对象思维实现的计算方式。当时恰巧赶上有不可推脱的事儿要我处理,但作业很快ddl了,队友竟然告诉我
要不您写个jml,我帮赶紧把它实现了吧。
这也太奇妙了,这大概就是我对面向对象理解的隐式演进吧,我已经愿意把这些知识用到解决生活中实际遇到的问题了,那再谈起我在OO中的收获,此处无声胜有声。二三单元真的没有白做训练,它对于我的OO思想是量变积累质变的量变积累。
1.4 Unit Four
第四单元需要实现一个UML类图分析器,详细的架构其实在这篇博客的第一章已经描述了。大致是通过MyPretreatment类进行存放的处理,把Uml...变成My...,接着再实现各种模式检查和信息查找。
我好像面对那一堆Element的信息自然而然就知道要构建自己的类(第一单元学的),把我们想存的东西通过我们喜欢的容器(第三单元学的),编写合适的方法并实现出来。可以说之前看似无关的知识,在第四单元中都被综合地应用起来了。一个个类已经不是费劲挤牙膏似的挤出来的了,而是一个水到渠成的过程,大概在这里我收获到了质变的回馈吧。
这应该就是我和我的作业架构以及OO思想演进的故事了。
三 测试理解与实践演进
我对于程序正确性的验证方式有个一以贯之的思想,就是读代码为主,对拍打辅助。如果对拍机一直跑不出我的bug,我很难相信我的程序差不多行了;但读了一遍代码之后,我往往会收获一种莫名的安心(特别感谢从第一次作业开始次次都愿意陪我一起读代码的hxd!)。
虽然自己读代码去验证很香,但对拍机也真的很重要。从第三单元开始,我加入了一个对拍小组,我们在方寸田园里共享一套对拍机,从搭评测机到样例生成器到找数据到手动构造极端情况,我克制不住地大喊,队友们真的太强了!但也因为持续被带飞,我有些一直停留在自己的舒适区内,应该勇敢一点往外走走的。
所以,我测试程序的过程总结起来是这样的:
-
第一单元
主要依赖读代码,并通过同学写的评测机对拍。这时候的数据生成器是宇宙无敌纯随机,但只要测得够多,就能量变引发质变地找出bug。但当时没有平衡好数据好debug和数据强度的关系。在第三次作业的测试里盲目取消了大整数(为了检查WF和好算bug在哪儿),然后我的大整数WF bug就巧妙避开了对拍机的搜查,被强测逮住了。不过主要还是那次写得太拉没来得及读代码吧。
-
第二单元
主要依赖读代码,并试图改造了一个现成评测机。但实际上,并没有实现对
CTLE的判断,导致轮巡阴魂不散地跟了整个电梯月。 -
第三单元
先利用对拍测试,直到对拍机健康运行后开始读代码。也在对拍小组里其他成员写的样例生成器中学到了很多东西,比如利用树构造特殊图;照着单条指令反复怼测性能;把最初随机生成的一些信息存放起来再使用,保证数据别光测异常而没有强度...数据很有效,帮我找出了不少性能问题。
-
第四单元
先利用对拍测试,直到对拍机健康运行后开始读代码。在这个单元里,写样例的随机生成器变得比较困难,我们拥有的测试点变得有限。于是开始试图手动构造极端样例(比如胡乱存放
null之类的)对程序进行测试。不少奇奇怪怪的锅被翻了出来。
可以说,做测试的时间往往不比写代码的时间短,二者旗鼓相当。
四 课程收获
收获其实很多啊,小的大的,真的能啰嗦一箩筐。我捡着比较重要的三点,说一说吧。
-
面向对象的编程思想
这部分的详细内容已经在第二章中分析了。
-
合作的重要性
一个人走得快,一群人走得远。
遇到对拍小组的各位是真的很开心,虽然更近距离认识到了自己的菜,但在合作的过程中看到了好多一个人可能看不到的风景。
-
永远不要相信自己的代码没有bug
我之前总是觉得,代码中出现bug,类似于一种需要女娲补天的严重漏洞。经过OO的学习我才发现,bug真的可以非常微小,但轻易达成千里之堤溃于蚁穴的效果。它可以是忘记一个=,可以是写错一个字母,可能是一个变量没加this.导致混用...因此充分的设计与良好的代码风格都是程序员的必修课,我们需要采取多方努力,尽量减少bug中的出现,并永远对自己的代码保持谦卑的心,没有代码不值得调试!
五 改进性建议
-
可以在博客周开放下一单元的Training
本学期的四次Training真的对我学习当前单元起了非常大的作用,但也导致了每单元第一次作业的时间紧迫,总觉得有太多东西要学,太高的门槛要入。所以或许可以提前开放以下Training,下放一些下单元的学习资料。
-
稍稍增加一些pre的训练量
第一单元的开幕雷击很大程度是还没有太熟悉java的面向对象编程思想,导致一切都慌张仓促着。而经过大半学期的训练,到最后写第四单元的作业,架构时明显顺滑了很多。所以究其原因,很多难受还是熟练度带来的,因此或许可以稍稍增加一些pre的代码量,并与后面的知识产生一定的关联,降低一下OO的开门门槛。
-
坚持开设官方答疑帖
希望大家能一起真的把讨论区利用起来。三四单元比较琐碎,很多地方难以理解,这时候同学们就需要一点必要的解释,可以利用官方答疑帖释疑的方式满足同学们的需求并及时实现消息共享。
算是一些吹毛求疵的建议啦,OO课程组已经很用心了,年年革新的勇气也令我非常敬佩!
写在最后
真的到了最后一笔,曾以为的千言万语突然如鲠在喉,那就...这样吧:
public class MainClass {
public static void main(String[] args) throws Exception {
System.out.println("Thanks to OO, wish the team prosperous!");
}
}

浙公网安备 33010602011771号