BUAA_OO 第四单元总结——UML&终章
前言
“靡不有初,鲜克有终”,便是很小就学会的道理,甚至数不清尝了多少次它的苦头,你说长教训了吗,自然是记下了;践行了吗,应该也是努力了的。coding前的设计也做了,代码也通读过了,即便众多ddl当头也还是测试到了最后一个周日晚上,于是依旧怀着像前几次一样的不安与谦卑交上了最后一版代码,像往常一样忐忑于秒针的跳动,等待着结果的揭晓,好坏都坦然。希望,也以为一切如常,却才意识到,最后一次,本就是不寻常。功亏一篑之人,似乎从未挣脱关键时刻掉链子的规律,可能触及到九仞的事屈指可数,自然,如果失望的眼神可以具象化的话,必是其中之一[笑哭]。这么想来,结束之前寻常的流程似乎也只是我愿意相信的寻常,似乎也只是用于掩饰不安,好像从最后一次开始的时候就有种对善终的期待,自然也有种踩空惯了的人条件反射式的不祥预感。于是我不停地安慰自己,像往常一样就好啦,强测也挂过呀,尽力就好啦,当然也以为安抚好了自己,规律也就不会应验,或者即便成真也不是没遇见过。但是不管怎么说,不妙的事情还是发生了,最后一次的不寻常之处就在于它隐藏的巨大杀伤力——“差一点点”远比一开始就不会更揪心(当然,任谁都不会选择后者)。(打断施法!我就是强测挂了俩点而已...而已?)
架构设计
本单元依旧采用增量式开发,但三次作业可以明显地划分为三个相对独立的部分,因此进行统一的架构分析。
各元素是乱序输入的,因此我按照逻辑层次和使用顺序用三次遍历进行筛选,重新构建起各元素的从属关系(重新套娃)。
buildStruct1 | buildStruct2 | buildStruct3 | |
---|---|---|---|
第一次作业 | Class, Interface, AssociationEnd | Attribute, Operation, Association, Generalization, InterfaceRealization | Parameter |
第二次作业 | Interaction, StateMachine, Region | Lifeline, State, Pseudostate, FinalState | Message, Transition, Event |
对于各种元素,我基本都各采用了两个容器进行存储,一是以Id为key,元素为value的HashMap,另一个是以name为key,存储元素的HashSet为value的HashMap。前者满足了初始化时根据Id查找元素的需要,后者主要满足执行输入的指令和进行模型检验时的需要。
为了存储各个容器,我使用了一个静态类MyMap,主要用于按name和Id查找元素。由于MyMap静态类可以随处调用,我不讲武德地把所有dfs遍历查找所需的Visited列表也放在了MyMap中,当做全局变量使用了。
我对一些UML元素定义了新的类,包括Class,Interface,Interaction,Lifeline,Operation,State,StateMachine,类中新增了很多存储内容或数据结构,用于记录各种关系。在其中我还存储了原本官方包的UML元素,便于调用获得原本的信息。
由于接口和类都具有属性、关联关系、方法等,因此我将MyClass和MyInterface继承自了同一个接口MyElement,不同之处在于MyClass类多了一个实现接口的容器,MyClass继承的父类为一个MyClass对象,而MyInterface继承的接口为一个装有很多MyInterface对象的容器。
对于前两次作业的查询指令,我没有通过调用MyMap里的查询方法完成,而是调用方法从MyMap里获取相关元素对象,调用对象的方法得到输出的容器,最后在顶层类里进行简单的判断,该抛异常的抛异常,该返回的返回。因为很多异常需要两个String,如果在更深层的位置抛异常,要么需要传入无关于该对象的参数,要么需要构建自己的异常类,抛到外层再填上另一个String,过于多此一举了。
对于第三次的模型检验,我直接在MyUmlGeneralInteraction类里调用了MyMap类中的查询方法(待查的东西在MyMap里都有存储,基本上遍历容器就行了),八个check方法加起来不到40行,非常简洁(其实只是因为怕超500行)。
对于检验重复实现接口、重复继承接口等问题,我借用了dfs的方法,即在顶层类里存储一个全局的记录容器,每次拿到一个实现或继承的接口,判断记录容器中是否已存在该元素,若存在,说明出现了重复,这时直接抛出一个自定义的异常,由某一层接收后返回即可;若不存在,填入容器完成记录。然而由于我的MyClass和MyInterface内部存储接口的容器均为哈希容器,若一个类在自身层级就出现了重复,这个接口只会存在一次,不能被查出来,因此被迫加入了一个记录本层已有重复的boolean变量,在向对象中加入元素时判断是否已存在并变更这个boolean变量。(可悲的是,重复继承接口做到了这一点,但重复实现接口犯懒没有加,于是强测寄了!!!)
强测暴毙的第二个点是check005,其恐怖之处在于我Enter敲高兴了调错了容器,同时我的测评机数据生成犯病一样地把null生成成了“null”(带引号),于是好巧不巧测不出来。
UML类图
四个单元中架构设计及OO方法理解的演进
前两个单元一开始很心急,读完指导书打开IDEA就想开始写,然后到后面的作业时发现架构并不那么优美,想改又改不动,像我这种略完美主义又肯狠下心来爆肝的人自然是啪的一下决定重构。虽然结果还是好的,但毕竟耗费了更多的时间,做了很多无用功。后两个单元决定静下心来,磨刀不误砍柴工,于是三单元认真读过JML规格,确定好适合的容器,每个方法的大致步骤后再开始;四单元设计好了架构,先在每个类里定义完成员变量和方法,然后再填充内容,这样写起来反倒更快了。
一单元在三次作业中逐步理解了继承和抽象的思想,从一开始只以为小的元素才可以统一抽象,到后来发现连整个式子都是可以放在一起抽象的,最后用一个Derivable接口统领全部。
二单元感觉在电梯运行上面向对象的感觉不是很强烈,但我在策略类的使用上运用了继承抽象的思想,即所有策略类都实现同一个接口,在不同模式下重写调度所需的方法,策略类的对象在电梯初始化时就传给了电梯,电梯调度时直接调用接口里的方法即可(当然最后一次作业因为优化翻车怕了,最后用于调度优化的类只实现了接口,没敢重写算法)。感觉本单元架构的层次感还是比较强的,各层只管自己的职责,比如电梯只管运行和状态的转换,提供外部查询电梯信息的接口,而具体的优化则单独拎出来处理。
三单元主要循着JML规格推进的,只实现了官方接口,自己没有再增加更深层次的东西。
四单元的官方接口大量运用了抽象的思想,所有输入的元素都继承自顶端的UmlElement,为防止因不熟悉接口造成的错误,也为了调试时不跳进我不熟悉的类,我自己建立的类并没有继承自官方所给的类,而只是把官方的类对象作为了我自己类的一个成员变量,由我的类提供查询接口,调用官方类获得ParentId等信息。在我自己的架构中,主要是把MyClass和MyInterface用接口统一了起来,提供统一的与属性、方法、继承等有关的方法。
四个单元中测试理解与实践的演进
自动测试 (True)
给测评机debug (False)
我觉得整个学期最有价值的一次努力就是在一单元第一次作业那个周末决定尝试搭建测评机,算是有了一个非常好的开端。计组靠着大佬的测评机苟活了全程,只觉得看不懂但大为震撼,非常高端,以为这种东西对我来说就像是猴子骑象,也万幸没有因为大象太大而不敢往上跳吧。
第一单元自己动手丰衣足食,觉得自己搭建完整测评机的能力是必要的,共享能提高效率,但如果我连一个简陋版都不会写,可能不会有人愿意和我共享吧......第二单元开始建起了“小圈子”,建立了一个Gitee仓库,开始合作搭建测评机,甚至在过程中熟悉了git的更高级用法(pull不下来直接重新clone算不算?)。总体来说,圈友们实现了自动查找主类编译,多进程多人对拍,多份不同结果输出结果序号,自动邮件等功能(圈友好强!)
表示体验极佳下次还来!
一单元三次作业都是自己写的测评机,最初是用对拍的方式,但多项式的输出,各个项的顺序还是有差异的,不能应用于二三次作业,因此受讨论区启发,用Python的sympy库对输入进行求导,对输出和求导结果进行计算比较。数据的生成最初使用的是几个池做不同的组合,数据随机性并不是很大,而且池子的设置很容易受到自己理解的影响,想不到的似乎也就测不到。后来使用了Python的xeger库,用正则表达式自动生成规定长度的数据(那么问题来了,我怎么知道自己的正则表达式就一定对呢?)。再后来发现xeger生成的数据嵌套情况不是很全,就按照自己的理解写了一个递归下降,具体过程在一单元博客里提及了。开始确实对着电脑鼓捣了很久,但一旦体系搭建起来,之后就是闲来无事提高测评机使用体验了(比如while(true)跑它个一宿,错误数据输出到一个文件里存储早上起来收割?)
二单元实在太顶了,在冯如杯的疯狂push下险些作业都写不完,于是提交前来不及靠测评机测试,而且两次究极结束前优化出锅,肠子都悔青了。在这里万分感谢小圈子和大佬同学们捞了我一把[合十]......第一次作业没来得及写测试,直接全部鸽掉了,心痛,还是博客周休息过头了。后来的作业采用测评机和手动数据结合的方式,比如针对某个到达模式输入一些***钻的数据,测试电梯调度,调头等问题,但感觉除了非常明显的死锁,偶发的死锁应该还是需要大量随机数据的轰炸。时间依旧很紧张,基本都是截止的那个早上才开始测试,虽然每次都比同屋人多刀一两个,但感觉还是测试做的不充分。
三单元主要采用测评机验证正确性,读代码查时间复杂度的方式,当然也用了junitng,JUnit等工具链进行测试(虽然依旧觉得没啥用),还面向某部分指令写了专门的数据生成。这一单元起数据生成不再是纯随机的,不然测到的绝大部分都是抛出来的异常。写数据生成时我用了两种方法,一是缩小随机的范围,使得随机出来的查询数据有更大可能在图里;二是在生成器里建起关系图,查询指令一部分查询图里的结点,另一部分随机图外的结点。
四单元依旧测评机,本单元只需要在三单元的基础上对数据生成做修改,其余均可保留原样,方法仍是在生成器里建起图,在图里随机用结点。我负责写顺序图相关数据和几个check方法的测试,这单元内容更碎,小错也更多,有时候几个人对拍只有一份输出是对的,测评机跑了两天,几万个点,直到最后还有新的bug被爆出来。不过因为犯懒没用starUML手造特殊测试用例,只造了一些小的特例用于测试时间复杂度、是否会爆栈等。
最后附上一份究极对拍结果,祝大家暑假快乐!
课程收获
一路走来,或许也只有在尘埃落定时才敢回头看看,才敢说真的有努力,也真的成长了很多。可是必须承认,过程中充满了欢乐,但也确实不乏痛苦,可能也正是困难才让整个过程的每一节都是回忆里跳跃的存在。
刚接触了半年计算机,所有的实践仅限于计组、C语言和数据结构,甚至打开命令行还一脸懵逼,连Python输出到文件都要现学现卖,这也是站在终点看起点时奇幻感觉的由来。有时候决定开始,面对未知,本身就是一件很容易自我感动的事。测评机让我在面向对象之外收获到了更多的东西,而奇幻感似乎就来源于,这一切只是因为脑子里闪过了OS重定向输出的知识,于是笨拙地在CSDN搜来搜去,对着代码反复尝试,折腾了几个半天。
二单元OO巅峰,OS开始冲顶,冯如杯ddl疯狂push,一个词,不知所措。于是有了某次到周六晚上还没有想好电梯的架构,不争气地哑着嗓子给家里打电话,差点哭出来。紧张之余想起了荣老师经常在c语言课上的“宣传”:OO能把人肝哭了,好家伙,亲身实践了。也有某次冯如杯肝到凌晨四点半,睡俩小时起来考蓝桥杯,回来加紧修死锁的电梯。现在想想也有点遗憾的,互测绝佳体验月,竟然被很多事分了精力。
当然,课程中肯定收获了面向对象的思想,在一次次实践中学会了如何进行抽象,我们操作的不是一个具体的类,而是抽象出来的接口,继承和实现像是一条线索贯穿了OO的始终,灵活运用可以使代码更加简洁优雅。我学会了面对一个大的项目,面对谜语一样的设计要求如何静下心来思考,如何做好设计准备,养成了较好的代码风格,也在互测阅读同学代码时收获了很多启发。诚然,所学只是面向对象编程宏伟宫殿里的小小一本书,但这门课程真的是一个很好的启蒙。
合作写测评机push着我学会了git更高级的操作,这也是一个受益终生的契机。
知识之外倒也收获了不少,比如心理素质[笑],结束前半小时紧急修锅,结束前一天才开始写代码,心理素质弱点儿还真不一定顶得下来。从前评测加载“转圈”的时候真的紧张得不敢看,现在强测蹦出来个八十多分还能喊一句好耶,互测刀和被刀都很刺激,也逐渐接受不完美或失败是常态,想来以后受挫时也能更强些。
研讨课上第一次尝试了无讲稿的报告,以前做展示都是写好稿子临场背稿,现在才发现,如果我认真做了,脑子里有东西,对着PPT是可以滔滔不绝的。(后来的报告都不写稿了,偷懒的良好开端)
三个具体建议
建议1
得益于两个预习任务,我第一单元作业进行的相对顺利,至少从一开始就比较清楚自己要做什么。但第二单元就出现了强烈的推背感,具体就是不知道该干什么,感觉就在摸着石头过河。这好像并不是少数人的感觉,似乎身边很多人那一周都在不知所措。所以我希望预习部分能增加一点多线程相关的任务。可能也不必真的写一个多线程,毕竟还需要课上的讲解,但或许可以有一些相关设计模式的训练,比如单例模式,观察者模式等,旨在让大家先对这些模式有个印象,知道大致是能做什么,这样也不至于开学后匆匆忙忙学习后不会应用。
建议2
如果我没记错的话,表达式求导单元似乎不支持括号的幂次,最后化简后还需要拆开,建议使输出支持这种形式。这一单元很多同学都将x**2优化成了x*x,我觉得这种做法并不优雅,虽说用户的需求总是多种多样,但让大家为了性能分做这种破坏统一形式的优化还是有些奇怪。
建议3
JML那部分确实感觉有点水,相比之下写规格远比读规格更难,但是如何验证写的规格的正确性似乎更难。我觉得可以将一次作业换成写规格,可能也不用太多,两三个方法?然后互测分为两部分,第一步分屋同学们互相读规格,提交错误原因,第二部分再分屋,同学们读规格和上一个同学给出的评价,绝大部分评价相同的进行相应的扣分,正误参半的由助教重新评审。
额外小建议
建议互测增加显示“刀中了哪个人”的功能,某次互测以为刀中了结果刀偏了。
最后