北航2022OO第四单元博客作业
第四单元作业架构
本单元要求实现一个UML解析器。主要分为类图、顺序图、状态图。
预处理
由于本次作业输入顺序不保证一个元素先于它的ID出现,所以如果按照输入顺序记录、处理,可能在某些时候发现需要的元素还未输入。此外,为了便于全局查询或使用,有的元素需要建立“ID-元素”的映射,有的元素需要快速得知是否存在……
基于这些考虑,把它们分类用HashMap(按ID快速查询)、HashSet(快速查询是否存在)或ArrayList(仅用于遍历)分别存储,供全局各方法使用。
除此之外,官方的UML类并不会完整地记录与它息息相关的其它元素。比如UmlClass不会记录它的UmlAtrribute,UmlInterface也不会记录它所继承的UmlInterface……但是在许多指令中,我们的确需要快速地获取有关元素,并且需要这些UML元素支持一些与自身相关的操作。
于是对于部分UML类,需要另设类进行封装。比如将UmlClass以及与它相关的方法、成员、父类等作为MyClass的成员,并且添加诸如查询方法数、成员数等操作,封装成MyClass类。单独封装的类具体见下。
类图
UmlClass
这是类图中最高的一级,可能存在多个。解析UML类图时,我们需要快速知道UmlClass的一些相关内容,它们以及其使用场景如下:
1,方法。计算类中的操作有多少个、类的操作可见性、类的操作的耦合度、
2,父类。计算类的子类数量、类的继承深度、R003(不能有循环继承)、R004(不能重复继承)
3,成员。类的属性的耦合度、R002(不含有重名成员)
4,实现的接口。类实现的全部接口
5,所有相关联的另一端的UmlAssociationEnd。R002(不含有重名成员)。
因此,设一个MyClass类,它的主要成员如下:
1 public class MyClass { 2 private UmlClass mySelf; 3 private HashMap<String, MyOperation> myOperations; 4 private MyClass extClass; 5 private HashMap<String, UmlAttribute> attributes; 6 private HashMap<String, MyInterface> myInterfaces; 7 private ArrayList<String> endNames; 8 }
包含了主体UmlClass即mySelf成员,以及操作需要的相关元素。其中UmlAtrribute由于没有更低一级的元素以及没有对自身的操作,故没有进行封装。
UmlOperation
在UmlClass之下一级。UmlOperation在使用时主要需要它的参数。参数分为NamedType和ReferenceType。由于有的操作需要用到其参数,故也将UmlOperation和它的参数封装成MyOperation类。
对于NamedType和ReferenceType,可以将它们区分为两类成员,也可以合并成一类,需要时用 instanceof 判断类型。我选择分两类。
1 public class MyOperation { 2 private UmlOperation mySelf; 3 private HashMap<String, MyParameter> namedTypes; 4 private HashMap<String, MyParameter> referenceTypes; 5 }
UmlParameter
属于最低的一级。但是由于它分为NamedType和ReferenceType,且存在对自己的操作,故将有关方法封装成MyParameter类可以带来便利。这些方法包括:
1,获取类型。由封装的方法自动区分两种type并返回对应的字符串。
2,返回类型是否正确。由封装的方法自动区分两种type并按对应标准检查。
除此封装以方便操作、缩短行数外,MyParameter没有什么特别之处。
UmlInterface
与UmlClass并列的最高一级。使用时需要用到它的成员UmlAttribute,以及它所继承的所有UmlInterface。
1 public class MyInterface { 2 private UmlInterface mySelf; 3 private HashMap<String, UmlAttribute> attributes; 4 private HashMap<String, MyInterface> extInterfaces; 5 }
顺序图
UmlInteraction
虽然它的头上还有UmlCollaboration一级,但是在本单元的限定下,实际要操作的最高一级还是它。在命令中,需要用到它的UmlLifeline、UmlMessage、UmlEndpoint。
1 public class MyInteraction { 2 private UmlInteraction myself; 3 private HashMap<String, UmlLifeline> lifelines; 4 private HashMap<String, UmlMessage> messages; 5 private HashMap<String, UmlEndpoint> endpoints; 6 }
由于UmlLifeline、UmlMessage和UmlEndpoint基本只用于取基本信息,所以没有单独封装成类。
在封装后,MyInteraction的方法可以支持查询某个UmlLifeline是否存在、查询是否有重复的UmlLifeline、查询指定UmlLifeline是否被创建、查询UmlLifeline是否被重复创建等等操作。这样对于各条指令,基本只需要调用封装好的方法即可。
状态图
UmlStateMachine
状态图里的最高一级。命令需要用到它的UmlState和UmlTransition。建立MyStateMachine类进行封装:
1 public class MyStateMachine { 2 private UmlStateMachine myself; 3 private HashMap<String, MyState> myStates; 4 private HashMap<String, MyTransition> myTransitions; 5 private MyState initialState; 6 }
在这个封装的类内部,支持查询关键状态、查询是否存在转移等操作。
UmlState/UmlPseudostate/UmlFinalState
状态图里第二级。由于它们三种存在比较高的相似性,所以它们都被MyState封装。命令有关操作需要知道一个状态能状态到哪些状态、有哪些UmlTransition转出。
1 public class MyState { 2 private UmlElement myself; 3 private HashMap<String, MyState> toStates; 4 private ArrayList<MyTransition> myTransitions; 5 }
在第四单元中,UML状态本身并没有什么操作,多数是查询其基本信息。但是由于在使用一个UML状态时,经常需要获得与它有关的UML状态以及UmlTransition,所以如此封装。这样,获得了一个MyState类,就可以快速地知道从它出发有哪些转移、它可以转移到哪些状态。
架构设计思维及OO方法理解演进
第一单元
第一单元主要任务是解析表达式。而由于表达式本身有层次,所以借此让我们在解析表达式的同时建立起层次结构。
第一单元我们第一次系统接触面向对象编程。虽然落到细节上还是面向过程,并且完全面向过程也可以解决作业——但是面向对象式编程显然有巨大优势:把表达式分层设计,每层只审视自己的问题、处理自己的细节。这样做最大的好处是把问题化整为零,做到各个击破,并且在迭代时可以保留不涉及的部分,仅修改有关的层次和细节。
第二单元
第二单元主要是引入了多线程编程。比较新颖的是,相比之前单线思维,现在还需要考虑多线在时间上的交互。最大的收获是掌握了一些多线程编程的思想和处理方法。
第三单元
第三单元以社交网络作为背景,主要是让我们学会了JML规格的阅读。在这一单元掌握了契约式编程,它在开发者与使用者之间建立起一套规范。
第四单元
第四单元主要是实现UML的解析器。实现起来与第一单元体验有一定相似,同样是建立层次结构来解决问题。在这一单元我加深了对UML的理解。
小结
通过一学期四个单元的学习,我有了一些粗浅的理解。
架构设计是为了将复杂的问题拆分,转化成一个个小环节,这样我们不会面对满篇的需求而感到头疼。并且拆分也需要遵循一定的逻辑,即从底层到顶层,从小到大等等。这样,我们完成任务时可以理清思路,有针对性地就一个问题集中精力攻破而不必着眼全局分神;在进行测试时,可以缩小范围,快速定位;在迭代开发时,可以保留大体而只动部分。
而在这个过程中,面向对象是架构的一种实现方式。通过一个层次对应一类或多类对象,使得每个层次的任务被特定的“角色”分去,形成一个个环节。在完成任务、测试以及迭代开发时,最后都是落脚到具体的类。不同类的对象实现了层次化结构,层次化结构决定了不同层次的对象着眼的任务不同。
测试理解与实践的演进
总体情况
按照我的理解,测试主要分白盒测试与黑盒测试。其中前者具体是对每个模块进行测试,在了解要求和程序结构的情况下构造用例进行测试,并且要求情况全覆盖。后者则是不管其内部各模块实现如何,直接投入用例看其结果是否符合要求。
在实践中,白盒测试比较依赖自身对需求和代码实现的理解。由于设计用例需要针对性,如果设计者忽略了一些细节,可能导致一些问题无法测试出。此外,测试者的时间精力也影响着构造用例的质量。
而黑盒测试则比较省事,只需要完成自动化测试,然后让其跑起来,保证测试足够多数据,可以大概率保证正确性。但是这个方法也有局限性。首先是数据生成,如果采用随机数据,那么有可能测出bug的数据过于特殊而始终没有出现,则造成测试未全面覆盖;或者生成的数据太复杂,那么好不容易等到一组出错的数据却发现debug无从下手……其次是正确性检验,要视情况而定。
作业中的表现
具体而言,我主要是在第三、四单元中采用过白盒测试(虽然从质量和规模上远不算正规的测试),通过仔细阅读要求和代码,针对不同模块不同功能构造数据,尤其是针对一些特殊、极限、边界情况。这个做法测出了一些我或他人的bug。
黑盒测试是用得最多的方法,因为一旦配合了自动化测试程序,基本是“一劳永逸”。
第一单元是表达式解析,数据生成非常容易写,并且正确性检验也有python自带的sympy库支撑(虽然面对冗长的表达式效率很低),所以几乎完全采用了黑盒测试。当然部分特殊数据难得生成一次,并不能保证测试的完备。
第二单元是多线程,数据生成也比较好写,正确性检验可以从另一个逻辑来检查答案。但是该单元有几个明显问题。首先,多线程结果的不确定性,导致错误难以复现;其次,数据规模不能过小,导致每组数据耗时长,难以做到足够大量的随机测试;最后,随机生成的数据不一定会生成那组帮助找到bug的数据。当然可以修改数据生成来构造高并发高竞争的数据,但是这样又回到了白盒测试的问题之一,那就是考验构造者的水平。
第三单元是JML规格有关。这一单元的正确性检验主要依靠不同代码的对拍,因为难以从另一个逻辑得到答案或者依赖可靠的现有的库。写好了数据生成和自动化测试,然后把几个人的jar包放一起即可。
第四单元与第三单元类似,也是多人对拍,不作赘述。
小小的感悟
在几个单元后,我大致有了这么几条总结:
1,总体来说,黑盒测试效率最高。可以通过巧妙地设计数据生成来使得它尽可能做到全面覆盖。
2,白盒测试针对性最强,但是全覆盖需要大量时间精力。一般用于对自己认为的易错点进行测试。
3,只用黑盒测试或白盒测试都是不可靠的。黑盒测试存在难以生成特殊数据和数据覆盖不足的问题,白盒测试存在耗时长、人为犯错概率大的问题。应该大体使用黑盒测试检验绝大部分功能,然后针对性地使用白盒测试。
4,不论哪种测试,都是建立在对需求和程序理解的基础上的。
课程收获
OO课程是为数不多的让人觉得收获满满的课程。也许是因为代码都是在自己的思考下写出的而不是像做完形填空(没有内涵某些课程)。
在知识层面上,我从0开始接触了许多东西,包括面向对象编程、多线程编程、JML和UML图等等。而在实践层面上,这些知识都是经过了一次次煞费心思的作业实践和巩固的,使我可以真正运用它们,算是真正学到了一点点本领。这些都为将来的学习打下了基础。
建议
1,在多线程单元中,对于多数同学来说,其性能分不确定性较大,甚至有些“玄学”。即,电梯数据的随机性使得性能优化看起来很不可靠,要么躺平要么下很大的功夫研究——结果甚至可能差不多。建议更改形式或载体,换用一种性能优化见效明显的场景,“做多少得多少”。
2,代码不仅要看结果正误和性能,代码风格和架构设计也很重要。建议考虑引入代码风格以及架构设计的评价机制。虽然目前没有什么科学有效的评价体系,但是可以尝试探索。
3,用专题的展示帖和研讨区代替研讨课。这样交流学习可以不限制在课堂,并且好的内容可以被所有人反复阅读。除此之外可以避免研讨课偶尔的尴尬。