一、本单元作业架构设计
1.第一次作业
本单元首次接触到UML以及相关概念,在面对第一次作业时首先花了很大功夫去阅读官方接口中各种UmlElement的代码,才理解了输入的模型元素中各属性的含义。总的来说,就是将原本UML图中的层次结构,通过使用id和parent_id的形式平面化,变成各类型的UmlElement依次输入,需要我们通过这些UML元素的相关属性再反向建立起原图中的层次结构,从而实现作业的各项查询要求。由于每个UML元素的id都是唯一的,故使用elementsMap建立起id与UmlElement的一一对应关系,以便使用id直接访问所需元素。为了恢复层次结构,需要做的是在现有的UML元素中增加属性来存储层次关系,故新增MyUmlClass和MyUMLInterface两个顶层类,这两个类的各项属性通过存储id来对应相关的各UMLElement(如属性、操作、关联、继承、实现的接口等),从而实现了层次结构的构建和维护,同时使用classMap和interfaceMap来实现存储和访问,以及使用nameMap解决类不存在或类重名异常问题。由于查询要求中有关于operation的类型的询问,故多使用一个类MyUmlOperation来存储相应UMLOperation的类型(有无返回值、有无参数),二者通过operationMap实现存储和查询。如此一来,在初始化时将各类UMLElement存入相应的位置,即可还原原有层次结构,各项查询的实现也就变得轻而易举。
2.第二次作业
第二次作业在上次作业的基础上,增加了对顺序图和状态图的查询功能,故通过继承上一次的MyUmlInteraction(在本次作业中更名为MyUmlBasicInteraction)来实现对类图查询部分的代码复用。有了上一次的经验,这一次在开始入手的时候方向十分明确,首先找到接口中新增的各项UmlElement,明确了他们的各种属性,然后通过对几个顺序图和状态图的解析,理清了这些新增UML元素之间的层次关系,由于本次数据进行了简化而且状态图和顺序图都只有3项所要求的查询,所以并非所有的新增UML元素都需要进行处理。顺序图部分,新增MyUMLInteraction类来存储与查询相关的内容,同时使用类似上一次作业中interactionMap以及interactionNameMap的方式来进行管理以及异常的处理,对于UmlMessage和UmlLifeline两个元素只需要统计次数即可满足查询要求,而对于incoming消息的数量,通过incomingMap建立每个lifeline与integer关系,即可实现更新和查询,同时通过lifelineName来解决异常问题。状态图部分与顺序图大致相似,首先是通过新的MyUmlStateMachine类进行存储和查询管理(异常处理方式也相同),前两个有关出现次数的查询只需要统计UmlState(包括UmlPseudoState和UmlFinalState)的个数以及UmlTransition的个数,而第三个后继状态查询与顺序图稍有不同,每个状态需要对其后继状态进行记录,故新增MyUmlState类以及ArrayList<MyUmlState> follows属性来实现记录,在查询时调用countFollows方法通过dfs即可完成统计。
这一次的作业还增加了3条有效性检查规则,主要针对的是Uml类图存在的一些不合法现象,是与顺序图和状态图没有关系的,所以自己在写代码的时候也是先完成了有效性检查这一部分。由于本次作业使用上一次作业作为父类,故对类图的有效性检查并没有涉及到新增的架构设计,只是一些算法设计和化简节约运算时间的问题。第一项检查是属性间重名与属性和associationEnd重名问题,比较简单,通过使用HashSet即可实现重复性检测,现将associationEnd的name都加入set(数据保证associationEnd无重名),再遍历attributes验证每一个name是否重复出现。第二项检查是对循环继承的检查,即在有向图中寻找循环路径,采用dfs进行路径搜索并使用链表记录当前路径,方便在递归时进行回溯,当发现要进入的结点已经在路径链表中存在时则找到了循环路径,然后再将环上的每个结点都加入result集合。由于只存在类与类的继承关系和接口与接口的继承关系,二者之间不会有交叉继承,故可以分别单独处理,这里我使用了handleClass和handleInterface两个方法。第三项检查是对接口重复继承的检查,即有向图中一个结点是否有多条不同路径到达另一个结点。首先考虑的是接口所形成的有向图中的重复继承,使用bfs的方法将起点加入队列,则整个bfs过程中入队的全部结点均是起点可达的结点,如果有结点重复入队则证明起点有到达该结点的多条不同路径,即起点是重复继承了该结点的,将起点加入result集合,如果整个bfs过程中直至队列为空都没有结点重复入队,则表明所有入队过的结点都不是重复继承的,可以打上标记使这些结点不用再次成为下一次bfs的起点去检查是否重复继承,可以节省一些算法时间。由于类对接口的实现,以及通过继承父类实现的接口都算作对接口的继承,所以还需考虑到类的多重继承情况,实质上与接口重复继承的检查类似,只不过初始化的bfs队列不是单个的接口结点,而是要将要检查的类以及父类所实现的全部接口都加入到队列之中,再进行bfs遍历看是否有重复入队的结点,同时由于已经知道哪些接口是重复继承的,所以如果队列中出现了重复继承的接口则该类一定是重复继承的,可以直接返回结果,从而提高检查的速度,节省运算时间。
二、四个单元中架构设计及OO方法理解的演进
1.第一单元:表达式求导
架构设计:通过封装Expression、Item、Factor类实现题目所描述的层次结构,同时CoFactor、ExFactor、SinFactor、CosFactor、ExpFactor五个子类继承自Factor类,针对不同的子类运用多态实现不同方式的求导,再将结果传回至上一层的Item,执行项的求导后再上传一层至Expression,完成最后求导。同时ExpFactor中包含Expression属性,求导时实现递归下降,封装分层清晰,分工明确。
OO方法:本单元初次接触面向对象编程,通过三次作业慢慢完成了面向过程思想向面向对象思想的转变,面向对象的三大特性封装、继承和多态也在第3次的架构之中得到了充分地运用,对“万物皆对象”的基本OO思想有了比较深的理解,在设计的过程中会运用抽象的方法将复杂的题目要求进行解耦和分工,封装出各种类、属性和方法,同时还了解到了一些常用的面向对象设计模式。
2.第二单元:电梯调度
架构设计:整体结构只有一个输入线程类Input和一个电梯线程类Elevator,两个类共享一个楼层类Floor对象来存储每一层的上行队列和下行队列(将请求分为上行和下行分开两个队列存储),Elevator调度算法采用的是look算法,即和日常生活中所乘坐电梯较像,向一个方向运行并判断每一层的运行方向上有请求则捎带,一直到电梯原运行方向上没有请求的时候才改变运行方向。对于每位乘客的乘坐策略则进行如下规定:1.能直达则不换乘2.快换慢晚下,慢换快早下3.有多个电梯可以坐时坐先来的,新建Passenger类来对每一个读入的请求进行预处理,为该乘客提前制定好乘坐方案,所存储和进行预处理的属性有id、起始楼层和目标楼层、是否换乘、换乘楼层以及当前所等的电梯编号,并将Passenger对象存到Floor对象的相应楼层之中,电梯到达相应楼层对等待的上下行队列以及电梯内的人进行操作,选择运行方向上等待该编号电梯的人进入,以及该层为目标楼层或者换乘楼层的人出电梯,同时为换乘乘客更新属性(去掉换乘标志、更改所等电梯编号),并根据方向加入相应的等待队列。3部电梯分别为Elevator类的3个对象,所以3者之间并没有统一的调度工具,只能自己调度自己的运行算法,是这一次设计之中稍有不足之处,可能会稍微影响整个时间效率。
OO方法:多线程概念的引入完全打破了自己对于程序的传统认识,打开了新世界的大门,日常生活中确实有不少需要各部分之间并行的实际需求,这也反应出了面向对象所具有的实用性。多线程间的同步和互斥问题是实现线程之间安全协作的重要基础,本单元也主要是对这一问题进行了着重讲解,我深入了解了经典的“生产者—消费者模型”,同时能够根据需求在合适的地方使用锁synchronized来配合wait(),notifyAll()进行线程间的协作,算是能够初步解决一些多线程问题的入门选手了。
3.第三单元:地铁查询系统
架构设计:本单元需要对由路径组成的图进行维护,用两个map对id和路径path进行双向映射存储,以便管理和查询。对于4种最小值的查询,可以发现它们本质相同,均有路径权值和换乘代价的概念,只有值上的不同,而其他的预处理、更新和维护操作都是相同的,所以使用一个新的类BuildGraph来封装这些操作,实现对4个图的维护,由于换乘代价的引入,需要对原图进行预处理以保证最短路径算法的正确性,我采用的是路径内部连最短边法,在添加Path的时候,需要在Path内部先进行一次最短路计算,得到Path内部各结点间的最短路,并为每个节点间添加相应权值的边;在计算最短路结果时,初始化answerMap将每个结点间的边最小权值加上换乘代价得到最终权值,并进行最短路计算得到的结果再减去一次换乘代价即为最终结果。由于这种方法会增加边,所以还需要用一个realMap结构来存储原图,以保证其他有关原图的查询指令的正确性。
OO方法:在本单元中主要介绍了以JML语言为基础的规格化设计,很好地符合了面向对象对于抽象和解耦的要求。规格的使用能够更好地实现设计与实现的高度分离,当我们进行架构设计时只需考虑类和方法想要实现的功能和最终得到的结果,而不去关心具体的实现细节。同时,规格作为一种纲领式的编码要求,统一明确且全面地写出了前置条件、后置条件、结果、异常等,使模块化的自动测试成为了可能。总的来说,规格是不同代码实现风格上的又一层封装,统一且逻辑严谨地对代码行为进行规范,让代码的交流和维护有理可依且更加方便。
4.第四单元:UML图查询系统
架构设计:本单元的总体难度并不高,整体架构思路就是恢复原图的层次结构,将需要扩展属性的元素使用新的MyUmlXXX进行封装,通过新的属性来存储各类UmlElement恢复层次,并再增加一些辅助的方法,从而满足查询需求。比较难的地方是理出各类UmlElement之间的从属关系,可能直接阅读官方代码理解起来比较困难,可以通过自己构造UML图并使用jar包解析的方法,使用图能够更直观地去理解层次关系。
OO方法:本单元的主题是UML图,和上一单元JML比较类似,并不是什么面向对象思想方法,而是一种便于实现抽象和进行架构设计的工具,相比于JML的文字形式,UML的图更为形象直观,而且还支持时序图和状态图形式,可以使整个架构设计更加全面具体,面向对象的特性在UML图中得到了充分的体现。这也为我的架构设计提供了思路,去考虑如何充分地抽象和解耦,使用明确具体的类,并考虑到状态转化关系以及时序交互关系。
三、四个单元中测试理解与实践的演进
1.第一单元
最开始采用的还是最基础的测试方式,即自己手动构造数据,并通过复制粘贴的方式进行测试,由于本次作业中存在WrongFormat判定,所以在数据构造的全面性上也存在一些疏漏,导致有两次作业都出现了小bug。在互测方面,由于没有大量构造测试数据的工具,我只能将自己测试过程中发现bug的数据用来测试其他人的代码,同时通过对代码的阅读来寻找一些可能存在的漏洞。
2.第二单元
本单元主要为多线程程序,所以测试的重点放在多个线程之间的互斥和同步问题上,而且在这一单元的测试中没有了对输入非法性的判断,所以整体的测试思路还是比较明确的,只需要在边界问题和时间限制上多加留意即可。测试方法还是和上一单元一样,采用自己手动构造的方式,尽量地测试了边界数据和时间极限数据,整个单元的三次作业中都没有出现bug。
3.第三单元
这一单元中官方介绍了Junit单元测试工具,可以实现对方法的局部测试,从而可以更加明确bug所在之处,同时结合TestNG可以实现对边界数据的测试。在本单元,首次尝试了自动化对拍测试的构建,查询了网上的一些教程,虽然最后的实现结果却不太理想,却还是对JML契约化编程以及自动化测试有了更深入的理解。由于这一次作业主要是实现各项查询功能,所以测试数据一定要全面覆盖每一项查询,在本单元的第3次作业中就因为只顾对新增指令的测试而忽略了之前的查询指令中的bug,导致强测直接爆炸,需要从中多吸取教训。
4.第四单元
最后一单元的数据需要使用官方jar包将图解析成符合输入格式的数据,而UML图的自动生成比较困难,所以就放弃了沿用上一单元自动生成测试数据的想法。同时吸取上一单元查询系统的教训,对每一项查询都构造了较为全面的数据集测试,保证了充分的测试。
四、OO课程收获
1.代码风格的改变:checkStyle中的空格空行、驼峰命名等基本规范已经充分融入自己的编程习惯之中。
2.面向对象思想与架构设计能力:本课程最重要的收获就是对于面向对象有了全面深入的理解,能够灵活地运用封装、继承、多态的三大特性来实现需求以及代码的充分重用,“万物皆对象”的思想以及JML、UML工具的应用提高了解决实际问题时进行架构设计的能力,能够将需求充分地抽象和解耦,明确各类的分工以及类间层次关系,完成清晰可扩展的架构设计。
3.多线程编程能力:通过对多线程的学习,对于可能涉及到并行的实际问题也有了解决能力,能够对较基础的线程安全问题进行简单的维护。
4.测试和debug能力:能够考虑到更加全面的测试数据集,包括极限数据、边界数据等,同时了解了Junit单元测试方式,有了更加灵活的debug手段。
5.Java语言能力:整个课程的作业都由Java语言完成,对于该语言的语法以及一些常用数据结构的使用技巧有了较高程度的掌握。
五、课程改进建议
1.后两个单元主要讲JML、UML两个工具,但作业却不是对这些工具的使用练习,而只是实现一些与这些工具相关的查询要求,而且官方还进行了接口的封装,所以整个作业需求就没有什么架构设计可言,最终的结果就是面向对象思想和架构设计没得到足够的练习,而对这些工具的使用也一知半解,没有什么实际的意义,希望可以选择一个方面进行着重练习,才符合作业的存在意义。
2.互测屋中人数较多,而且越到后面的作业代码的整体复杂度越高,阅读每个人的代码几乎是不可能的,这就与互测设立的初衷“相互学习代码中的一些优点并寻找bug”所违背,使互测成为了自动测试对拍器的天下,大家只着眼于自己的分数,而没有人去真正地看别人的代码,适当地减少互测屋人数,让每一份代码都能得到充分的重视,更能实现互测的意义。
3.最后一个单元的两次指导书某些查询指令所包含的情况十分不明确,而且官方也没有及时地给出充分的解释,只有等同学们在论坛上提出问题之后才会回答,我自己在写的时候是等到解决了内心疑问后才开始动手设计架构的,但实际上这个时间点已经来到了指导书发布的两三天之后,而这个过程中由于许多未明确的情况导致根本无法入手,因为如果和自己的预想结果不一样则必将导致代码重构,这一单元的两次作业都是如此,很多情况都没在指导书中提前说明清楚,导致写起作业来十分的不舒服,希望可以重视一下指导书的质量,把可能存在的情况考虑全面。
最后,感谢一个学期以来老师和助教们的辛苦付出,希望未来OO课程越来越好。