OO第四单元及课程总结

OO第四单元及课程总结

架构设计


UMLElement为由子节点指向父节点的树,但查找时需要从父节点向子节点查找,因此解析UML的关键就是重建树转为父节点指向子节点。本次作业中,主要采用了组合的策略。在解析UML图时,对UML图中的各个元素,采用了在UMLElement外面包装一层MyUMLElement,同时保存树状关系信息的方式。解析时,对UMLElement进行分类,按照先解析父节点,再解析子节点,在解析子节点的同时将子节点的信息保存到父节点中的方式进行解析。

对于MyUMLElement,采用了MyUmlElement \(\rightarrow\)UmlXXMetaObject\(\rightarrow\)UmlMetaObject的继承结构,UmlMetaObject提供向UmlElement中相关字段的查询接口,UmlXXMetaObject提供对该UmlElement所属Interaction的查询接口,比如通过id查找类名等。

public class MyUmlXX extends UmlXXMetaObject {
	Set<String, MyUmlXXChild> name2child;
    
    // some methods about query
}

public class UmlXXMetaObject extends UmlMetaObject {
    xxInteraction xx;
    public UmlXXMetaObject(UmlElement element , xxInteraction xx) {
        super(element);
        this.xx = xx;
    }
}

public class UmlMetaObject {
    public UmlMetaObject(UmlElement element) {
        this.element = element;
    }
    
    public String getName(){
        ...
    }
    public String getId() {
        ...
    }
    // some other methods
}

部分UML类图如下:

image-20210626154520505

查找时,主要通过哈希表进行加速。同时有一些查找采用了初次访问时缓存的策略,建立树结构时并不计算,在第一次访问时进行计算并缓存,之后的访问直接返回缓存的值。

一些特殊实现

继承与实现

采用多叉树的方式,子类的MyUmlClassOrInterface可以找到上一级父类/实现的MyUmlClassOrInterface

参数不同的同名方法

采用c++的重载实现原理,将类型信息拼接到方法名的后面,原本的方法名只用作一级查找索引,映射到同名方法的重载后的方法名集合,重载后的方法名则映射到MyUmlOperation

R002

虽然继承与实现已经构建了树\图结构,但如果直接在这个图上运行经典图算法,判断是否已经访问过需要采用Set的方式,效率较低,而且由于继承与实现的存储方式不同,包括可能需要写好几遍同样的算法,因此建立由图中节点到自然数的一一映射,之后的图算法运行在自然数图上。

采用tarjan算法进行强连通分量的分解确定循环继承

R003 R004

通过递归的动态规划算法,查找任一两节点之间的路径个数。由于通过了R002,可以确定没有环,对于图\(G(V,E)\)将节点\(u\)到节点\(v\)之间的路径条数记为\(f(u,v)\),容易得出关系\(f(u,v)=\sum\limits_{<u,p> in E}f(p,v)\)

同时定义递归边界\(f(u,u) = 1\),可以以\(O(V^2)\)复杂度完成一次检查。

各单元理解


第一单元

理解

我认为第一单元中最重要的时理解抽象这一概念。在解析表达式时,如果采用了模式匹配的方式,每次添加规则都需要重写正则表达式或匹配规则,很难利用上一次写出的规则。同时,对表达式的存储方式也是,每次都要做大量的改动。但如果采用词法与语法两级分析,存储时采用抽象语法树的方式,就能具有较好的扩展性。

归根结底,设计架构时应该充分考虑抽象,架构不可能脱离实现,但不应与实现有过强的耦合关系。

另一方面,第一单元中很重要的一点就是,抽象是有代价的。一般而言,抽象程度越高,架构的复杂度就越大。因此,需要在抽象程度与复杂度之间做一个权衡。

实现

为了追求扩展性,从最开始我就采用了递归下降来解析,这也导致了我的第一次作业的代码比绝大多数人都要复杂许多。但由于抽象程度不够,没有做词法分析,之后的作业还是几乎完整的重写了。

第二单元

理解

第二单元,我认为最重要的是理解并掌握封装接口的重要性。在OS课程中,讲解进程同步的PV操作时,曾提到过PV操作的缺点就是PV操作会遍布整个实现,对于一个复杂系统,很难进行集中的控制,而且出了问题也很难debug。对于java语言提供的多线程机制,已经是对PV操作的高度封装了,我们可以比较容易的控制临界区代码的范围到一个较小的范围内。将所有同步控制封装到一个类之中,对外只提供线程安全的接口,这样可以有效降低复杂度。

实现

在第五次作业时,我几乎没有封装同步操作,synchronized块到处都是,为debug带来了极大的难度。同时由于设计不当,代码中存在许多忙等待的部分。而在第六次作业时,我依然没有意识到需要进行封装,而为了解决忙等待的问题,重写了绝大部分的代码。但由于没有封装,debug难度过大,没有完成。到第七次作业时,将同步操作封装到每个线程类中,并通过统一的对外接口sync()进行同步,主线程调用工作线程的sync方法后,就会自动阻塞至工作线程一次工作完成。

第三单元

理解

我认为第三单元最重要的是形式化验证单元测试。通过将代码拆分成许多小块进行测试,同时通过jml保证算法的正确性,并可以进行一定程度的自动化测试。

实现

本单元最大的问题在于优化时间复杂度,直接依照jml实现很可能会TLE,因此需要空间换时间加速查找。

第四单元

理解

我认为第四单元最重要的是组合的思想。继承会产生强耦合,可以说,在不了解一个类的情况下继承它,很可能会产生问题。但若采用组合,很容易就可以保证只要了解接口就可以使用。如果通过继承相应的UMLElement的方式来重新建立树结构,首先就要搞明白如何实例化UMLElement,而组合则不需要。同时,通过组合也可以很容易扩展功能。在第十四次作业中,如果将时序图、类图、状态图的解析的实现放到同一个类中,可能在实现时序图与状态图时就会影响到之前的类图实现,而如果分别解析,再通过组合对外提供接口,功能扩展的复杂度就会低很多。

实现

见上

测试理解与实践演进

在OO课程开始之前,计组课上的测试经历,已经充分理解了“与标准答案对比”和“与其它实现对比”两种测试方案,而OO中测试理论上最大的收获,就是单元测试与形式化验证。之前虽然听说过单元测试,但实际上并没有实践过。

在测试的实践上,我充分认识到了边界数据的重要性。无论是hack其他人,或是被其他人hack,好几次都是问题出在边界数据上。另外,构造复杂数据也很重要。即使对每一种基本情况都可以正确处理,在多种情况复合时可能就会产生状态不一致等问题导致错误。

课程收获

  • 熟练使用java语言

    之前虽然能写java程序,但并不熟练,经常会因为语法问题查资料。经过OO课程之后,语法自不必说,还掌握了容器类以及多线程的使用。

  • 面向对象思想

    面对一个复杂的问题,如何将其抽象,设计架构,并做出高效且正确的实现。

  • 测试能力

    手动构造评测机,黑盒测试、白盒测试以及形式化验证等。

改进建议

  • 建议修改指导书时能确保通知到所有学生。
  • 建议可以减少对于算法的考察,转而更加注重架构的考察。
  • 建议实验课可以公布结果,否则无法确定正确性,感觉帮助不大。
posted @ 2021-06-26 17:05  kirimiko  阅读(50)  评论(1编辑  收藏  举报