第四单元总结

一、作业架构设计

本单元作业尽管逻辑并不复杂,但工程量是较大的,尤其是第一次作业。

本单元的作业是json格式描述的UML图的解析器。其中,所有的解析程序都已经给定,并会向指定的类的构造函数传入一个包含了所有UML图元素的数组。

UML图包含三种类型:

  • UML类图:包括类和接口,类和接口之间的继承、实现,类和接口之间的关系,类和接口内部的方法(包括参数)、属性
  • UML顺序图:包括属性,属性的生命线,消息,终结点
  • UML状态图:包括起始状态、终止状态、状态,状态迁移、迁移触发事件,状态行为

其中,每个UML元素都具有一个_id域和一个_parent域,分别代表自身的唯一标识符和所属上级的标识符。

本单元的作业本质上是图的搜索问题。想要对图进行搜索,首先要将各种UML类型使用适配器进行包装并将每个变量链接起来。这就涉及到一个问题:当_parent所指向的元素的适配器尚未建立时,将无法进行链接。因此,我采用了将数组遍历两次的方法,第一次只建立适配器,第二次进行包括链接在内的填补。

另外,为了方便地对同名称的元素进行查找,我设计了泛型表MultiMap,一个键可以对应多个值。

前两次的作业没有什么难度,因此不赘述实现逻辑。我在前两次中对于所有的UML图元素都制作了适配器,但UmlOpaqueBehavior和UmlEvent在作业中并没有出现任何相关的查询或检查,故我在生成适配器时并没有生成这两类适配器。在第三次作业中,加入了模型有效性检查,其中有三条如下:

R002:不能有循环继承(UML008)

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

R003:任何一个类或接口不能重复继承另外一个类或接口(UML007)

  • 规则解释:
  • 该规则考虑类之间的继承关系、接口之间的继承关系,包括直接继承或间接继承。

R004:任何一个类不能重复实现同一个接口(UML009)

  • 规则解释:
  • 该规则考虑类之间的继承关系、接口之间的继承关系,以及类对接口的实现关系,包括直接继承或间接继承。

其中,又规定类不会出现多继承,因此类不可能重复继承类。

若将UML类图视为一个由类和接口组成的有向图,则R002就是在寻找该图之中的环路。但是仔细思考就会发现该问题的本质并非环路,而是强连通分量。因此,若找到图中超过1个点的强连通分量,则说明找到了循环继承。

强连通分量的算法有Tarjan强连通分量算法等可供参考。

关于R003和R004的解法,我发现多数人选择了暴力寻找的方法,从每一个类向上寻找是否有重复的继承(实现),优化也多是找到了就将其子类一并标记为重复继承。然而,反向思考一下就会得到一个更简便、复杂度更低的方案:先将所有接口的最高级祖先找到,即先将图中出度为0的点找到;再从每一个最高祖先,分别反向地向下进行遍历,若存在重复继承的点,该点一定会被遍历两遍以上。这一做法需要确保图中不存在环路,因此需要在确保R002正确的条件下进行。

 

 

二、编程方法进化

我的架构设计方法实际上并没有什么较大的变化。我在学习该课程之前,就已经一定程度上接触到工程,并曾经完成过一个数千行的游戏模板,对于工程的架构有一定的了解。

在拿到问题的一开始,我的习惯是先思考整个工程需要什么样的模块。计算机组成的课程教会我高内聚低耦合的设计思想,这一思想在各种工程中都适用。因此我将整个问题划分为模块之间的交互。例如在电梯的作业中,我首先将整个工程划分为输入端口、电梯、电梯控制器、任务分配器这几个部分。划分了这几个模块之后,就可以去思考每个部分各自的功能如何实现,而非直接思考整体的问题;且模块化使得工程容易拓展,无论是在模块内增加功能还是增加新的模块都并不是难事。

对于一个细节的问题,我会首先思考应该使用什么样的数据结构。选错了数据结构,将会导致工程的代码变得冗杂,并导致性能大打折扣,因为没有好的数据结构就谈不上好的算法。例如,对于多项式的存储,我采用了表达式-复合项-简单项的存储方式,通过TreeMap自动排序的特性和重载compareTo方法的方式进行处理。

在算法设计上,应尽可能考虑复杂度低的算法,能用O(n)就不用O(n²),能用O(1)就不用O(n),否则在数据集的规模增长的时候,运行时间将会急剧增长,最后到无法忍受的程度。特别是图的查询方法,好的算法和差的算法之间的运行速度差距往往不只是几倍那么简单。对于需要经常查询但不常变化的计算结果,可以考虑将计算的结果缓存并在数据更新或查询的时候更新;若不常查询但经常变化则应选择实时计算。算法和数据结构紧密结合,才能设计出真正优秀的程序。

在多线程设计上,合理的同步互斥机制是极其重要的。Java使用的管程虽然简单易用,但在嵌套时极其容易造成死锁。这就造成了一个问题:若想要两个线程相互唤醒,若使用嵌套管程将会造成死锁,但不进行嵌套又会导致两线程同时阻塞。因此在这种问题上需要花大量的心血进行调整。另外,可以使用信号量、可重入锁等更为灵活的方式,这样便不易造成死锁或同时阻塞。

在代码风格上,要使得自己的代码便于理解,尽量不要出现三层以上的循环和较长的方法、连环内嵌类这种不便于理解的结构,否则会加深理解难度,导致在经过一段时间之后,连自己都不知道自己代码的逻辑是什么样的。checkstyle在一定程度上限制了这种不便于理解的代码的产生,但仍需注意代码的可维护性。

 

三、测试方法进化

尽管我使用过JUnit进行测试,我测试的最主要途径仍然是随机生成的样例和评测机。

JUnit到底好不好用?我的答案是不好用,而且是很不好用。JUnit提供了一种使用手动构造的样例进行单元测试的方法,换句话就是说可以将所有的public的方法单独地使用手动构建的例子进行测试,且测试结果的标准答案是人工计算得来的,并且不能通过从控制台输入数据进行测试。JUnit的局限性导致只能对可以想到的情况进行测试,而大意导致的错误、隐蔽而难以想到的错误没有办法得到充分测试,也可能由于人工计算失误或问题理解错误而得到错误的结论,从而放过那些最要命的bug。若是去重新研究自己的代码逻辑,并寻找一个可以遍历所有分支的测试集,JUnit可以作为一个可行的测试工具,但作为学生实在没有这么多的时间在每个单元的测试上都下这样大的工夫。

使用加权随机得到的样例,可以用数量代替质量来对bug进行暴力查找。使用评测机对输出结果进行检验,就可以得到正确性的判定。对于一些没有办法通过与问题解决方法不同的方法进行正确性判定的问题,一般可以使用和其他实现的输出进行同一性判定的方式进行检验。我对每个单元的作业都采用了这样的方法。

1. 多项式求导的测试

对于多项式求导的正确性判定,我采用课程组在指导书中提到的sympy库作为标程。使用xeger库可以通过正则表达式生成符合该表达式的随机字符串,因此可以通过循环使用xeger来达到生成嵌套表达式的效果。使用生成的表达式作为输入并得到输出,代入sympy进行求导并检验是否相等。使用这种方法让我避免了一些致命bug,例如稍不注意就会出现的空指针,以及深浅拷贝问题。

2. 电梯调度的测试

电梯调度想要给出一个答案并不容易,但检验一个结果是否正确实际上很简单。只需要检测电梯的每一步是否合法,以及最终是否所有请求都被满足即可。电梯的每一步是否合法,体现在两层的到达之间时间之差是否大于等于电梯在每两层之间运行的时间、乘客的出入是否在电梯的open和close之间、乘客的出入和楼层是否匹配等。最后,检查所有的请求是否都已经满足。

3. JML规格的测试

该单元的问题实际上是图论问题,需要随机生成一个图来检验。我在这个单元使用全随机的生成器,在指令达到一定条数之前更加可能生成person,在达到之后生成的可能性下降。在一定的概率下,我使得生成错误的查询,否则生成可以查得结果的查询。该单元的检测并非不易编写,而是该单元没有另外的方法可以对该问题进行检验,因此必须使用一个标程来对照。这就引起了鸡生蛋的问题,因为如果我写的算法是错的而不自知,那么我写的标程将会也是错的。因此需要使用其他同学的jar包进行对比。

4. UML图的测试

该单元的问题也很大程度上是图论问题,且总体难度低于JML单元。检测的方法与JML单元基本相同。

 

四、课程收获

在四个单元的课程中,我学习到并实地演练了多线程编程,实际体验了迭代开发,并侥幸地没有进行重构。面向对象课程训练了我拆解问题的能力、对算法举一反三的能力、对代码进行正确性检验的能力等,并且肉眼debug的能力上升了。我也对Java的运用增加了一定的熟练度,了解到了更多我不熟悉的语言元素,如管程、泛型、反射等。

 

五、课程改进建议

  1. 实验课提出的问题模棱两可,表述模糊、语义不清,问题内容具有错误和歧义,令人甚至要为了做题去揣测课程组的想法。建议进行严格的整改,出题之后出题者之外的老师或助教应先做一遍并对所有错误或可能产生歧义的地方进行修正,不要用自己的过失惩罚学生。
  2. 理论课内容讲述节奏不好,特别是UML单元,在看完视频的内容之后甚至并不能对UML有一个清晰的基本认识,各种元素之间的关系没有展开阐述,一些容易混淆的概念没有得到任何的解释(如”关联“、”组合“、”聚合“),需要课下花大量的时间去网络上找到答案。不过至少比OS课好。
  3. 每个单元的各次作业之间难度梯度大,建议在发布某一次作业的同时对下一次作业的部分内容进行预告,这样可以减少学生为不必要的重构而浪费的时间,也可以循序渐进地对学生的能力进行训练。

 

六、课程体会

本学期的面向对象课程整体而言较为令我感到遗憾,也许是有线上授课的因素在里面,课程讲得没有线下那么生动,我也经常左耳进右耳出。我通过这门课程还是学到了不少东西的,我的编程技能也得到了大量的磨练,但是我不是很愿意费尽精力去猜测课程组指导书中的想法。我希望,这门课程能够越来越好。

 

你可能感兴趣的:(第四单元总结)