一、三次作业架构设计
-
第一次作业——类图解析
(1) 架构介绍
这次作业跟上个单元类似,也是只需要实现对应的接口就行了,咋一看只有10个查询函数,感觉分分钟搞定啦。然后做起来完全不是这个亚子。。。。
这次从构造函数中传进来的是一堆乱序的
UMLElement
,然后需要从中完成各种查询。而且这次没有任何JML的指示,只能凭借自己对UML的认识完成代码。所以首先还是去熟悉了一下UML类图的结构。类图有很多元素,而这里只是选取了其中一部分,大致可以分为“成分”和“关系”。“成分”包括
UmlClass
,UmlInterface
,UmlAttribute
,UmlOperation
,UmlParameter
,他们是有比较明显的层次关系的。所以我对其中位于较高层元素用类包装了一下,便于组织起整个层次架构。“关系”里面包括
UmlAssociation
,UmlAssociationEnd
,UmlGeneralization
,UmlInterfaceRealization
。除了UmlAssociation
和UmlAssociationEnd
有层次关系,其他均是独立的。但是我并没有给这些关系用类进行包装,而是把它们转化成“类”和“接口”里面的成员,比如对于UmlAssociation
我们只关心对端是哪个“类”或者“接口”,所以只需要存储一下对端就行了。如果是自关联的话就要从两个角度来存储自己。// MyClass 的成员属性 public class MyClass { private UmlClass umlClass; // 对应的UmlClass private ArrayList
attributes; private ArrayList operations; private MyClass parent; // 将UmlGeneralization转化为parent属性 private ArrayList interfaces; // 将UmlInterfaceRealization转化为接口序列 private ArrayList associations; // 将UmlAssociation转化为关联对象序列 } (2) 类图
第一次作业架构比较简单,也就是对这些类稍微包装了一下。
(3) 方法复杂度
Method ev(G) iv(G) LOC v(G) main.MyClass.getOperationCount(OperationQueryType[]) 3 3 32 13 main.MyUmlInteraction.fillParameter(UmlElement[]) 8 5 21 8 main.MyUmlInteraction.getClass(String) 5 4 17 5 main.MyUmlInteraction.fillGeneralization(UmlElement[]) 1 7 17 7 main.MyClass.getAttributeVisibility(String) 4 5 17 5 main.MyUmlInteraction.fillOperation(UmlElement[]) 4 5 15 5 main.MyUmlInteraction.getClassAttributeVisibility(String,String) 2 1 14 3 main.MyUmlInteraction.fillAssociation(UmlElement[]) 1 5 14 5 main.MyClass.getOperationVisibility(String) 1 2 13 2 main.MyClass.getInformationNotHidden() 1 4 13 4 main.MyUmlInteraction.MyUmlInteraction(UmlElement[]) 1 1 12 1 main.MyOperation.addParameter(UmlParameter) 2 2 12 3 main.MyClass.getAssociationClassSet() 1 4 12 4 似乎有些方法复杂度有点高。
getOperationCount
主要原因是里面有一个switch-case来判断参数类型是否有冲突的。不过应该用一个HashMap的话可读性会好一些。另外很多方法里面都包含了instanceof
关键字,还有大量的if,所以圈复杂度都会稍微比较高。(4) 类复杂度
Class CSA CSO LOC OCavg WMC main.MyUmlInteraction 2 44 217 2.68 59 main.MyClass 6 32 178 2.55 51 main.MyInterface 3 21 58 1.56 14 main.MyOperation 4 16 37 2 8 main.Main 0 13 6 1 1 MyUmlInteraction
实际上每个查询方法都非常简单,任务都分派到下面的类里面去了(主要是MyClass
),但是MyUmlInteraction
需要很多预处理工作,这部分贡献了大量的代码行数和圈复杂度,也使得本该轻松的MyUmlInteraction
变得臃肿。MyClass
成为了跑腿的,自然就承担了大量的业务量。(5) 依赖矩阵
从这个也可以看出
MyClass
被MyUmlInteraction
支配的命运。 -
第二次作业——类图、顺序图、状态图解析
(1) 架构介绍
第二次加入了两种新的图。这两种图对于我来说比较陌生。不过还好讨论区对各个元素以及他们的层次关系有比较详细的介绍,让我快速上手了。
这次很显然,三个图得分别处理。所以我分开了三个包,并且重构了
MyUmlInteraction
来适应新的需求。为了解决上一次作业构造函数过大,头重脚轻缺点,用了工厂模式来进行了一波重构,实际上就是将构造函数单独抽出来形成一个工厂类。其他的图也采用了这种形式。
新出的两个图的组织方式跟类图差不多,也是以“元素”为单位,“关系”转化为元素内部的成员属性。只是另外两个图的询问不多,元素倒不少。
为了解决上一次作业一堆
instanceof
并且还要多次遍历整个elements
数组的情况,费力不讨好,所以这一次采用一个比较巧妙的方式提前分好类。HashMap
> elementMap = new HashMap<>(); for (UmlElement element : elements) { elementMap.computeIfAbsent(element.getClass().getSimpleName(), k -> new ArrayList<>()).add(element); } (2) 类图
这次作业的类图就比较分明了。三个图层次也比较清晰。
(3) 方法复杂度
Method ev(G) iv(G) LOC v(G) classdiagram.MyClass.getOperationCount(OperationQueryType[]) 3 3 32 13 classdiagram.MyClassModelInteractionFactory.fillParameter
(HashMap,HashMap ,HashMap ) 7 4 22 7 statediagram.MyStateMachine.getState(String) 5 6 22 6 classdiagram.MyClassModelInteractionFactory.fillAssociation
(HashMap,HashMap ,HashMap ) 1 4 19 4 classdiagram.MyClassModelInteractionFactory.fillGeneralization
(HashMap,HashMap ,HashMap ) 1 6 18 6 classdiagram.MyClass.getAttributeVisibility(String) 4 5 17 5 classdiagram.MyClassModelInteraction.getClass(String) 5 4 17 5 classdiagram.MyClassModelInteractionFactory.fillOperation
(HashMap,HashMap ,HashMap ) 3 4 16 4 statediagram.MyRegion.addTransition(UmlTransition) 1 5 16 5 statediagram.MyStateChartInteractionFactory.create(HashMap ) 1 3 16 3 classdiagram.MyClassModelInteractionFactory.getEnd
(ArrayList ,String,HashMap,HashMap ) 3 3 15 3 sequentdiagram.MyCollaborationInteractionFactory.create(HashMap ) 1 3 15 3 classdiagram.MyClassModelInteraction.getClassAttributeVisibility(String,String) 2 1 14 3 classdiagram.MyClassModelInteractionFactory.create(HashMap ) 1 1 14 1 classdiagram.MyClass.getInformationNotHidden() 1 4 13 4 classdiagram.MyClass.getOperationVisibility(String) 1 2 13 2 statediagram.MyStateChartInteractionFactory.fillState(HashMap ,HashMap ) 1 2 13 2 classdiagram.MyClass.getAssociationClassSet() 1 4 12 4 classdiagram.MyOperation.addParameter(UmlParameter) 2 2 12 3 可以看到,上面表格中大部分的方法都是工厂类,说明预处理的占比之大,也说明其他的查询方法是相当简洁的。
(4) 类复杂度
Class CSA CSO LOC OCavg WMC classdiagram.MyClass 6 32 178 2.55 51 classdiagram.MyClassModelInteractionFactory 0 23 151 3.18 35 main.MyUmlGeneralInteraction 3 45 99 1.06 18 classdiagram.MyClassModelInteraction 2 34 95 1.58 19 statediagram.MyStateChartInteractionFactory 0 17 59 2.2 11 statediagram.MyStateMachine 2 19 55 1.86 13 classdiagram.MyInterface 3 21 52 1.56 14 sequentdiagram.MyInteraction 5 20 52 1.25 10 statediagram.MyRegion 4 19 52 1.86 13 sequentdiagram.MyCollaborationInteractionFactory 0 17 50 2.2 11 statediagram.MyState 3 20 43 1.38 11 classdiagram.MyOperation 4 16 37 2 8 statediagram.MyTransition 5 20 35 1 8 sequentdiagram.MyCollaborationInteraction 1 20 34 1.4 7 statediagram.MyStateChartInteraction 1 20 34 1.4 7 sequentdiagram.MyLifeline 3 21 28 1 6 sequentdiagram.MyEndpoint 3 20 25 1 5 sequentdiagram.MyMessage 3 16 20 1 4 main.Main 0 13 6 1 1 可以看出,有了工厂类之后,
*Interaction
(不包括MyInteraction
)的规模保持在比较低的水平。而工厂类的规模反而是相对较高的水平。(5) 依赖矩阵
“关系”与“元素”的循环依赖,应该算是比较正常。为什么在这两个图的“关系”要单独用类包装?因为其中可能会有一些信息需要保留,为后面的扩展留出空间。(其实类图没有包装在第三次就翻车了,最后还是新建了一个
MyAssociation
) -
第三次作业——类图、顺序图、状态图解析,UML规则检查
(1) 架构介绍
第三次作业增加了UML规则检查。原来的架构基本不需要变动,不过之前的架构是舍弃了
UmlAssociationEnd
信息,所以这次作业需要增加对UmlAssociation
的包装,以满足检查的需要。因为每一条检查规则都是对一个特定的图,我对每个图都增加了一个用于检查的类。通过读取图的信息进行检查。
(2) 类图
因为没有将检查职责下放,基本都是获取开放访问来实现检查的,所以检查模块跟多个类都有依赖关系,不过总体架构跟上一次作业一样。
(3) 方法复杂度
Method ev(G) iv(G) LOC v(G) classdiagram.MyClass.getOperationCount(OperationQueryType[]) 3 3 32 13 classdiagram.ClassModelCheck.checkClassDuplicatedImplementation(MyClass) 5 3 26 5 classdiagram.MyClass.getDuplicatedAttributeNames() 5 5 22 9 classdiagram.MyClassModelInteractionFactory.fillParameter(HashMap ,HashMap ,HashMap ) 7 4 22 7 statediagram.MyStateMachine.getState(String) 5 6 22 6 classdiagram.MyClassModelInteractionFactory.fillAssociation(HashMap ,HashMap ,HashMap ) 1 4 21 4 classdiagram.ClassModelCheck.checkClassCycleInheritance(MyClass) 5 3 19 5 classdiagram.ClassModelCheck.checkInterfaceCycleInheritance(MyInterface) 5 3 19 5 classdiagram.MyClass.getAssociationClassSet() 1 5 18 5 classdiagram.MyClassModelInteractionFactory.fillGeneralization(HashMap ,HashMap ,HashMap ) 1 6 18 6 classdiagram.ClassModelCheck.checkNoNamed
(UmlClassOrInterface,UmlAttribute[],MyOperation[])6 3 17 6 classdiagram.MyClass.getAttributeVisibility(String) 4 5 17 5 classdiagram.MyClassModelInteraction.getClass(String) 5 4 17 5 检查方法独揽大权,必然比较复杂了。同时里面有好几个循环和分支,所以圈复杂度也相对比较高。
(4) 类复杂度
Class CSA CSO LOC OCavg WMC classdiagram.MyClass 7 36 216 2.62 63 classdiagram.ClassModelCheck 2 25 193 4.31 56 classdiagram.MyClassModelInteractionFactory 0 23 158 3.27 36 main.MyUmlGeneralInteraction 3 62 131 1.04 26 classdiagram.MyClassModelInteraction 3 35 100 1.54 20 classdiagram.MyInterface 4 25 72 1.54 20 statediagram.MyStateMachine 2 21 71 2.11 19 statediagram.MyRegion 6 21 67 1.89 17 statediagram.MyStateChartInteractionFactory 0 17 59 2.2 11 sequentdiagram.MyInteraction 5 20 52 1.25 10 sequentdiagram.MyCollaborationInteractionFactory 0 17 50 2.2 11 classdiagram.MyOperation 4 17 48 2.4 12 statediagram.MyState 3 21 46 1.33 12 statediagram.MyStateChartInteraction 2 21 39 1.33 8 statediagram.MyTransition 5 20 35 1 8 sequentdiagram.MyCollaborationInteraction 1 20 34 1.4 7 classdiagram.MyAssociation 5 18 31 1 6 sequentdiagram.MyLifeline 3 21 28 1 6 sequentdiagram.MyEndpoint 3 20 25 1 5 statediagram.StateChartCheck 1 15 25 3 9 sequentdiagram.MyMessage 3 16 20 1 4 main.Main 0 13 6 1 1 排在前三甲的是
MyClass
,ClassModelCheck
,MyClassModelInteractionFactory
,全是类图的(本来访问和检查都是多数对于类图的)。第一个负责主要职责,第二个负责了六个规则的检查,第三个负责了整个类图的预处理。不过这也说明,职责应该可以再细化。(4) 依赖矩阵
基本都是包内依赖比较严重,层次比较清晰。多了一个红框是因为
MyClass
和MyInterface
有一个方法内容几乎相同,所以抽出来随手放到了ClassModelCheck
作为静态方法,其实应该设置一个ClassOrInterface
的接口,因为这两个类有很多时候还是有相似之处的。处理方法也有相同的地方。
二、四个单元中架构设计及OO方法理解的演进
四个单元走来,我从一开始的胡乱设计,经历了12次代码作业的洗练,变得逐渐重视架构。架构混乱,导致新需求到来时难以适应,于是不得不进行大规模重构,甚至重写。后来架构的重构也逐渐减少了,但是还是不能完全做到开闭原则。
第一个单元的架构是混乱的,一开始有一个相对清晰的布局,后来为了性能分逐渐增加耦合,最后就变成了一团乱麻。每一次作业都是大的重构。最后的结果自然是炸了两次强测,没能成功活过一次互测。
第二个单元的架构还算是清晰,但是有一个不好的地方,就是为了追求性能老是更改架构,在这个上面也花费了很多冤枉时间。虽然最后一次作业的性能有了一定的提高,但更换架构的操作还是需要多加斟酌。
第三个单元由于给定了JML,我就开始放弃思考架构。所有操作都在方法内完成。所以最后导致类变得十分臃肿。后来从课程视频中学习到,应该将实现接口的类的功能分离,变成几个类,这样就不会出现臃肿的情况。
第四个单元从一开始就想好架构。不过可以这样做的原因也是UML的各个元素层次本来就十分清晰。第一次作业的整体架构一直沿用到了第三次,而且扩展起来也比较省力。
不过也不是完全去否定重构。经过四个单元的迭代作业训练,我觉得做迭代开发时,小的重构是常态,大的重构是灾难。并不是所有人都是上帝视角,能够想一个适应性max的架构固然是好事,但是需求总是千奇百怪,要满足新的需求,有时候的重构也是必须的,即使不重构而采用冗余或者补丁的方式去处理,时间长了,整个项目也会像一艘打满补丁的船,显得十分臃肿。通过不断的小的重构,有时候不仅能提高项目的可扩展性,也能提高性能。当然,如果要进行大的重构,要么就是整个架构跟不上时代了,要么就是架构的设计是失败的。
三、四个单元中测试理解与实践的演进
测试一直是整个OO课程当中我极其重视的一环,虽然有的时候对于进一步的测试有点无能为力,但是也在尽量做到覆盖面全的测试。每次出锅都是我测试偷懒的外在表现。
前三个单元的测试都是采用黑盒+白盒的测试,第四单元采用白盒测试。
第一个单元,我用python+命令行写了一个测试程序,尝试随机生成输入数据和根据输出数据判断项目行为的正确性。同时留了一个白盒测试接口,用自己构造的数据进行测试。不过第一单元的测试做得比较急,也没有对项目进行很充分的测试,加上这个单元输入数据的组合方式有很多,随机数据难以覆盖到,自身也考虑不全,所以这次的测试算是比较失败了。白盒也比较难看出来。
第二个单元,用的是基于JAVA+python的测试程序,我参与了测试程序的整合,并用它来完成对项目正确性的检查。由于第一次作业疏忽了对程序运行时行为的检查,导致翻了车,所以后面两次作业在白盒测试阶段针对性构造数据测试,监视程序在运行时的情况,包括CPU的运行状态,所以后面两次都没有出问题。
第三个单元,测试相对简单了一些,跟第一个单元类似,不过相比第一单元,输出正确性判定的标准由python内置库的结果变成同学程序的运行结果,即对拍器。整个程序是由同学完成的,我就只管直接拿来改一下使用。当然,因为覆盖性的问题,我也构造了一些数据进行针对性测试。至于为什么没有使用JUnit,我觉得这个单元的结构还算比较简单,而且每个需要实现的方法其实就对应一个输出接口,我用JUnit进行针对性测试,跟我用特殊数据进行测试,效果其实是一样的。
第四单元,因为UML的结构比较复杂,比较难编写数据生成器,所以只是写了一个能够用mdj文件进行测试的对拍器。后面就依靠手搓mdj来进行测试。不过这个单元测试的均为询问指令,情况相对之前来说比较有限,所以自己就根据需求手搓数据,尽力保证覆盖性了。但是还是有考虑不周的时候。第一次作业造数据偷懒了,然后翻车了,幸好只有一个点。
测试给我带来的不仅是自己作业的正确性保证,还有在互测时期的狂欢。黑盒加白盒的测试让我在9次互测中8次有收获,其中有一次贡献出全场唯一刀,中间某一次断刀确实可惜了,但是那一次估计整个房间的代码确实都没有bug。测试偷懒的时候也是出现漏网之鱼的时候。有好几次出现比较明显的bug我没有刀中,就是因为没有细细品读他人代码的耐心。
虽然说,OO锻炼的面向对象的能力,但是在完成作业的过程中,测试无疑是十分重要的一环。而且在需求变化的过程中,项目开发随之迭代,测试也会随之迭代。我也比较享受测试程序迭代开发的过程。测试迭代开发比起作业的迭代要简单得多,因为只需要改变的是数据生成器的部分,生成满足新需求的数据。其余部分几乎不用改动或者改动很小。测试的框架从一开始就定好,清晰而层次分明,各部分直接耦合比较松。从某种意义上来说,在测试上反而做得比作业还要好。
除了迭代这一点,测试还能做到共同开发。一个比较完整的评测机包含数据生成器和验证器,而且两部分耦合比较松,完全可以由两个人来开发,一个人负责进行整合,只要事先约定好接口规范,那么就可以进行合作开发了。事实上第二单元我对同学评测机的整合也没有深入了解他的实现,只是阅读了最外层的代码文件,了解了调用规则就可以进行整合了。第三单元同学的对拍器中文件对比的部分也是我上学期做计组对拍的代码,而且最后也参与了整合。我写的评测机也经过他人的调整和修改。所以说OO的自动化测试是完全可以实现共同开发的。
下面是某一次评测机的开发日志记录。
四、课程收获
- 深化了对JAVA语言的理解,熟悉了各项语法知识和容器的使用。
- 熟悉了多种工具链的使用,包括IDEA,Git,JProfile,JUnit,StarUML等。
- 学习了多线程并发机制,了解了如何用JAVA实现分身之术(电梯反复横跳)。
- 学习了JML,懂得如何用规范化的语言去安排别人代码的一生,以后或许能用到。
- 学习了比JML多了一个 $l$ 的UML,不过仅仅停留在阅读和简单使用的基础上,没有真正用到UML来进行项目设计。
- 学习和巩固了一些算法知识,图论的算法比较少接触,幸好能在课程当中巩固了。
- 解锁了写简单评测机的技能,学习和巩固了命令行以及python的使用。
- 开始重视测试,理解了测试的重要性,以及测试人员的艰辛。
- 学习了很多设计模式以及设计原则,给我的作业设计提供了指导思想。
- 了解到事前设计的重要性,以及架构选择和设计的重要性。架构对成败有很大的影响。
- 在跟同(da)学(lao)们交流的时候,学习到了很多理念,对作业的完成也有很大的帮助。
五、课程改进建议
- 课程对测试的引导很少,只有课程视频当中和研讨课中提到过自动化测试。建议可以利用测试可多人开发的特性增加一个新的环节,允许同学们自行上传测试集,测试数据生成器,验证器到平台,平台可以让同学们进行测试。使用次数较多的测试集,测试数据生成器,验证器作为评测机的参考。
- 课程对使用UML进行项目开发设计的引导不够。建议可以用一次实验课来进行引导。可以给出UML图,进行代码填空补全或者直接编写。也可以利用作业来进行引导,比如指导书给出UML参考图,让学生按照UML图的设计来完成作业。
- 第三单元的博客要求中包含SMT Solver和JMLUnitNG的使用。我理解课程组是想让学生们了解如何JML进行形式化验证的用意。但是这两个工具到现在仍未成熟,配置过程也对使用者及其不友好,而且实用价值不高。建议不要将其列入必做项或者是加分激励项。
- 部分实验课课程目的是好的,但是课程内容需要改进。比如垃圾回收的题目,本意应该是理解垃圾回收机制和JML的使用。但是感觉内容有点指向不明,出现了一些让人迷惑的代码。我也十分理解助教们的辛苦,但也希望能够在实验内容上面再多打磨一下。
- 实验课的分数和标答应该至少公布其一,这样才能知道自己在实验中有哪些疏忽,这样的实验才能真正起到效果。
- 暂时想到这么多,这个艰难的学期助教和老师们都辛苦了!
六、总结和体会
我觉得OO这款游戏还是挺好玩的。界面友好,关卡众多,能捡到很多装备(学到新知识),有副本(中测和强测),也有竞技场(互测)。体验还是很不错的。
线上学习确实有很大的不便性,缺少了跟老师和助教的面对面交流。不过也有便利性,能够比较方便的控制学习的节奏,能够回顾以前学习的内容。
相对于其他的课程,OO算是影响比较少了。本来就是一门要求线上完成任务的课程,影响最大的也只有原来作为线下的上课和研讨。不过线上的话可能同学们会更愿意去交流,毕竟隔着屏幕会少了一丝尴尬。
通过迭代的方式来引导学生重视架构设计,分中测和强测来引导线下测试,通过互测来体验测试的过程和实现对他人代码思路的学习,这些都是比较新颖的设计。真的佩服为这门课程呕心沥血的老师们和助教们。
通过一个学期的学习,收获了很多,十分感谢这门课程给予了我难忘的记忆。尽管课程仍然有瑕疵,但瑕不掩瑜,OO在我心目中依然是一门好课。
来日方长,有缘再见。