OO 第四单元总结
本单元三次作业的架构设计
第一次作业
实际上本单元三次作业,做的是信息查询的工作。.mdj
文件是用 JSON 格式表示的 UML 图,通过官方给的转换器转换成 JSON 文件。每个 JSON 文件一个节点,可以在 AppRunner
中解析回原来的对象。整个是一个序列化与反序列化的过程,但 UML 图的各种关系需要被重建。
由于操作以查询为主且 UML 图各种元素的关系比较清晰,使用分类的方式进行重建,UmlClass
、UmlOperation
、UmlParameter
等类的对象按照单独的类处理,每个类有它的管理器来管理。顶层的 MyInteraction
负责从各种管理器中得到相应的数据,并进行查询和判断。管理器中普遍使用了记忆化,确保只遍历一遍 elements
就能记住所有需要记住的关系。
具体存储对应关系是以 Map
为主,关系对端的多重性用 Set
解决。
类图如下。
第二次作业
这次作业沿用了第一次作业的架构,新增的操作仍然是查询操作。新增两个类管理 UmlInteraction
和 UmlStateMachine
,需要注意元素间的对应和从属关系。由于类图查询的操作不变,原来的代码不需要修改。
类图如下。
第三次作业
这次作业同样沿用了第一次作业的架构,但是在检查方面有变化。R002 到 R004 涉及图的遍历,因此代码量比较大,需要新开一个新类 Checker
,让它实现 UmlStandardPreCheck
接口,同时沿用原来 MyUmlInteraction
的管理器。然后,在 Checker
类中实现检查操作,MyUmlInteraction
只负责调用 Checker
的方法。这样就可以通过管理每种对象的管理器,比较方便地进行扩展,同时也不破坏原来的架构,减少重构代价。
类图如下。
四个单元中架构设计及 OO 方法理解的演进
第一单元
第一次作业,感觉之后要把语法变复杂、把公式里的项变多,就采用了递归下降和采用 AST 的大体架构。这确定了本单元三次作业的基础。由于合并同类项和幂次属于数学规律,所以在对表达式和项的操作上把内置这些变换。同时,把项和表达式设置成不可变对象,减少运算时引用出错的可能性。感觉 OO 多少有为对现实世界做一个模型的思路,可以通过这个思路去写程序。
第二次作业,由于引入了三角函数,许多同学采用四元组做优化,但是基于 AST 的架构反倒不方便做这样的优化。采用把要输出的表达式再次处理的方法进行优化,却碰到了操作的麻烦,因为做优化需要依赖项和表达式的内部实现。体会到优化有时与架构是冲突的,而且需要更灵活的设计方法应对与原来未优化做法的内部实现相关的问题。实际上可以直接把表达式转换成四元组,然后在四元组的基础上做优化,但是感觉这种方法相当于写了两个程序,所以没有使用。实际上使用了的话,除了多耗费一些时间精力,也没有太大害处。体会到无论是 OO 方法本身还是具体的某种 OO 方法,都只是一种思维模式,面对现实不能特别强调某种特定的思维模式,无论这种模式之前多么管用。
第三次作业,引入了嵌套求导,AST 的作用开始显现出来。不需要改动原来的节点,只需要允许把表达式变成项就可以了。但是,把表达式变成项需要考虑单项表达式和单表达式项(single-term expressions and single-expression terms),在对这种过度嵌套的节点进行操作时需要解包。体会到有时架构可能是不完美的,但是面对一个特定的问题,有时需要很大的心力才能做完美,在现实中不一定可行;每种架构都有实现的特殊性(particularities),需要去应对和克服。OO 方法是有层级的,简单的问题用简单的方法,复杂的问题用复杂的方法。用复杂方法解决简单问题,反倒把方法用得僵化了,影响问题的解决。
第二单元
第一次作业,采用两个线程的设计和观察者模式,使得电梯和调度器合二为一。由于是阻塞 I/O,所以输入必须单开一个线程。两个线程之间由于是一对一通信,所以采用了生产者—消费者模式。这次架构设计用的时间,大概是真正开始写程序的一半。感觉架构设计好了,写程序会写得比较快。
第二次作业,由于是多个电梯,考虑过使用零调度。但是零调度是所有的电梯带保护地去一个请求队列取请求,感觉没法保证请求接收的稳定性。于是只好再开一个分配器线程,让该线程把请求分配给每个受调度的电梯线程。这种架构有一个缺点,就是没办法具体地进行特别微观的操作,统筹五部电梯的能力不强。但是可以复用生产者—消费者模式,不会出现线程安全 bug。同时,难以获知电梯的具体状态,只能用把乘客人数变成 AtomicInteger
然后让分配器线程直接读取这种反馈机制弥补。架构对思维的影响体现出来了,虽然是思维想出架构,架构也会束缚思维。如果用一个更灵活的架构,就可能会有比较有效的微观上的操作了。但是架构背后代表的安全性与性能的权衡也是比较矛盾的。实际上,如果不用 OO 方法,写这样的多个线程互相用消息管道通信的程序,不是太好写。因为线程和管道都能被抽象成对象,都是一个概念的具体化,OO 这种范式反倒更适合写出更安全的多线程程序,更适合需求在本质上(inherently)更复杂的情况,更不用说 Java 自带管程。
第三次作业,涉及到多部电梯动态处理请求和动态增加电梯。实际上这种方法的变化比第二次作业当然是多得多,需要让电梯之间的配合更强。同时,还需要考虑拆请求的问题,因为不是所有电梯都能到达所有楼层。于是改动分配器,让它能够处理电梯发来的反馈请求,让它能够把人看成事务,基于事务处理运输问题。同时,电梯和分配器之间可以互相传递消息,通过观察者模式让电梯和分配器包含各自处理消息的具体逻辑。通过在消息中携带异常,异常中携带新调度器,实现电梯的现场重调度(live rescheduling)。通过线程的中断机制,实现电梯线程能够在收到删除指令后从容地(gracefully)退出,虽然在最后并没有用到。感觉架构的选取实际上是具体问题具体架构,架构确定后对扩充后的需求的应对方法很大程度上是在原来的架构上修修补补,因为重构代价太大了。OO 方法还是要和语言、平台的一些特性结合起来,这样才能更大地发挥出它的作用。
第三单元
第一次作业,发现对算法有了更高的要求,同时功能有了更大的独立性。于是尽量采用比较高效的算法,把功能变成模块,数据结构也封装成类。更加感觉架构设计要根据具体问题来定,因为这次作业功能之间的依赖关系和数据依赖关系不是特别强,所以也适合比较扁平化的架构。
第二次作业,感觉性能要求比较严格,于是采用离散化,尽量不使用标准容器,因为标准容器用了太多对象,性能可能会有损失。同时,由于 Group
要依赖 Network
的某些操作,所以把 Group
放在 Network
中实现,真正的 MyGroup
中的函数几乎都是桩函数(stub),因为为了效率,实在没办法。同时,把数据结构同样封装成类。体会到架构设计只是考虑问题的一个方面,有时有些问题重要的不光是架构。OO 方法不但适合编写比较复杂的程序,同时也适合对这种程序进行测试,因为这次作业中使用 JUnit 测试数据结构感觉比较好用。
第三次作业,性能要求一般,但是某些方法仍然对性能有一定要求。这时由于机制和策略分离的设计思想和代码行数的限制,把常见的图算法也封装成类。同时,把原来在代码里的一些比较简单的数据结构(比如并查集)拆分出来,单独开类。桩函数和数据结构封装成类的操作都留着,减少重构的代价。架构设计涉及到的方面比较广泛,包括代码的放置、抽象层的分离等等,有很多变量需要考虑。OO 方法不仅可以准确、高效、可扩展地给需求做模型,也可以给代码做封装,提高程序的可维护性。
第四单元
第一次作业,感觉是对数据进行查询的操作。但是查询是分类查询还是保留 UML 图原来的结构,是一个比较需要考虑的问题。经过真正把 StarUML 使用了一遍以后,还是采用了分类查询的方式。因为分类查询避免了保留原来结构的高开销,在每行一个无序的 JSON 数据的影响下,每个 type
不同的数据都需要扫描一遍,但是分类查询也就需要扫描一遍,保留 UML 图原来结构的方式就不敢说了。虽然这样做的结果是代码的主要逻辑看起来是由一跳又一跳的查询数据的操作组成的。进一步感觉架构不仅和性能要求有关,还和输入与输出有关。不了解输入和输出的特性,就难以做出一个适合的架构。同时,进一步体会到 OO 方法在建模复杂系统时,相对面向过程方法巨大的灵活性。面向过程管理的数据只是一堆数据,OO 方法让这些数据有了更多的生命力。
第二次作业,整体上架构还是沿用第一次作业,但是需要对顺序图和状态图进行解析。这时在分类查询的架构下,只需要再把输入数据遍历两批,每批不会超过五遍。之后,就是把原来遍历好的数据再处理。感觉一个理论上不太优美的架构,面对实际问题的扩展性有时却很高。同时,这次作业可以使用 Java 标准库解决问题,Map
和 Set
等抽象数据结构的称呼有了更实际的意义。体会到 OO 方法对思维的适应性,以及对程序编写者思维的转变。
第三次作业,是在前两次作业的基础上加入了检查钩子,如果检查钩子不通过就不能执行。为了保证功能独立性和代码行数,采取了独立开类检查的方式。这种方式也需要提前把 UML 图的各种元素解析好,沿用原来的分类查询的架构即可。只是有些方面因为要适用 null
元素,做出了一定的改动。但是通过检查以后数据的某些保证还是确定的,所以不需要太大改动,而且之前有过一些判断(比如判断 UmlAttribute
的父元素是不是 UmlClass
来判断它是不是类图的 UmlAttribute
),所以需要的改动更少。感觉原来比较确定的架构,为了应对实际问题的变化,还是需要小改动,但是越成熟的架构,通常需要的改动可能越少。进一步体会到 OO 方法在数据和代码封装上的作用。
总结
一开始对架构并没有什么特别深刻的理解,只是感觉架构是一种对需求做模型的方式。在课程中,体会到架构对思维的限制作用,架构对封装和可维护性的帮助,以及架构和现实中问题、要求和运行环境的紧密结合。所以感觉架构是一个特别关键(vital)的概念,是程序中不可缺少的一个部分。
同时,对 OO 一开始只感觉面向的对象就是现实生活中的对象,只需要一对一映射就可以。在课程中,感觉自己对 OO 方法的理解更深入了。OO 实际上有对数据赋予的生命力,有对程序编写者思维的适应与改造,有对本质上更复杂需求的贴近,有对封装、性能与具体的思维模式之间的权衡与妥协。OO 是一个用程序看世界的新的、不可缺少的、不可避免的(inevitable)角度。
四个单元中测试理解与实践的演进
第一单元
第一次作业,考虑到生成规则就是上下文无关文法,决定采用它的黄金模型 ANTLRv4 测试解析器。同时,数据生成由自己写的 Python 脚本生成,也是采用了上下文无关文法的原理。试过 grammarinator 这种自动生成工具,总是觉得不灵活。[1]语法树上怎么按照一定的概率生成某种节点,以及如何生成变长列表而不死板地采用偏差树(deviation tree)的方式生成,grammarinator 都做不好。于是自己写了一个,沿用了三次作业。然后评测时考虑到输入和输出都是公式,而且由于有 CFG,所以利用 ANTLRv4 的 Python 绑定写了一个转换编译器(transpiler),直接把输出的公式转换成 Python 语句,用 eval()
执行,并使用 sympy 判断等价性。同时,采用 bash 写成的自动测试框架,使得生成、评测、正误样例分类自动化,方便测试驱动开发。
第二次作业,由于 sin()
和 cos()
涉及到浮点数运算,有浮点数误差了。同时 sympy 直接判断相等不能使用了,因为公式变得过于复杂。于是采用 Python 内置的 float
进行计算,如果偏差太大,就用标准库里的 Decimal
类进行计算。[2]同时,做好对 0 等特殊点的处理。感受到测试需要与真实情况相结合,不能认为测试时是比较理想的模型就忽略了测试本身的复杂程度。
第三次作业,由于采用了嵌套求导,在数据生成器中限制了嵌套层数。其它部分没有太大的变化。感觉测试本身也是有架构、有扩展性的,之前测试对架构的考虑,可能会对之后的测试服务。
第二单元
第一次作业,同样还是用 Python 写评测机,模拟和验证电梯及人员的状态。同时,在命令行参数中添加 -s
选项,使等待时间变成原来的十分之一,从而加速评测。然后,为了验证算法的效率,在每次评测之后让评测机输出性能相关指标,尤其是程序最后一次打印的时间戳,通过多次用随机生成的数据评测来测定性能。评测框架也做出了相应的改动,让它能够捕捉到每次评测机输出的性能相关指标。但是时间限制并不严格,而且跟平台相关的细节测试不到,比如 System#currentTimeMillis
的准确度。[3]每次输出的评测相关指标,可以通过画图的方式展现出来。画图的工具仍然是 Python 脚本,借助 matplotlib
画图。[4]感觉测试实际上要综合考虑环境、理解平台的特性。同时,测试可以与性能评价、性能剖析(profiling)相结合,衡量程序的性能。
第二次作业,测试架构没有太大的改变,同样是用 Python 写评测机。但是,通过 GNU Parallel 实现了多线程评测,同时每次评测的输出同样存放在文件中,与 -s
选项结合,大大提高了测试效率。[5]如果开 8 个 GNU Parallel 中的线程测试,又启用了 -s
选项,测试的吞吐量可以变成原来的 80 倍,大概是提高两个数量级。同时,采用 GNU Time 监测程序运行时间和占用内存,包括内核态、用户态和总计的 CPU 时间,运行内存,周转时间等。[6]用 Python 脚本解析 GNU Time 的输出,一旦超时就提示失败。同样是利用画性能图的方式优化算法,这次加入了多种算法性能图的比较。感受到测试软件也是软件,也可以用一般软件的设计思想,比如多线程等。它也与平台特性密切相关,因为在 Windows 下不太好找类似 GNU Parallel 的工具,而且进程启动代价是比 Linux 要高的,反倒不适合多进程测试。
第三次作业,仍然沿用了原来的测试架构,但是采用多种测试策略测试,经过分析、比较,选出看起来最好的策略。体会到了测试策略的重要性,感觉新测试策略的引入提供了观察程序性能的新视角。同时,感觉测试并不能保证一切,某些正确性应该建立在比测试更严格的基础上。
第三单元
第一次作业,通过对拍器进行黑盒测试,同时用 Python 写出标程。用 GNU Parallel 进行多进程测试。感觉多进程测试哪怕仅仅是为了缩短时间,也是值得的。
第二次作业,由于数据结构较多且比较独立,引入单元测试,使用 JUnit 编写测试代码。仍然沿用第一次作业的测试架构。在每个单元测试意义下的单元比较有测试价值的情况下,使用单元测试真的比较值得,因为能够集中精力进行测试,减小了 bug 查找、分析和修复的开销。
第三次作业,同样沿用前两次作业的方法进行测试。体会到测试流程比较重要,之前作业中养成的测试流程,都能让之后按照该流程进行测试比较习惯,测试的效率和效果都比较明显。
第四单元
第一次作业,通过对拍器进行黑盒测试,同时使用 JUnit 进行白盒测试。同样沿用之前的测试架构。但是,也通过阅读源代码,思考特殊情况,排查出了一两个 bug。感觉实际上用思考去测试应该也是必不可少的,因为思维也有工具不可取代的独特性。
第二次作业,由于顺序图太多,通过手工生成样例的方式保证分支覆盖率。同时,使用 JUnit 创建假对象(mock object)的方式更方便地进行白盒测试。黑盒测试虽然也讲究覆盖率,但是毕竟隔了一层,白盒测试往往在很多情况下更加有针对性,也更加精巧。
第三次作业,测试方法跟第二次作业类似。但是,通过对算法和实现的思考,重点检查图论算法的部分,理清了具体检查的步骤和要求。测试归根结底需要程序编写人员把程序本身要满足的要求想明白,不然测试的目标难以明确。
总结
测试是让程序满足需求的手段,因此测试首先需要明白有什么需求,然后要根据程序的结构、需求和特性定制测试方案,还需要结合多种策略,也需要与程序复杂度、性能评价、性能剖析相结合;但测试软件本身也是软件,因此测试的架构、易用性、复杂度、效率、与平台的良好结合也非常重要,同时从测试这个操作本身来看,也需要程序编写人员习惯测试的流程。测试本身也能提供观察程序的新视角。黑盒测试编写方便、在比较小且功能简单的程序中效果很好,但是与程序的具体实现挂钩、难以在规模上适应(scale)更大的程序、生成数据策略需要构思;白盒测试对程序的结构有洞察力、适应性强、需要更少的测试样例,但是代价较高、写得比较精细。编写程序有策略,但是测试的元策略,乃至关于测试元策略的元策略,也就是是否非要用测试来保证正确性,也是需要着重考虑的。
课程收获
Java 编程
感觉对 Java 编程的陌生心理逐渐消除,能够运用标准库写出一些比较小的程序。一开始对 HashSet
、ArrayList
等的实现确实不熟悉,有些遍历较频繁的地方使用了 Set
,有些添加、删除较频繁的地方使用了 List
。之后随着对标准库代码的阅读和编程的实践,感觉熟练程度提高了。感觉对一门语言的学习,实践当然是离不开的,但是标准库也是离不开的,阅读标准库的代码同时也是离不开的。
OO 思想
原来对 OO 思想只是能够理解到模仿现实世界这一层,之后随着学习的深入,感受到 OO 的内涵比这个深得多。同时,也学到了不少设计方法和设计模式。感觉 OO 其实有比较强的哲学背景,类和对象分别代表概念和存在,权限控制代表着某个操作是否归某个概念所有。同时,对象理解的概念和对象的实在,也不是一回事。垃圾回收意味着在概念世界不存在的东西,需要把它清除掉以避免占用内存。程序对自己的觉知和改造,类比思想对自己的觉知和改造,是通过反射机制实现的。OO 也是一个灵活的、与实践紧密相关的、看世界不可避免的角度。
架构
原来使用架构的时候,并没有考虑许多与架构设计相关的细节,也没有太考虑架构的可扩充性。OO 作业中的架构,进一步让我深化了对架构的理解,强化了架构的现实性,体现了一个可扩展架构的好处。同时,思考架构在编程中所用的时间,并没有想象中那么大,这进一步强调了架构的关键性,增大了用心思考架构的好处。同时,也实践了不少设计模式,而设计模式的实践性很强,不经过实践难以灵活掌握,所以也进一步加深了对设计模式的理解。
测试
原来对测试的思维比较局限,只局限于在黑盒测试基础上的测试,尤其是对拍。通过 OO 对质量的严格要求,学到了如何强调测试数据的合理性、覆盖性,如何分清黑盒测试、白盒测试,如何让测试手段互相结合、互为补充,如何提高测试效率、降低假正确和假错误率,如何缩短测试时间、堵上思维漏洞,如何通过各种方法来应用测试手段从而着重测试、通过其它方法去保证程序正确性从而不用测试。不但编写程序有策略,测试也是有策略的,同时也有很强的现实性。
分析
一开始写代码时,并没有分析的习惯,感觉代码的好坏写完了就能看出来。后来通过参与每个单元的研讨课,以及学习优秀同学的博客作业,逐渐明白了自己在面向对象程序设计中的不足,比如不重视单元测试等等。通过这些分析,不但掌握了程序分析技巧,学会了如何分析、评价、测试其他人的程序,而且让下一单元进行得更加顺利。
正则表达式
原来使用 grep
和 sed
的时候接触过一点正则表达式,但是并没有很认真地去学习。这次,有了学习的机会和课程组提供的资料,大体上感觉对正则表达式有了一些思路,也学会了如何去分析。感觉正则表达式被广泛用到,因此学到还是很值得的。
JML 规格
在接触 JML 规格以前,并没有考虑到还可以真的用按契约设计的模式来编写程序。原来只是在 IDEA 自动添加的注释(annotation)里看见契约(contract)这个词,但是并没有想到对整个程序的设计都可以利用契约来进行。接触过 JML 以后,思路一下子打开了。程序设计如果能够用契约来进行,那么可以严格地保证程序满足需求,把设计和实现比较干净地分离了。
JUnit 单元测试
单元测试不光是环境配置,还有思维的问题。体会到单元测试是无法被黑盒测试替代的。通过对每个模块独立地进行测试,能够把每个模块之间的组合延迟到之后测试,而且能够假定在测试模块组合时模块的功能是正确的。这样就把测试的复杂度从各个模块复杂度相乘变成了各个模块复杂度相加,大大降低了测试的复杂度,更有效地保证了程序的正确性。以单元测试为代表的白盒测试也有对程序结构有洞察力、能够保证覆盖程度的优点。
多线程
实际上,对多线程之前一直有一种恐惧心理,认为多线程一写就容易出错。但是通过仔细思考和认真测试,多线程出线程问题的概率可以降到非常低。认识的几个大佬都出过多线程问题,但是通过仔细排查都能够解决。因此,OO 课程提升了对多线程的理解和实践,加强了对多线程的应用和测试能力。同时,还学到了多线程的设计模式、常见的线程安全问题等内容。
三个具体改进建议
- 课程网站查看评测结果的地方,是所有的内容按照列表一个一个往下排列的。这样在桌面端信息密度不够大,在移动端下面查看具体评测结果时换行又比较麻烦。感觉可以在桌面端把 CPU 时间、真实时间、内存占用放到一行,排列成一行中的三个单元格,宽度平均分布就可以。在移动端可以把具体评测结果的字体缩小,或者弄一个链接到新的页面,在单独弄的这样一个新的页面里查看具体评测结果和反馈信息。同时,还可以把导出
stdin
、stdout
、stderr
和评测结果的按钮放到上面,方便需要测试的人直接导出。或者一键以 JSON 等格式导出全部测试点的测试结果也可以,毕竟网站的后端接口应该是 JSON 格式。 - 其实可以加大测试在预习作业中的比重,因为至少应该在预习的时候了解一下常见的测试方法与技巧。第一单元是多项式求导,对测试有一定依赖,因为字符串处理一般特殊情况比较多。可以直接在第一单元引入 JUnit 等测试工具,也直接在第一单元使用单元测试,这样写出的程序质量会好很多。尤其是希望讲一下怎么用好
@SetUp
和@TearDown
,怎么针对程序的特性、使用的数据结构来具体地构造测试,这样可以把测试真正地落实,让它有实际作用,从而切实地提高写出来的代码的质量。 - 可以把 OO 企业号跟课程网站的通知联动。实际上,感觉有发布通知的接口了,可以在发送课程网站的广播通知以后,直接往企业号里发送通知。现在感觉企业号发送通知的节奏和课程网站不太匹配,内容也有点不太匹配。不知道企业微信有没有自动发送消息的 API,如果有的话能不能和课程网站的后端对接上。虽然 OO 课程的节奏要求一周多次登录课程网站,但是感觉微信通知部署上的话,不但发送通知方便,大家的体验也会更加顺畅。同时,对 deadline 前时的通知效果也有提升,会减少压线提交的情况,因为微信又催了一遍。
线上学习 OO 课程的体会
感觉 OO 课程本来跟在线学习联系非常紧密,所以线上并没有感觉有太大的不方便。同时,课程视频也可以安排提前观看,提前深化对相关概念的认识,为作业或实验做好准备。同时,线上的课程还保证了大佬们在同一时间在线,正好可以在合适的时间,找到合适的人,问合适的问题(🌸🐔)。
总结
本学期的 OO 课程就这样圆满结束了,感觉学到了不少东西、收获很大。感谢课程组的各位老师和助教们的辛勤付出,以及各位大佬对我的帮助和启发。希望 OO 课程能够每年迭代改进,越来越好。
来源:oschina
链接:https://my.oschina.net/u/4324616/blog/4312973