BUAA_OO第四单元暨全课程总结

BUAA_OO第四单元暨全课程总结

(0)前言

北航计算机学院三剑客 “coooos” 之一的OO课程即将迎来尾声。犹记得几个月前的寒假,我坐在这张书桌前,看着一望无际的 Pre 作业发呆。那时,我已经听闻了多项式的复杂与电梯的恐怖,也曾担忧过自己是否能按时按量完成一周一次的作业、是否能在如此高强度快节奏的课程中存活下来。今天,我依然坐在这张书桌前,回想着这一个学期的一点一滴。令心中悬着的石头落地的是,四个单元下来,十多次作业,基本上没有出大岔子,顽强地坚持到了最后。于是,当再一次回头,仔细思考这一路的旅程时,收获也是颇多的。当然,其中的教训也是深刻的。到达世界一流课程——北航OO,太美丽了OO,哎呀这不学期总结吗?不过在诉说这一切之前,还是先看看远处的第四单元总结吧。


(1)第四单元作业架构设计

第四单元的主要任务是制作UML解析器,并进行UML一致性检查。整体分为类图、状态图和顺序图三部分,各部分之间相对独立。为了更好地对UML元素进行解析,可以对一些典型的元素设置单独的类,将一些性质包装起来,而MyImplementation类只需调用相应的方法即可。简要的设计架构如下图所示。

由于查询指令是在.mdj文件解析完之后再依次输入的,故可以在指令输入前便进行一系列的初始化操作。为此,在MyImplementation类中设置initial()方法,在构造方法中直接调用此方法。

在研讨课上,已经有很多同学指出了初始化的方法,它主要由几个循环构成,每一次循环都处理一些特定的元素。由于各元素之间存在大量的依赖关系,故每一次循环处理哪些元素是需要严格要求的,否则极有可能出现bug

在第一轮循环中,我主要处理了一些顶层元素,比如类、接口、状态机等等。在第二轮循环中,我主要处理了属性、方法、泛化、接口实现、生命线、状态、关联端点等元素。在第三轮循环中,我主要处理了参数、状态转移、事件、关联等元素。最后,还需要对消息、操作和事件进行进一步的处理,以便完成一些指令的准备工作。

MyImplementation中,我设置了非常非常多的容器(可能有大约30个)以便快速实现各类查询需求。对于普通的查询指令,我会首先判断一些基本的异常(如重名异常),再根据各查询参数找到自己所建立的那个类,再直接调用这些类的内部方法得到答案。对于一些指令而言,答案在初始化的时候就已经得到了;对于另一些而言,则是要等到查询的时候再进行实时计算。但由于对每种指令而言,答案是固定不变的,故只需计算一次并将答案保存,后续再有相同的指令时就可直接输出结果。

对UML一致性的检查同样是在初始化的时候完成的。对于一些只需知道“是”或“否”的错误类型而言,只要检测出一次就不用再检测了。对于一些需要输出所有名称的错误而言,需要在检查的过程中实时地将相应结果存入容器中,检查结束后再一并输出。

虽然从上图中可以看出我单独设置了MyCheck类,看似是专门负责检查工作,但事实上我依然将大部分的检查工作交给原有的那些类处理,因为它们本身便含有丰富的信息,可以快速判断是否存在错误。只有那些涉及全局性的错误(循环继承、重复继承)等,不等单独地在某一个类中完成,则交由MyCheck类统一处理。

对于一些复杂的指令或检查任务,可能需要对图进行搜索。由于本次作业的时间限制较为宽松,且元素的数量规模不大,我直接采取了DFS+回溯的方式。

在最初写作业的时候,我发现了一些奇奇怪怪的bug,例如有的时候不会返回空列表等等。这些错误虽然很小但也很致命,这进一步提醒我们要仔细阅读指导书,不放过里面的每一句话。如果选择性地忽略甲方的需求,最终的结局会很悲惨。


(2)四个单元中架构设计思维及OO方法理解的演进

经过四个单元的学习,我认识到了什么是架构设计,以及好的架构的重要性,也对面向对象思维有了更深刻的理解。

完成第一单元作业的过程中,由于缺乏对架构设计的认知,我走了不少弯路。首先,在第一次作业中,我因缺乏远见而选择使用正则表达式对多项式进行解析,虽然能确保正确性,但毫无迭代空间,导致第二次作业不得不重构,这也是唯一一次重构。之后,我仔细设计了递归下降的方法,同时也兼顾了架构的可扩展性。第三次作业虽然没有进行较大的改动,但依然由于之前设计的不完整性,额外补充了一些运算类,多出了许多工作量。三次作业跌跌撞撞、磕磕绊绊地经历让我下定决心,十分注重最初架构的设计和可扩展性。

简要回顾一下最终的架构:

img

时间来到了电梯月。在最开始,由于对多线程的生疏,以及对功能及性能的追求,我在架构设计上花费了大量的时间。在相当长的一段时间内,我没有写任何代码,而是不停地在纸上设计各个线程、比较各个算法。尤其是电梯调度器的设计以及调度器的位置,是比调度算法还要重要和根本的事情。本来,我已经构思了一套看似可行的方案,但是在看到实验课上的实例代码后,我豁然开朗,于是决定全盘推翻之前的思路,按照新的方式从头开始重新设计,确立了主线程、输入线程、主调度线程和电梯线程,这一架构成功地应用于后续的几次作业中。在调度算法方面,我选择了比较成熟的LOOK方案,没有做很多重大的变动。在全相连调度中,为了确保正确性,我也没有设计动态换乘的方案。虽然说富贵险中求,但成熟可控的架构才是我的选择。

简要回顾一下最终的架构:

img

安全度过电梯月,紧随其后的是JML单元。这一单元中,虽然整体架构不需要自己设计,但一些细节上的处理依然具有较大的自由度,这集中体现在几个图论算法的实现上。戏剧性的是,此时我也受到了很多设计上的困扰,不是因为毫无思路,而是因为有两条思路,这在第三单元的作业总结中已有详细说明。最终,我索性选择写出两份代码,同时测试,同时对比,选择表现更好的那一份。这充分体现了实践的重要性。

在最后的UML单元中,架构设计已经不再像当初那样举步维艰,仿佛是一件自然而然的事情,更需要注重的是一些细节问题。但这不意味着架构不再重要,恰恰相反,正是因为有良好的架构,才可以让设计者能将时间和精力放在其他方面,从而进一步完善整体的设计。

回顾四个单元的架构设计历程,也许可以总结出以下几点经验:

  • 好的架构十分重要,但想要设计出好的架构并非易事。
  • 好的架构需要坚持“高内聚,低耦合”的原则。
  • 好的架构具有简洁性。这种简洁是相对而言的:能让人一眼理解其中的构思和原理。清晰的架构不仅能够降低编程难度,还能够降低程序的复杂度,由此减少出错的概率。能一个人完成的事情,就不要让两个人甚至一群人来完成。
  • 好的架构具有可扩展性,便于迭代开发。这句话的含义是:在后续添加新的功能时,对之前的代码很少进行改动甚至完全不改动。

与此同时,对OO思维的认知也逐渐深入。首先需要强调的是以对象为主体的OO方法。对象及具有自己的属性和方法,对象和对象之间也具有复杂的关系。其次需要强调的是面向对象设计的SOLID原则:单一职责原则、开放封闭原则、里氏替换原则、接口隔离原则和依赖倒置原则。之后,对于一些常见的设计模式,也需要熟记于心,例如工厂模式、适配器模式、代理模式、观察者模式、装饰模式等等。


(3)四个单元中测试理解与实践的演进

测试是开发的重要环节,没有经历过千锤百炼的程序不是好程序。在OO课程中,测试不仅可以在很大程度上帮助我们找到层序的bug,增强程序的鲁棒性,还可以在互测环节发挥重要作用。四个单元的作业要求差异较大,因此测试方式和测试数据也具有很大的不同。

第一单元的测试主要是验证程序的正确性,主要方式是特殊数据+随机测试。首先手动验证一些容易出错的、人为构造的数据,再编写评测机随机生成各种多项式及函数。受益于多项式形式的较为清晰,数据的构造也较为容易,非常适合进行随机测试。很多同学都是通过这种方式找到了很多bug。但还需要注意的问题是,测试仅仅只能显示错误的结果,至于究竟是哪一步出错了,还需要再单独判断。

第二单元的测试,虽然也需要验证正确性,但更多地还是体现在线程安全和性能上。最重要的测试目的是检验线程是否安全、是否会发生死锁、是否能正常结束等等。其次是测试算法的可靠性,以及特殊性况下算法是否能正常工作。同样,可以先手动构造一些极端情况下的数据,观察程序是否出现问题。其次再是随机生成大量数据。在大概率保证线程已经安全的前提下,可以进一步地通过测试来验证算法的正确性并优化性能。通过观察测试的输出结果,或许可以发现电梯不能正常完成所有请求,或是可以发现一些十分耗时的请求,由此有可能发现算法本身的问题,以及改进算法的性能,让更多的人在更短的时间内到达目的地。

第三单元的测试最为特殊,因为测试与JML是紧密相连的。如果说其他几次的作业,测试是为了完善作业,那么这一单云的测试,则是学习的一部分。通过JUnit等集成工具,利用JML规格来准备测试数据,可以实现全覆盖的单元测试。与此同时,针对一些复杂的图算法,还需要进行压力测试,考察程序在数万条指令下的运行时间。在生成数据时,还需要注意选择不同形态和密度的图。

第四单元的测试形式也较为复杂,因为不同的指令之间较为独立,需要分别进行针对性的测试。生成数据也有两种方法,一种是直接在StarUML中绘制并借助官方包进行输出,另一种是按照输入的格式编写评测机。前者适用于手动构造特定的情形,后者适用于随机测试。

在四个单元中,测试都是不可或缺的环节。既不要高估自己作业的完成质量,也不要低估强测的测试强度。只有经过全方位的测试,才可能排除尽可能多的错误、不断提高程序的性能。

可以发现,四次测试虽然在形式上差异较大,但也有很多共同点,例如:

  • 测试主要分为两部分:手动构造和随机测试。在手动构造部分,通过人为构造一些特殊情况下的数据(比如边界条件),测试程序的正确性。这种做法的缺陷是构造的数据不能覆盖所有的情况。因此还需进行随机测试,通过大量数据检测程序的薄弱部分。
  • 当然,即使这样也不能完全保证没有遗漏。如果有条件的话,还可以借助现有的一些工具进行全方位的单元测试。

(4)课程收获

  • 系统性地学习了面向对象的程序思维,从另一个角度看待程序设计方法。
  • 对多线程、JML、UML等概念有了更深刻的认知。
  • 在很大程度上锻炼了思考问题、解决问题的能力。每一次作业都是一个挑战,顺利完成需要多方面能力的结合。
  • 更加注重顶层设计思维,跳出无脑编写代码的困境。
  • 学会多样的测试方式,在不断发现问题的过程中提高能力。
  • 与同学积极讨论交流,交换思路与想法,学习了很多闪亮的观念。
  • hack和被hack之后更加注重细节。
  • 更加注意代码风格,规范代码编写。
  • 在快节奏的课程中锻炼了时间管理能力。
  • 更加清楚地领略了大佬的过人之处,明白自己还有很多需要提升之处。

(5)改进建议

从整体上而言,OO课程已经较为成熟,无论是课程平台、教学模式还是作业完成方式,都没有太多槽点。但在一些细节方面还有改进的空间,例如:

  • 指导书可以更全面、更严谨、更清晰。虽然目前的指导书已经非常详细、对非常多的问题进行了全面的说明和规定,但每次总有几处容易引起歧义,导致在作业进行的过程中需要多次修改和补充。对于容易引起歧义的部分,可以避免使用自然语言进行描述。
  • 实验课虽然为我们提供了宝贵的示例代码和设计思路,帮助我们进一步了解课程内容,但存在感难免有一些低。我们并不清楚自己最终是否正确,可以在之后酌情提供一些标准答案或进行解答。
  • 性能分的算法上尽量避免极端情况对整体造成的影响,不会出现“一人内卷全系受害”的局面。解决方法可以参考电梯单元的第三次作业。
  • 对互测的要求稍微放宽一点点。很多时候强测的数据强度都不适用于互测,可以稍微缩小一些二者的差距。
posted @ 2022-06-27 20:32  DreamWave  阅读(27)  评论(1编辑  收藏  举报