本单元主要学习关于Unified Modeling Language (UML)的设计与分析的内容,重点对UML中的类图、顺序图和状态图的表达方式和有效性进行了分析。
UML与上个单元学习的JML同样是一种建模语言,但与JML不同,UML是以图形化的方式描述一个程序的整体架构,并且UML提供的建模思想与方法并不限于Java语言,而对所有面向对象编程的语言都通用。
理论上,UML建模完成后可以通过算法直接生成对应语言的源代码,在如starUML等UML工具中也确实具有通过UML类图生成源代码框架的功能,但UML模型无法像JML模型一样明确的,无二义的提供模块功能定义和检验方法,不能帮助验证已完成的程序的正确性。
本单元的作业是完成一个解析UML文件中的类图、顺序图和状态图的程序。在UML文件中,模型中的一切,以类图为例,模块(类、接口)、模块的子模块(成员变量、方法、方法的参数)、模块之间的关联(关联、继承、实现)均是以独立的对象的形式记录的,本次作业的目的是读取UML文件中的对象,建立逻辑意义上的图,并实现有效性检查和查询等功能。
一、 本单元作业架构
本次作业使用了图的数据结构来保存UML中的各个元素对象,将类图中的模块(类和对象)作为图的节点,模块之间的关联作为边。边的存储方式是在每个节点中建立一个链表,储存邻接节点的引用。因此我将官方接口中提供的元素类再次进行了包装,用以储存邻接节点的信息以及对图进行不同查询时的各种方法。
类图:
顺序图:
状态图:
二、 四个单元中的架构设计及对“面向对象”的理解
本学期的四个单元,第一单元为以熟悉面向对象的继承与多态为主的多项式求导,第二单元是使用了多线程的电梯调度,第三单元是使用JML进行模块化编程的地铁信息图,第四单元是UML文件解析。
在面对对象编程的过程中,我一般是“从下向上”地分析问题,建立模型。我先从现实的角度从问题中提取出具体的对象,比如多项式求导中的项和各种因子,电梯调度中的电梯和乘客,地铁信息图中的站台和线路。
之后再分析这些具体对象之间的关联,设计对象相应的接口,这时对于问题的初步模型已经建立完成了。
之后添加抽象对象对模型进行优化。对于功能上重合或类似的对象,可以设计一个父类对其共同进行管理,如UML解析中MyClass和MyInterface都可以作为类图的节点,因此设计了一个共同父类MyNode进行管理。现实中具体的对象可能拥有多种不同的功能,但在设计程序时,为维持单一功能原则,需要设计一些抽象的功能类,分别管理具体对象中的不同功能。如此被分割的具体对象之间的交互可能变得更加复杂,这时可以考虑设计一个中间缓冲类,辅助两个对象之间的交互。
优化的另一个方向是选择一个合适的数据类型对对象进行管理。如我在地铁图作业中针对不同的线路要求设计了三种不同的图对站点和线路进行管理。如果能找到合适的数据类型进行管理,它可以一定程度上替代上述功能分离类和中间缓冲类的功能,还可以使用现成的算法,提高程序运行效率。
我认为一个好的程序架构应该能清楚的表示出程序实现的思路,让人可以快速理解程序的层次和逻辑,并且可以想象出每个模块大致该使用什么算法去实现。模块之间的交互不应太过纠结,应尽量减少模块的接口对于其他模块的依赖。在设计时,可以参考一些经典的设计模式,如生产者消费者模式、工厂模式等。
我对于“面向对象”的理解也基本与上述过程相符,我认为这是一种建模思想,从现实问题中每一个具体的个体出发,分析每一个个体的功能和个体之间的联系,再将相似的个体归纳成抽象的类同一管理,逐级向上抽象,进行建模。而与之相对,面向过程编程的方式更倾向于模型的实现方式,事实上,在面向对象建模完毕后,在实现每个接口的功能时使用的依然是面向过程的方法
三、 测试方法总结
第一单元作业时,我主要针对我在设计架构时认为比较复杂容易出现疏漏的地方设计测试点进行测试,设计压力测试、边界测试,但这样针对性的测试无法检测到在设计时没有意识到的问题,因此也与其他的同学交流,交换一些测试点进行测试,争取能尽可能多覆盖一些情况。
到了第二单元的电梯作业,测试用例的长度大幅增加,我意识到人的力量是有极限的,因此我设计了一个随机测试数据生成器,对程序进行随机测试,用程序检测运行结果的正确性。在随机测试中可能发现程序表现异常的情况,再猜测原因进行针对性调试。使用随机数据生成器进行测试,虽然测试的效率比手打测试数据高上不少,但本质依然还是覆盖测试没有改变。
第三单元我们引入了JML建模语言,这是一种定义模块功能,提供检验方法的建模语言,使用JML工具可以自动生成一定的测试样例,也可以在运行中检测各个模块的运行是否符合功能定义,对简单程序可以从逻辑上证明是否与功能定义等价。在实践中,我发现JML的逻辑证明功能对于复杂的程序表现不佳,样例生成功能也有很大的局限性,JML的价值可能更多在于运行中检验,这可以使某些在某次运行中出错但没有明显现象的错误暴露出来。
总体而言,使用大量测试数据进行覆盖性测试并在此基础上对高危区进行针对性测试依然是现在测试的主流。
结合历次作业的感受,我认为bug可以分为两类,一类为偶然性bug,一类为系统性bug。偶然性bug是一时疏忽少打一个负号程度的错误,这类错误可能导致模块的这一部分功能完全瘫痪,但也因此在覆盖性测试中很容易被发现并改正。而系统性bug是在设计中疏于考虑导致的功能上的冲突,这类bug即使被找到也很难被改正,很可能所谓的改正只是在破洞上打了个补丁,越打越多,最后让程序纠结成一团乱麻,我在第一单元和第二单元的作业便分别遇到一次这种情形,第一单元是多项式类既设计成了可修改类,又对实例进行了共享,为了debug还费尽心思为链表写了深层拷贝函数,第二单元是多线程,电梯和控制器相互竞争,同步块写了一大堆,不但没调节开,还总是死锁。我认为,所谓bug不一定是程序运行的bug,也可能是程序设计的bug,所谓测试也不一定是写完了程序再测试,写程序的过程就是对程序设计的测试,当在写程序时遇到非常纠结的情况,及时回头思考是不是程序设计出了问题,否则就真的不是在写程序而是在写bug了。
四、 改进建议
1. 关于强测分数
我们的强测分分正确分和性能分两部分,性能分有按排名区分梯度的算法,我希望正确性也能设置一些梯度。多项式求导有标准答案,电梯各步骤环环相扣不好区分,确实难以设置梯度,但对于像地铁图和UML图这两单元,不同个功能之间是相互分离的,但强测每个测试点都会覆盖所有的功能,一旦一个功能出了问题,整个测试点就全部挂掉了。我身边的同学有好几人都在强测中因为一个bug被直接扣掉了50分,在之后的debug中也确实证明是同质bug。
我建议对于JML和UML单元的强测中出现错误时,除非是程序崩溃造成的错误,否则都继续进行测试,之后根据出错的该种指令在测试点中的占比扣除相应分数。
或者对于强测数据debug出现同质bug的情况能够有一些补救措施,找回一些强测分数。
例如:某JML强测测试了1000条指令,某人测试中其中一条最短路径指令输出错误,该测试点中总共有100条最短路径指令,那么这个同学这个测试点依然可以拿到70~90分的正确分,而不至于0分。
2. 关于互测
我建议助教可以在互测期间向同学们征集一些经典狼人样例,对全部房间进行测试,对于样例被选中的同学有一定奖励措施。
另外我认为应当采取一些措施来避免某些同学大量提交随机测试数据的行为,在第二单元的测试中,曾出现过某同学hack了140刀的情况,而我认为这不是应当被鼓励的,相对的应该鼓励同学进行针对性的构造数据,数据应能简单明了的指明bug发生的原因。
3. 关于作业提交
我建议对提交的作业的readme做一定的要求,至少应表明自己程序的设计思路,否则在互测期间通过读代码来寻找逻辑上的bug是完全不现实的,这与设计互测环节的初衷相违背。