BUAA OO Unit4 & Course Summary

BUAA OO Unit4 & Course Summary

一、本单元架构设计

需求简述:UML解析器

  • 支持对于类图、顺序图以及状态图相关信息的查询
  • 对于该UML模型的合法性检验

以下是三次作业总的类图:

structure

  • 总体设计:从上面的图可以看出,在MyImplementation类中,有MyClassDiagramMySequenceDiagram以及MyStateDiagram分别处理各自部分的解析、查询和合法性检验。同时,处于统一和规范性的考虑,将基本上所有的Uml*都封装为My*,储存必要的查询信息、元素之间的关系并以此构建图中的树状储存结构。

  • 容器选择:基本上都采用了Hashmap<String, MyElement>或者Hashset<MyElement>来储存“id-元素”,比较简单明了。仅在最后一次作业中,由于要考虑重复接口,使用了Arraylist

  • 解析:采用多重循环读取的方式进行读取、自顶向下进行构建。由于元素之间关系都是由parentId给出的,与上面的容器设计相契合,所以很容易获取到元素的parent。

  • 查询:由于模型的大小和输入的指令数都比较有限,所以处于简便考虑我没有在解析时进行维护,也没有采取记忆化搜索。查询逻辑大致分为以下两步:

    • 按名获取目标元素,同时进行异常判断,如:
    private MyClass getClassByName(String className)
                throws ClassNotFoundException, ClassDuplicatedException {
            List<MyClass> classList = this.classes.values().stream()
                    .filter(myClass -> Objects.equals(myClass.getName(), className))
                    .collect(Collectors.toList());
            if (classList.size() > 1) {
                throw new ClassDuplicatedException(className);
            } else if (classList.size() == 0) {
                throw new ClassNotFoundException(className);
            } else {
                return classList.get(0);
            }
        }
    
    • 利用Stream等方式进行筛选、映射和收集合并,如:
    public List<String> getTriggerList(String sourceId, String targetId) {
            return this.transitions.stream()
                    .filter(myTransition -> myTransition.getSource().equals(sourceId)
                            && myTransition.getTarget().equals(targetId))
                    .map(MyTransition::getEvents)
                    .flatMap(Collection::stream)
                    .map(MyEvent::getName)
                    .collect(Collectors.toList());
        }
    
  • 合法性检验

    • 对于比较简单的检查,在解析是就进行判断,设置标志使出现一个不合法之后就无需检验其他
    • 对于比较复杂的检查,因为要求完整的继承、实现接口链(图),待读入完毕后再进行,其中:
      • 重复继承采用了dfs算法
      • 循环继承中大于2的环采用Tarjan算法、自环进行特判

二、架构设计思维及OO方法理解的演进

第一单元

本单元作业的目标是进行表达式的括号展开与化简,其工作流程如下:

\[读入表达式 \rightarrow 解析表达式 \rightarrow 展开表达式 \rightarrow 化简表达式 (+力所能及的优化)\rightarrow 输出表达式 \]

在实验课和训练的指导下,选择了用递归下降的方式进行解析,与之相配合的架构就是层次化设计:将表达式划分为表达式、项和各种因子三个层级,在解析的基础上完成复杂的计算时,我们只需要将其分配到每个对象,让每个对象完成自己负责的简单任务即可。

第二单元

本单元作业的目标是实现一个新主楼的电梯调度系统,初次接触了多线程编程,其架构设计的关键在于线程之间的协同以及线程安全的保证。我主要形成了InputHandler -> Scheduler -> Elevators两级生产者—消费者模型:第一层是输入队列作为生产者、全局等待队列作为共享变量、调度器作为消费者;第二层是调度器作为生产者,各个楼层/楼座的等待队列作为共享变量、电梯作为消费者。同时,在第三次作业借鉴了一点流水线模型的思想,新增加了RequestCounter类用于结束线程、 Scheduler类用于实现新请求及换乘的调度。

当然,本单元还有一个重难点就是多线程本身,如何利用锁的机制控制对临界区的访问、在合适的时候等待/唤醒线程,都是需要慎重考虑的。

第三单元

本单元作业的目标是根据 JML 的描述实现一个社交网络,因而架构设计的核心就是规格化设计。一方面,整体的架构必须遵循于规格的指导;另一方面,在每个方法的实现上又不能照搬规格,要在理解规格的基础上优化其性能和效率,颇有矛盾对立统一的意味。此外,在经历两个单元以后,已经初步有了面向对象的意识:会自然地将构建图模型的GraphEdge以及支持算法的DisjointSets抽象出来作为工具类,保证结构的清晰与复杂度的解耦。

第四单元

本单元作业的目标不再赘述,正如上文中所说的,重心落在对模型元素的解析和管理上。感觉上,这一单元的架构设计师最为自由的,既可以落实树形的结构、也可以“大道至简”而在查询上下功夫,算是对oo学习成果的一个检验。在本单元的设计中,我对元素的层次进行了合理划分、对有共性的部分进行了抽象合并(如MyStateMyClassOrInterface接口)、并选择合适的数据结构对元素进行组织等,初步地将所学融会贯通,培养更好的架构思维和能力。

此外,UML 语言本身就是一个纯面向对象的语言。图中的每个元素都是一个对象,UML 模型实际上是实例化的结果,对此的学习和了解也加深了我对面向对象的理解。

三、测试理解与实践的演进

第一单元

如上面所述,第一单元有一个顺序化的工作流程,所以可以对每一流程/模块进行阶段性的单独测试,确认基本没问题后再进行后续的开发,这样也可以避免出现bug难以定位的问题。在自动化测试上,可以根据 BNF 文法生成嵌套括号的表达式,再利用 Sympy 库(一个更nb的程序员)进行结果验证。此外,严重的问题,往往只需要简单的样例就能发现。所以手动构造一些数据也是很必要的:针对数据限制的边界情况、针对自己实现的优化等等,能更好地保障鲁棒性。

第二单元

在第二单元,首先应该保证单线程下的正确运行,再检验多线程的协同的正确性。所以,一开始可以针对单部电梯+少数几个请求测试电梯的调度策略是否符合预期、是否会出现死循环等犯病问题。私以为,第二单元的数据生成很容易,但结果的判定逻辑就比较复杂了,也是抱着大腿进行压力测试(doge):包括多电梯、随机请求、集中添加请求等情况进行正确性和效率的测试。当然,对于多线程的测试不是一次正确就能溜之大吉了,还必须重视潜在的线程安全问题。一方面由于复现性的问题需要多次测试,另一方面也需要利用println或者专业工具 Jprofiler 协助调试——通过观察线程状态折线图以及cpu占用时间,检查轮询和死锁的问题。

第三单元

由于第三单元输出的一致性,可以通过数据生成和多人对拍的方式进行测试,这里数据生成器的设计需要考虑如何提升数据的强度以及对于基本/异常情况的覆盖率。同时,由于对runtime有所限制,所以也需要针对时间复杂度高的指令构造边界数据:以加人 -> 加关系/加群 -> 大量指令的方式进行。当然,在课程组的推荐下,我也尝试利用了 JUnit 进行了基本的单元测试、获得测试覆盖率。

第四单元

第四单元的黑盒测试与第三单元的思路基本一致,前两次作业可以采用随机数据+对拍的方式,也实现了比较高的测试覆盖率;而第三次作业对合法性进行检测,更好的方法可能是综合自己考虑的以及讨论区中比较”诡异“的边界情况,利用 starUml 直接画图导出数据。同时,在第三和第四单元,在给出了 JML 规格/具体要求的情况下,静态阅读代码(结合以指导书)也是很有效的一种测试方式。在最后一次作业中,正是因为忽略了一行指导书中的“特殊规定”导致了出错,也让我更加深刻地体会到白盒测试的重要。

小小总结

毫无疑问,测试是我这学期oo课中做的最差的一个部分,每个单元都只能实现很简单的数据生成、然后通过手动构造特殊、边界数据。继上学期计组的碌碌无为之后,也没有实现过一次完整的自动化测试,反而是对大佬产生了依赖,非常respect和感谢无私帮助我的朋友@FlyingAns、@Harahan还有@Longxmas。这也算我的一个遗憾了,希望在未来的学习中能在测试上有所突破,不求高深但求入门。

四、课程收获

  • 对于面向对象的编程方式有了初步的理解。一开始觉得OO不过就是类、封装、继承、多态等种种;后来发现,它更是一种设计思想,要求层次化、解耦合:将一个大型的工程、任务进行分解,抽象出带有属性和操作的对象,然后进行关联组合等等。同时,从面向过程的短小代码中跳脱出来,意识到在实际项目庞大的代码中,一个优秀架构的重要性:不仅要立足当下、考虑复杂度和耦合度,更要放眼未来、考虑架构的可拓展性。
  • 从零开始入门了java语言。基本熟悉了各种容器的特性和使用、浅尝了函数式编程的甜头、简单了解了JVM的内存管理和GC机制。
  • 学到了很多相关知识。每个单元都有新东西:递归下降词法分析、多线程、JML规格、UML语言和契约式编程。此外还有常用的设计模式、在 checkstyle 督促下养成良好代码风格、时不时复习回顾经典的算法等等。
  • 积累了写文档的经验。虽然我非常不喜欢写这种东西,但不得不承认每单元一次的博客还是很有用的。毕竟以后可能要写设计文档开发文档测试文档用户手册FAQ等等等等诸如此类的东西,现在开始学着写以后肯定也得心应手一点。

五、改进建议

提供有关测试方面的教学!

有时候并不是不想做,而是无从下手。既然多次强调了测试的重要性,又让我们完全通过自学的方式学习测试,感觉不太合理。希望至少能有一两节课或是其他方式把测试“领进门”,在以后可以帮助到和我一样能力有限的同学。

其他建议

  • 完善实验课的安排。首先是给予实验结果的反馈、提供正确答案;除此之外,可能还需要以视频等方式提供必要的讲解和解析。感觉第一、二单元的实验的代码都对作业给予了很大的帮助,如果能充分理解实验的内容将会非常有益。

  • 在寒假或开学增加预习内容。比如,可以加入对于递归下降的简单介绍(就和第一次实验差不多)以及多线程的简单介绍、以及各种java锁的应用方式。现在寒假预习正则表达式,然后第一单元上来就不推荐正则然后用递归下降,实话说有点剥离。而多线程的提前入门也能让第二单元开始不至于手足无措、也能让锁的选择更加多元而不是一个synchronized走到底。

  • 研讨课的方式有待商榷。具体怎么改进我不好说,但个人感觉现在的研讨课是有点水分。首先小组讨论基本就一半的人说话,剩下都在摸;然后小组总结可能就一共就几个点、设计方式,结果让每个组都上台复读;最后就是会议记录,有点过于形式化了,身份基本是摁编、讨论内容也都是每人分一点,感觉没啥意思……

    P.S. 个人分享part算是研讨课之遗珠了,感觉每次听大神们分享都很有意思也有收获。

posted @ 2022-06-29 12:31  ezmoneysniper  阅读(21)  评论(1编辑  收藏  举报