OO第四单元总结

OO第四次博客作业


 

终于到了令人愉快的暑假最后一个单元啦,在完结撒花之前,让我们先来完成这最后一次轻松的OO的博客作业。

第四单元总结

第四单元的核心内容就是UML的一些基本处理,这个阶段的作业跨度相对前面几个单元较长(也非常感谢贴心的OO助教和老师们考虑到我们考试的原因予以延期),所以我们有了充足的时间去理解这次作业的一些有关内容。

其实有关UML的知识其实很多,在简单的查阅资料后我发现它能够体现的类之间的关系并不只是有我们作业所涉及的继承(Generalization)、实现(Realization)和关联(Association),还包括了依赖、聚合、组合等关系,但是为了贴近这个单元的两次作业,所以在这篇博客中我仅仅就两次作业中涉及到的点来进行说明和讲解,若有不当或失实之处,还望老师和同学们予以指正。

一、涉及到的工具

这个单元我们主要利用的工具就是StarUML,我们可以利用这个画图软件工具来绘制我们希望实现的类或者接口之间的关系,而并不需要自己挖空心思去写代码考虑架构,然后就可以结合作业给出的jar包并利用cmd来将我们绘制好的类图导出成本次作业使用的标准输入格式,来对我们的代码加以测试,可以说功能已经是非常的清晰和完善了。

当然StarUML并不仅仅可以用来画类图,第二次作业涉及到的状态图和顺序图也都可以一并用它绘制出来,具体的操作就是点击Model下的Add Diagram,对于状态图我们新建一个Statechart Diagram即可,对于顺序图我们选择Sequence Diagram即可。

同时它还有一个很方便的功能,那就是绘制已经写好的java代码的类图,操作同样很简单,在安装java插件之后,选择Tools->java->Reverse Code...,然后直接选中整个java工程即可。

当我们掌握了正确使用这项工具的姿势后,我们就可以很轻松愉快的开启第四单元的作业之旅啦。

二、两次作业的设计

(1)第一次作业

其实第一次作业我的大部分时间都花在了一件事情上,那就是读源码,虽然不知道为什么直接复制开源包的源码到工程里没办法编译通过,但是对于源码中关于element的部分我却丝毫不敢疏忽,因为这是整次作业做关键的地方。

在一开始写代码的时候我做了一件非常愚蠢的事情,那就是单纯地把读取到的所有元素都当做UMLElement来处理,但是后来我发现这样做的后果是我不知道怎么样获取不同种类的元素对应的属性,例如AssociationEnd的source和target等。于是我开始怀疑自己代码书写方式的正确性,便去阅读源码,果然我发现了在众多的UML类型中,UMLElement是一个顶级类,其他的类都是作为子类继承了这个类,我们只需要简单的进行一个类型转换,便可以对子类中的一些方法进行访问。在解决了这个问题之后,这次作业留给我的问题就已经为数不多了。

在第一次作业中我们一共要实现九条指令,而其中绝大多数的指令都是进行简单的计数,例如获取类操作数量,只需要在输入的时候将所有的类提取出来,再将所有的操作(Operation)提取出来,根据类的ID和操作的ParentID进行匹配,最后计数即可,但是在这里,部分指令需要考虑到子类继承父类的情况,写法也并不难,其余几条类似的指令也是同样的过程。

在整个第一次作业中最为复杂的一条指令也是最容易出错的一条指令就是获取实现的接口列表了,虽然一个类只能继承一个类,或者实现一个接口,但是接口是可以多继承的,所以计算起来很容易出现重复计算或者漏算的情况,在这里我使用了如下的算法,先贴上代码:

    
 1     /** 获取实现的接口列表CLASS_IMPLEMENT_INTERFACE_LIST*/
 2     public List<String> getImplementInterfaceList(String className)
 3             throws ClassNotFoundException, ClassDuplicatedException {
 4         if (!nameList.containsKey(className)) {
 5             throw new ClassNotFoundException(className);
 6         } else if (nameList.get(className) > 1) {
 7             throw new ClassDuplicatedException(className);
 8         } else {
 9             UmlClass classFound = getClassByName(className);
10             HashSet<UmlInterface> list = new HashSet<>();
11             getInterList(classFound, list);
12             ArrayList<String> list1 = new ArrayList<>();
13             for (UmlInterface key : list) { list1.add(key.getName()); }
14             return list1;
15         }
16     }
17 18     private HashSet<UmlInterface> getInterList(
19             UmlClass classFound, HashSet<UmlInterface> list) {
20         UmlClass son = classFound;
21         addInterList(son, list);
22         while (!sonToFather.get(son).equals(son)) {
23             son = sonToFather.get(son);
24             addInterList(son, list);
25         }
26         return list;
27     }
28 29     private HashSet<UmlInterface> addInterList(
30             UmlClass son, HashSet<UmlInterface> list) {
31         for (int i = 0; i < classToInter.get(son).size(); i++) {
32             UmlInterface inter = classToInter.get(son).get(i);
33             list.add(inter);
34             getFather(inter, list);
35         }
36         return list;
37     }
38 39     private HashSet<UmlInterface> getFather(
40             UmlInterface inte, HashSet<UmlInterface> list) {
41         for (int i = 0; i < interSonToFa.get(inte).size(); i++) {
42             list.addAll(interSonToFa.get(inte));
43         }
44         if (interSonToFa.get(inte).size() == 0) { return list; } else {
45             for (int i = 0; i < interSonToFa.get(inte).size(); i++) {
46                 getFather(interSonToFa.get(inte).get(i), list);
47             }
48             return list;
49         }
50     }

首先,为了防统计接口的时候出现重复的情况,也就是下图这样的情况:

 

我采用了HashSet来存放接口,这样哪怕多个类同时实现一个接口,也只会统计一次。然后从给定的类开始,做以下循环:

  1.对当前的类进行访问,查看是否有被实现的接口,若有,进行递归查找该接口继承的接口,并将这些接口全部添加到HashSet中,访问完毕后,进行2操作。

  2.接口访问完毕后,如果当前的类有父类,那么访问父类,对父类进行1操作,否则结束。

这样一来所有被实现的接口都至少被访问了一次并进行了添加,既不会重复也不会遗漏。

 (2)第二次作业

第二次作业相较于第一次作业而言就要难了不少,因为引入了两个新的概念:状态图和顺序图。这次作业要求我们在继承第一次作业的基础上,实现九条新的指令,分别是对状态图和顺序图的一些查询,以及对类图一些规则的检查。在这里我想重点讲一下其中的三条指令,分别是:获取后继状态数检查UML008规则检查UML009规则,下面我将一一说明这三条指令的一些难点和细节。

1.获取后继状态数

一开始我以为后继状态数就是简单的数数问题,但是在我二次思考以后我发现它本质上是一个连通图的问题,并且在这里,按照吴际老师所指出的所有的final state都视为同一个状态,关于这个问题的讨论在讨论区和大班群里都变得愈发焦灼激烈起来,但是后来课程组给出了只会出现一个或零个final和initial状态的限制条件,并且吴际老师也给出了final不会有outcoming、initial不会有incoming的说明之后,这个问题就变成了一个标准的有向连通图问题,处理方法就变得丰富多彩了,在这里我个人采用的是Floyd方法(但是没有想到在这里给自己挖下了一个很大的坑)。Floyd方法过程中有一个操作,那就是会在初始化图的时候把自己和自己判断为连通,并且在后续判断连通时跳过,但是显然在这次作业中我们并不能这样来进行处理,于是我一开始将自己和自己判断为非联通,在后续判断时不跳过,这样一来我们得到的图就和实际相符了。但是在中测的过程中,我发现自己weak5和mid5始终无法通过,(在这里万分感谢郭神提供的思路)于是我用如下的状态图去测试自己的代码:

 

发现问题就在于当我的状态图循环回到请求的状态时,我会漏记一次数,在我将这个bug成功修复后,也就愉快的通过了中测。

在这里做一个小的思考延伸,在本次作业中课程组为我们限制了initial和final state的数量只能为0或者1,但是如果可以有多个的话,我们应该如何去处理,其实很简单,只需要在计数的时候利用两个flag记录是否有initial或final state,如果有,将最终的count加一即可。

2.检查UML008规则

R002规则主要是对循环继承的检查,具体指导书的说明如下:

该规则只考虑类的继承关系、类和接口之间实现关系,以及接口之间的继承关系。所谓循环继承,就是按照继承关系形成了环。

但事实上我们只需要考虑三种关系中的两种,就是类的继承和接口的继承,因为并不存在接口对类的继承,所以一个环中并不可能同时存在类和接口,所以我们只需要分开考虑接口和类的循环继承即可。

类的继承很简单,因为单一继承,所以只需要顺着父类一直往上找查看是否出现重复类即可(虽然在这里我的代码出现了一些小小的问题导致强测最后一个点没有通过)。

而接口的继承则较为复杂,在这里我再次使用了Floyd来处理连通图,如果一个接口和自己连通,那么他就是循环继承了自己。

3.检查UML009规则

R003主要是对接口重复继承的检查,这里我利用了前一次作业求实现的接口列表的部分代码,因为这条检查本质上就是遍历一个类或接口继承或实现的所有接口,我使用了一个HashSet来存放所有继承或实现的接口,一旦在遍历的时候出现重复的接口,那么就不符合规则,非常清晰明了。

三、度量分析

第一次作业

既然本单元学习了StarUML的使用,那么我觉得利用这个工具来绘制这两次作业的类图简直再合适不过了。

第二次作业

(由于第二次作业实在有些复杂,如果看不清具体内容还请见谅)

 

 

然后是代码的复杂度,利用Statistic工具生成

第一次作业

 

第二次作业

 四、bug修复

讲讲我在第二次作业中出现的bug,在最后一个而测试点处发生了死循环,由于类图看起来太复杂,最后我通过程序的一些输出大致猜想类图应该是这个样子的:

 

当我查询Class1时,类图在Class2、3、4三个类之间发生循环,导致既不会找不到父类,又不会回到Class1,这样程序就无法停下来而进入死循环。解决方法也很简单,利用一个HashSet将已经访问的类储存起来,如果当前访问的类已经存在于HashSet里,那么就停止,这样问题就得到了解决。这里把代码贴出来:

    
 1 private HashSet<UmlClassOrInterface> checkClassCircle(UmlClass key) {
 2         HashSet<UmlClassOrInterface> list = new HashSet<>();
 3         HashSet<UmlClassOrInterface> repeat = new HashSet<>();//已经访问的类
 4         list.add(key);
 5         repeat.add(key);//将查询的类储存起来
 6         UmlClass son = key;
 7         while (classSonToFa.get(son).size() != 0) {
 8             son = classSonToFa.get(son).get(0);
 9             if (son.equals(key)) {
10                 return list;
11             } else {
12                 if (repeat.contains(son)) { break; }//如果当前访问的类已经存在,停止
13                 repeat.add(son);//将当前访问的类储存起来
14                 list.add(son);
15             }
16         }
17         return null;
18     }

 

这就是对于第四单元作业的全部总结

 

以为OO到这里就结束了吗

并不是

下面让我们来回顾一下整个OO血泪史学习过程吧

 

OO血泪史回顾

首先用一个状态图来描述一下这个学期的一些经历吧

 

四个单元一路走来,我从寒假一个俩小时憋不出来二十行代码的萌新,变成了如今两天一千七百行代码的超级萌新,到底经历了什么呢,容我细细道来。

从萌新到超级萌新的OO之旅

为了提升各位看官们的阅读体验,我还是按照作业的顺序来依次进行讲解吧。

第一单元

第一单元要求我们对给出的表达式进行求导运算,主要考察的是正则表达式的匹配和应用,三次要求都在前一次的基础上依次递增,因为之前并没有足够的经验,所以我在这个单元对代码进行了两次重构,确切的说,每次的代码都是全新的风格。我到现在还记得第一次作业结束后,荣文戈老师在讲台上问,有多少同学整个代码只有一个类的,班上有一些人不好意思的举起了自己的手,当然,那里面免不了有我。为了进一步触景生情,我打开了自己的第一次作业,发现我竟然还使用了内部类(滑稽),我仿佛还能感受到几个月前那个完全不知道什么是面向对象的我坐在电脑前,看着指导书抓耳挠腮的样子和心态。

但是从第二次作业开始,我就尝试着去理解面向对象的概念,把执行不同功能的代码放入不同的类中,我一共设计了三个类:Main、处理多项式的类、处理单项式的类,虽然整体架构和代码风格依然是不堪入目,但是相比起第一次作业还是有了些许进步。然后是第三次作业,在第三次作业中,我应该算是较为完整地将面向对象的概念体现了出来,我将代码分成了Main、多项式、项、因子,而因子类则包括了有幂函数子类、正弦函数子类和余弦函数类这三个部分,应该来说从第三次作业开始,我对面向对象的理解有了一个较大的提升,对java的各种数据框架和API也有了较为熟练的应用。

第二单元

第二单元主要考察了多线程的知识,其中有一个很关键的知识点“锁”的概念贯穿了整个单元的作业,如何使用锁,哪些地方需要用锁,什么时候锁被谁拿走,什么时候锁被归还,谁可以拿锁,谁不能拿锁,这都是多线程代码编写过程中需要考虑到的问题。

第一次作业很简单,一部电梯,傻瓜调度,只需要三个类就能完成:Main、调度器和电梯,标准的生产者消费者问题,而请求队列则是二者必须互斥访问的资源,这样一来,什么时候调度器工作,什么时候电梯工作,就全部一目了然了,我们只需要顺着这个思路用代码实现即可。

第二次作业则是在第一次作业的基础上对调度算法进行了提升,我们需要在调度器中添加对请求的排序,以方便电梯对满足条件的请求进行捎带,同时电梯也需要做到记录多条指令的详细信息,确保所有人都在正确的地方上下。

第三次作业则是三部电梯同时运行,规定每一部电梯只能在固定的楼层开关门,也就涉及到了最关键的问题:换乘,在这里我设计了一个很巧妙的方法来进行和记录换乘的信息,妈就是我新建了一个类,在这个类中对PersonRequest进行了扩展,当换乘的时候,这条Request的信息就会随之改变更新,并且我在调度器中添加了一个count变量,用来判断中转请求是否执行完毕,只有当同时满足输入为null、中转指令全部运行完毕、调度器请求队列为空时,调度器才结束运行。

第三单元

第三单元的作业具有很好的继承性,我几乎每一次的作业都成功继承了上一次的作业,主要考察的内容就是增删改查,我觉得实际上就是检验我们对数据储存查找的一些方法是否科学。

前两次作业都比较简单,涉及到了一些最基本的图的知识,即可达矩阵和最短路矩阵,只需要用floyd算法或者dfs算法即可很好的得到解决。

第三次作业的难度则相较前两次较大,但是本质上是对图的一些升级,增加了边权的一些计算,整个的数据结构并没有收到任何改变,只需要建立起正确的索引,确保初始化和赋值的过程正确,就并不会有太大的问题。

第四单元

第四单元也就是刚刚过去的类图的一些操作,在前面已经详细讲述过了,就不再进行赘述啦。

再讲一点和测试有关的东西

相信不少同学听到强测和互测这两个词就很头痛(大佬和狼人除外),总管整个OO学习的过程,那些代码写的最好的,鲁棒性最高的,刀人最狠的 大佬们无一例外的都有着属于自己的高强度测评机,因此就可以不再依赖强测或者互测来检验自己的程序是否还存在这样那样的问题,而只需要将测评机跑起来,然后去干别的事就ok了,这着实很让菜鸡我感到羡慕,于是我也在这几个月里尝试着去写了几个属于我自己的弱化版测评机(每次跑起来笔记本风扇就起飞,着实心疼)。

按照我自己的理解,首先,好的测评机需要有一个良好的数据生成器,有了刁钻的、边界的、足够复杂的测试数据,我们才能对程序里一些可能被遗漏的地方进行检查。其次,一个好的测评机需要有一个能产生正确答案的答案生成器,在第一次作业里,我们完全可以利用MATLAB或者python来计算应该输出的结果,并且与我们自己的答案进行比较,但是在后面几次作业里这一点看起来就比较困难了,所以我们需要一个靠谱的小伙伴,来和我们进行对拍,相信绝大多数同学都或多或少了实践了这一点。然后我们需要做的就是让测评机“自己不停的跑,直到产生不一样的输出”,之后进行相应的debug就可以啦。

是不是会写测评机就够了呢?OO告诉我们,不尽然。

很多问题并没有固定的输出,所以我们还需要掌握一门叫做单元测试的技术,也就是我们在第三单元里用到的JUnit等工具,利用它,我们可以做到对程序的所有判断条件进行完全覆盖式检测,保证边界条件不会出现问题,或者发生我们没有处理的异常和错误。

当然了,在面对第四单元这样的作业时,手动构造一些测试样例也是必不可少的,关键就在于构造的数据是否具有一定的复杂度和特殊性。

当我们熟练掌握了以上方法之后,我们也就离成为一个真正的大佬(狼人)又近了一些。

一点自己的话

虽然发际线又高了不少,但是OO这门课真的让我感觉收获了很多,除了如何求导、如何设计电梯、如何设计地铁、如何使用画图工具之外我真正的明白了什么叫做面向对象的思维,并不再是之前那个那道题目就开始疯狂写代码的少年了,我学会了运用新的眼光去看待一个要求,去对它进行分析。

同时,我也真正地感受到了自己身上发生的变化,实力的提升,具有了一定的工程代码的能力,知道了什么样的代码是有质量的代码,什么样的代码风格应该避免,尤其是Checkstyle的使用让我极大地规范了自己的代码风格,有意识地去做好命名、缩进、换行等细节。

当然正如我前面讲到的,我还学会了很多工具的使用,一些检测代码、纠正错误的方法,很大程度的提升了我对自己代码的掌控能力。

我真的觉得课程组在这门课上花费了相当大的经历来做好这门课,它也确实让我们感到非常的惊艳,收获了很多的东西(除了头发)。

给课程提的三个建议

首先是有关强测中出现的同质bug,这个问题其实之前有同学提过意见,我本人也已经经历了一次修复数个强测bug的事情,我觉得这个现象可能确实会稍微对同学们的信心造成一些打击,毕竟大多数人也真的是用心在写自己的代码并且测试,可能由于这样那样的问题,也许是能力确实不够,也许是漏掉了一个非常小的细节,但是偏偏被强测放大了,希望助教和老师们可以设计出更好的分数计算方式来让同学们把精力更多地放在对自己的代码进行完善而不是如何得分上。

其次是理论课和实验课的时间问题,这个问题的好坏我并不是特别好去评价,因为趁热打铁并不是一件坏事,但是一个中午的时间可能确实对于一些需要午休又担心实验的同学是一件不太理想的情况,如果可能的话,希望课程理论和实验课的时间错差能够长一些。

最后,关于bug修复,个人感觉5行的限制确实是少了一些,即使是同质bug,增加一个判断条件也就至少占据了三行,可以稍微放宽一些,也许10行的限制可能会让大家和助教都轻松一些。

除此以外,OO真的是一门很让人有成就感的课程,哪怕肝的过程很让人痛苦,看到自己ac中测和强测的一瞬间也都烟消云散了。

完结撒花的最后一点话

终于到了结束的时候(放假之前最后一件事也终于做完了),很感谢助教和老师一学期以来的辛苦和陪伴,虽然也为这门课熬了不知道多少个夜通了多少个宵,但是都已经走到这里了,还是十分开心和激动的。希望这门课能发展的更好,成为6系神课(其实已经是了)。

最后附上一张图(滑稽)

 

posted on 2019-06-22 19:26  S1mpleee  阅读(303)  评论(0)    收藏  举报

导航