OO第四单元作业总结与学期回顾
第四单元作业总结与学期回顾
一、第四单元作业架构
本单元作业的要求是实现一个UML解析器,在对UML文件进行解析后实现UML的一些基本的正确性检查以及查询功能。字符串的解析工作已由课程组完成,我们需要填充的部分为对UML元素的解析以及正确性检查和查询功能的实现。
1.1 程序运行流程
本单元作业代码的架构可通过程序的运行流程说明。
程序的运行分为四个阶段。
- 第一个阶段为字符串解析,将UML文件的字符串解析为具体的UML元素对象。这一步由课程组提供的官方包完成。官方包提供了许多由“Uml”开头的与UML标准相同命名的类作为这一步的输出。这一步处理结束后将把处理结果以一个
UmlElement
数组的形式传递到下一阶段。 - 第二个阶段为元素解析与建模,将
UmlElement
处理为具体的、便于后续正确性检查与查询的模型。一部分正确性检查也在这一阶段进行。用于建模的类命名以“My”开头,实现MyElement
接口,并统一包含一个对应的UmlElement
对象。 - 第三个阶段为正确性检查,对一些需要提供对应输出的检查项目进行检查。这一阶段是第三次作业添加了正确性检查的需求后才加入的。这一阶段仅对作业要求的R002-R004进行检查,其余部分在第二个阶段即已处理完毕。
- 第四个阶段为查询处理,对用户的查询指令作出反应,提供对应的输出。这一阶段实现了前两次作业的查询需求。查询功能的具体实现与维护在对应的
MyElement
中进行。
流程与涉及的类如下图所示:
本单元作业的架构十分清晰明了,下面对本人实现的后三个阶段进行比较详细的展开,以展现代码全貌。
1.2 元素解析与建模
该阶段在MyElementProducer
类中进行。这一节介绍几个这阶段处理的要点。
1.2.1 元素解析顺序
由于部分元素间存在依赖关系,且UML文件中的元素顺序无任何保障,若单纯地顺序解析传入的UmlElement
,部分元素的解析是无法完成的。要解决这个问题有两种方法:
- 遇到当下无法完全解析的
UmlElement
时将该元素暂存,或保存为另外的便于后续处理的形式,等待其依赖的元素解析完成后再进行解析。 - 将传入的
UmlElement
按依赖关系进行排序,使得所有元素依赖的元素都在该元素处理前得到处理。
在第一次作业的实现过程中,本人曾尝试过第一种解决方法。但这种方法有着明显的缺陷:一是一次暂存可能无法处理掉所有的依赖关系,届时或许需要再次暂存,进行第三轮、第四轮解析,元素遍历开销较大;二是这种方法不易修改,且复杂化了解析过程中的逻辑抽象层次。
因此,本人在第一次作业的最终版中换用了第二种处理方法。MyElementProducer
中定义了一个排序类ElementComparator
。该类确定了各UmlElement
的优先级,制定了UmlElement
的排序规则。元素解析前,使用该排序类对传入的UmlElement
进行排序。使用这种方法则可以在一次遍历中处理完所有元素,不需要为元素依赖关系设置更多的抽象,也方便进行依赖关系的修改。
1.2.2 元素建模
第二阶段的目的是为第三、四阶段建立可供操作的模型,即MyElement
对象,但在建模过程中实际建立的数据结构比传递给后面阶段的数据结构要多出许多。这多出来的部分模型用于辅助元素解析过程以及实现第二阶段中的正确性检查(后者在1.3中进行介绍)。
一个简单的例子是UmlParameter
类的解析。UmlParameter
对象的parentId
指向的是一个UmlOperation
对象,因此在处理UmlParameter
对象需要设法获取到其对应的MyOperation
对象。而在后两个阶段会使用到的模型中,MyOperation
作为MyClass
的成员存在。系统仅维护了一个MyClass
数据结构,无法通过MyOperation
的id
直接获取到对应的MyOperation
对象。因此,在元素解析过程中,MyElementProducer
额外维护了一个MyOperation
的数据结构用于UmlParameter
查找父元素,这就是元素建模中“多出来”的部分。
1.2.3 模型抽象
本单元作业在对UML元素建模时对课程提出的需求进行了高度概括性的抽象。
举例而言,UmlTransition
的抽象并没有传递到第三、四阶段,即模型中不包括UmlTransition
这一层次,MyState
直接通过MyEvent
互相进行连接。因为在需要实现的查询指令中,UmlTransition
的存在是不必要的。同理,UmlCollaboration
之类的几类UmlElement
在第三次作业引进正确性检查之前甚至根本没有在第二阶段进行处理。
因此,MyElement
的内部结构与对应的UmlElement
不同,这也是特地设置MyElement
这一系列类型的原因。尽管每个MyElement
都包含一个UmlElement
的引用,但不一定实现UmlElement
具有的功能。MyElement
将功能集中在程序需求的实现,屏蔽了那些与需求无关的部分。这样的设计固然降低了代码灵活性,增大了增量开发的难度,但也使得代码的结构更加清晰。
1.3 正确性检查
正确性检查在第三次作业引入,虽然占有了第三阶段这一运行阶段,但大部分正确性检查仍是在第二阶段的元素解析过程中同步进行的。
1.3.1 正确性检查的分类
第三次作业引入的正确性检查可分为两类。一类不要求额外输出,在发现异常后可直接抛出,不要求提供与异常相关的详细信息;另一类要求额外输出,在抛出异常时需要提供异常相关的信息。显然,R002-R004属于后者,其他错误属于前者。
由于题目保证输入的UML文件中只可能出现最多一类错误,且在发现错误后程序终止运行,不会进行第四阶段,因此前一类异常可以在发现的第一时间立刻抛出。这造成了两类正确性检查的处理方式的不同。
第一类正确性检查在第二阶段与元素解析同步进行,一旦发现错误,则元素解析中止,程序抛出异常后终止运行。第二类正确性检查在第三阶段进行,检查流程与第四阶段的查询类似,具体逻辑主要在对应的MyElement
中进行。
1.3.2 异常传递
第一类正确性检查在第二阶段MyElementProducer
中进行,而正确性检查的接口在MyImplementation
中实现,因此异常需要从前者传递向后者。由于异常的抛出是在第三阶段进行的,因此该异常传递不能通过方法的异常传递直接抛出,本人选择了添加一表示异常的变量,通过传递该变量传递异常类型。(也可以通过方法的异常传递进行,使MyInplementation
在初始化中catch到对应异常获知异常类型。但本人在上个单元玩异常传递翻车了,这次选择了保守的做法。)
MyElementProducer
在每进行一个元素的解析前对异常变量进行检查,若该变量已设置则说明错误发生,第二阶段终止,并把异常变量传递给MyImplementation
。第三阶段对应的正确性检查实现则是检查异常变量的值是否与正确性检查编号相同。
1.4 查询
查询阶段的处理与上一单元的模式接近,采用了许多在上一篇博客3.1,3.2中提到的做法,这里不再赘述。OO第三单元作业总结与心得 - hyc140 - 博客园 (cnblogs.com)
二、架构设计思维及OO方法理解的演进
架构设计思维以及OO方法的理解是面向对象课程的重点以及教学主题。四个单元的作业分别从不同的角度让同学们参与了代码编写的实践,从而使得同学们对以上内容产生了自己的理解。这一部分本人将从四个单元不同的角度说明本人的相关理解以及演进。
2.1 第一单元——层次化与模块化
第一单元的作业归结而言就是字符串的解析与处理,其中体现了OO风格的部分便是字符串解析的层次化。表达式的结构本身是层次化的。对每一层次的元素而言,该元素可以看作下一层次对应的元素序列,该层将这一元素序列解释为一个新的元素,由此形成新的元素序列传递给上层。这种层次化的工作具有结构上的重复性,在代码中就体现为代码的重复,而减少或消除这些代码重复的工作就是代码复用与封装。
面向对象就是一种以代码复用和封装为中心的概念。一个类或一个模块将其内部的具体实现与外界隔绝,只提供有限的接口供外部使用。这种模块化让每个模块专注于其他模块提供的接口,而无视其内部实现。当模块发生修改时,只要保证接口提供的功能不发生变化就能保证其他模块不受影响。对于功能相近的模块,也可通过继承等方式进行代码的复用,而继承等关系本身也形成了一种层次结构。
分层结构作为模块结构的一个子集很适合用来进行OO思想的初步训练。本人在第一单元的作业中进行过一次重构,这次重构让我深刻地感受到了OO模块思想以及代码结构的重要性。
2.2 第二单元——并发编程
第二单元通过电梯调度的背景让同学们进行了面向对象编程下并发编程的实践操作。虽然在这个单元中,本人与同学们关注的重心好像有些偏移(指疯狂卷调度算法),但这一单元带给我们的在OO方法理解上的收获也不少。
并发的概念在计算机科学的多个领域中都有所涉及,无论对于硬件还是软件,并发性都是系统为提高效率与灵活性所追求的重要特性。在第二单元的学习中,我们不仅了解了并发编程的相关概念,学习了线程的共享与同步的相关技巧以及死锁的处理方法,还了解了线程安全的相关规范。但这些内容其实属于并发这一领域的交集,并非面向对象独有的特点。
在面向对象的方法中,代码的构成是模块化的,因此并发的处理最好也以模块为单位进行。Java的synchronized关键字提供了简单的并发支持,而这个关键字的使用则体现了面向对象中并发编程的特点。
synchronized关键字可以用来修饰变量与方法,被该关键字修饰的变量与方法以此获得原子性。在实际的使用中,对这些成员的访问或调用可以在一定程度上无视并发编程带来的不确定性,这些成员因此获得了“线程安全性”。通过保证一个封装好的方法或对象的线程安全性来保证系统的线程安全性,比起通过保证相关代码执行顺序与加锁等内容的合理性来保证系统的线程安全性而言在结构上更加清晰,也更加易于维护。这种结构的清晰性正是面向对象方法所追求的。
很遗憾,本人在完成第二单元作业中并没有很好地实现面向对象方法应有的并发编程方式,基本上都在通过synchronized子句实现共享与同步。但在老师的解读以及自我的总结中,本人认识到了OO中应有的并发编程方式是怎样的,并且愿意进一步开展学习。
2.3 第三单元——规格的书写与理解
第三单元的代码作业是根据JML规格实现相应的接口功能,并且也在其他地方考察了JML规格书写的能力。这一单元的内容进一步地把我们思考的范围从代码中抽象出来,进入了设计的领域。
不同的编程语言有着不同的编程习惯,但规格往往是从设计的层面,抽离了语言的因素,从代码执行前后的现象出发规定程序的功能。也即是说,规格通过规定我们在黑箱外能够观察到的具体现象限制程序的行为。这种思考方式和面向对象的模块化相契合。正由于规格规定了模块的外在表现,而模块间的配合也仅需要了解对方的外在表现,因此规格具备了模块间需要了解的具体信息,仅通过规格,我们就能够进行架构的设计。
书写规格时需要做到不重不漏,理解规格时需要做到语义的转换以及充分的实现。这两者都在这单元的作业中得到了一定的训练。在这单元的学习之前本人并不了解规格,但在学习了规格的相关知识后,本人对程序架构的设计以及程序员在工作过程中的配合有了一个更加立体的认识。
另外,本人在这一单元中还对程序的优化产生了更加深刻的认识。这一单元的作业通过用户操作-程序反馈的模式交替进行,这一模式也符合当今大多数软件程序的工作模式。而在这种模式下视用户的操作行为不同,程序表现出的用时也会大有不同。本人在这种模式的实践下总结出了一些优化的思想,如1.4提到的那样,写在了上一篇博客中。
2.4 第四单元——UML的理解与使用
第四单元的重点是让同学们了解UML的概念并学会使用,代码作业总的来说算个附加产物,不是这一单元的重点。这一单元通过与第三单元不同的形式,同样在架构设计的层面上开展学习与讨论。
UML通过各式不同的表达方式从各个角度对程序的架构进行了描述。正因为程序的结构并不是由几个模块的互相关联体现出来的那么简单,所以一代代的相关从业者们都研究过程序结构表述的方式,而UML可以说是集大成者了。话是这么说,但这一单元的学习并没有给本人带来对于UML的特别清晰的认知,UML中一些元素的使用究竟需要参考怎样的标准也不是很清楚。虽然本人理解了UML的伟大与重要性,但在使用这一方面还有很大的进步空间。
不过,这一单元的代码作业是本人在四个单元中完成得最好的。这一单元的代码架构在本人看来几乎没有什么大的瑕疵,作业完成的过程中也十分顺利,算是这学期的OO课程学有所长吧。
三、测试的实践与理解
面向对象这门课程无疑是把测试放在了一个很关键的位置的。若不对代码进行充分的测试,即使轻松通过了中测也可能在强测受到惨痛的教训。而本人在本学期中受到了三次这样的惨痛教训。
先不论本人在测试上的实践,本人对于测试的理解确实在四个单元中逐渐明晰了起来,既在各方面理解了测试的重要性,也从助教与同学处学习到了许多测试的方法。
第一单元本人采取了手动构建数据的测试方法,通过考虑各种边界条件,有针对性地构建特殊数据。本人也考虑过采取自动生成数据加自动验证正确性的测试模式,但又意识到评测机的实现与作业代码的实现似乎没有什么不同,如果代码出现了问题那么评测机也理所当然地会出现问题,因此认为这种方法不可取。最终,手动构建的数据并没有全面测试到所有可能出bug的点,而且因为数据量过小,一些普遍性的bug反而没有被找到,测试效果不理想。
第二单元的输出正确性可以通过与代码实现截然不同的方式进行判定,因此本人实现了上一单元的设想,采用了自动生成数据加自动验证正确性的测试模式。在进行测试的过程中,有许多或明显或隐蔽的bug被测出,因此这样的测试模式取得了比上一单元理想许多的效果。但数据随机生成的逻辑没有覆盖到一些特殊条件下可能产生的bug,因此仍有不足。
第三单元本人又陷入了与第一单元时相同的困境。但观察其他同学的做法可以发现,大多数同学采用了对拍的做法,与其他同学实现的代码一起运行同一份数据,再比对输出结果,若结果呈现出有效的区别则说明有人出现bug。这种测试方式不要求实现评测机的结果检验逻辑,只需要比对几份不同的输出结果,因此更加易于实现。另外,课程组在这一单元强调了对模块进行单独的局部测试。由于这是本人一向的做法,因此没有发生什么值得一提的改变。
第四单元的测试可谓是举步维艰,数据极难生成,虽然也可采取随机生成的做法,但生成逻辑较难构思,很难生成有意义而有效果的测试数据。因此,这一单元的测试也主要依赖手动生成测试数据以及形式化验证。
四、课程收获总结
本人从本学期面向对象课程中取得的大部分主要收获都已经分布在第二、三部分内容中了。这里进行一些小小的归纳和补充。
- 对面向对象的架构设计有了一些具体的、深入的认识。尽管本人也曾应用面向对象语言进行过程序的开发与工程的搭建,但那时的代码架构基本上都是跟着感觉走,有时会体现出许多冗余的结构以及反直觉的协同关系。在完成面向对象课程代码作业的过程中,本人逐渐积累了足够的经验,能够有条理、有结构地进行代码的架构以及功能的实现了。
- 体验到了根据他人需求完善代码的开发过程。代码的搭建与功能的实现往往不是一个人的工作。面向对象课程以发布要求并进行版本迭代的方式模拟了实际开发过程中的工作流程,尤其是强调了测试的重要性,以实际工作的评价标准为我们积累了开发经验。
- 加深了对程序测试重要性的认识。一切尽在不言中,强测带给本人的伤害已经将测试的重要性牢牢地烙印在了本人心中。
- 实际实践了许多了解过但没有上手过的技术。包括并发编程、git以及作业与测试过程中用到的一些小trick,许多东西是我以前就有所了解但没有实践过的。面向对象课程提供了许多实践的机会,让我们能够实现知识从脑到手的流动,积累编程的经验。
五、课程改进建议
既然课程组让提三点,我就只提三点吧。😃
- 研讨课可以多些引导。看得出来研讨课的设计十分用心,但同学们在规则比较“丰富”且讨论主题公布时间往往较晚的研讨课中不太容易发挥出课程组设想的水平。基于经验与习惯的差距,我们不太能很好地带入到课程组为我们设置的几个角色中,分工要求有时甚至会阻碍我们的思想交流。最后呈现出来的就是我们觉得自己讨论不出什么,干脆摆烂,完成任务了事,但老师又十分清楚该讨论什么,怎么讨论,但只能眼睁睁看我们摆烂的尴尬局面。
- 课程关于测试的内容可以更加重视些。这里说的是教学的过程,而非考核的内容。现在课程对测试的考核已经可以说是比较严格了,因为中测很容易通过,甚至往往没有检测完所有的功能,而强测比起中测来跨度很大。老师与助教也多次强调了测试的重要性,看得出来测试是我们课程的一个重点。但课堂上关于测试的讲解内容还是相对较少,我们理解了测试的重要性却往往不清楚该怎么进行实践。希望课程组可以就这个方面多些引导。
- 课程的评分标准可以在适当的地方给得更加清晰些。现在评分标准的公开部分在我看来显得有些奇怪。对于一些给分的细节,课程组把评分的方式设置得十分复杂,描述地十分详尽,但对于各部分给分的比例却是只字不提。互测部分的得分在每次作业后都告知给了我们,但我们却不清楚这些具体的数字是怎么参与分数计算的。互测的得分往往在10分以内,这10分与强测的10分相比是否占有同样的权重?和中测相比呢?在这些信息不够清楚且互测得分没有上限的情况下,告知我们互测得分的效果十分有限,因为我们根本不知道这分有多大用处,顶多和上次互测的表现进行一下比较。
虽然看上去抱怨了许多,但我还是真心感谢课程组为我们提供了这样一门优质的课程。祝老师同学们学业进步,事业顺利!