BUAA-2024    OO-U3总结

李凌-22373240 学生 2024-05-16 10:01:03

BUAA-2024    OO-U3总结

写在前面

在面向《对象程序与设计课程》课程(OO)学习中的核心即为面向对象的思考分析方法,这种面向对象的思想在诸多问题的解决中带来了不同思路.在第二单元学习与实践过程中,笔者对于对象选择、封装复用以及高内聚低耦合等诸多思想获得了诸多体会。

本文用于总结OO第二单元的学习时间体会,供未来回顾学习,也为更多面向对象学习者留下少许资料以供参考。

hw_9

0.关于JML的一些废话

第三单元工作围绕JML展开,相比前几轮作业风格从自己架构变成了看图写话的风格,JML画的是图,我们按照要求用java方法实现需求.

JML是什么?依据笔者来看,它其实就是高级的注释,是注释是因为它描述一个方法的效果,而'高级'则体现在它比笔者过去随手写的白话注释要更加严谨与规范.

老实说,开始动手本轮作业时,笔者第一眼看到的便是指导书短了一大截,置于短的原因,一方面是任务内容搬到了JML中,更关键的一方面也在于严谨化的表达带来了表达效率的提高.改到JML中的指导书变得较为晦涩(不说人话),但是实际经过level0手册的洗礼后可以感受到编写java代码实际轻松了很多,JML在这里实际扮演了白话描述到java程序的桥梁,JML限制一个方法 的具体行为,实际让代码的思路变得更加清晰(写代码不太动脑子,爽).

1.任务分析与架构

本次作业的任务核心内容是编写一个交际软件中的用户与好友网络,并支持基本的用户联系情况查询,修改.代码的架构其实已经基本给出,作业实际内容为实现一个Person接口与Network接口,并完成一系列自定义异常种类.考虑到用户id唯一的特性,笔者自然的采用HashMap,以Id为索引存储Person用于管理NetWork中的persons和Person中的acquaintance+value,并以此为基础实现基本的增删查改操作.

2.Junit测试

至此主题任务已经完成,但指导书的最后一个"小"任务让笔者意识到问题没那么简单,那便是Junit测试要求.按过去先导课的经验而言,Junit以覆盖率的形式作出要求,因而只需设置不同条件走遍各方法的所有分支即可.但这次的Junit发生的变化,对Junit的测试强度提出了要求并引入了评测手段.评测中,Junit部分的Test代码会被提出用于测试若干个或正确或错误的方法,要求Junit能够实际分辨方法的正误.

那么问题就来了,每个人可以有自己的架构,若方法的架构不确定,Junit要如何评测?还记的前面的JML吗,在这等着呢.JML对方法的基本输入输出作出了约束,这便是Junit测试的抓手.测试主要分为两个方面.

首要的当然是结果的正确,方法的运行结果是否正确可以通过生成随机数据,分别交付待测方法与正确的模版方法,比较结果并进行断言检测.

其次即操作权限的检测.这一部分问题相对比较隐蔽,例如对本次检测的"三角关系数量统计"方法,其没有修改Persons的权限,需要检测该方法执行前后数据状况不变.这里由于接口的限制,对与数据执行前后的状况难以进行深拷贝记录,笔者采用了数据镜像方法(别人也叫影子数据),即生成两份相同数据,一份交付待测方法处理,略显粗暴但好用有效.

3.bug修复

在JML约束下其实单个方法不会出现致命的逻辑错误,但是应当明确JML仅描述方法的作用效果,描述方法并不代表实现方法.例如描述中可以随意出现的数组遍历,高运算量方法调用,但在实际使用中必须面对时间空间上的性能开销.

笔者在本次强测中低估了测试数据的强度,不幸TLE,笔者第一轮设计中也做出了一定的性能上的优化,包括hash替代数组遍历,以及寻路算法的hash优化.但对于需要反复寻路的方法,在并查集上笔者并没有设置合理的数据保存措施(其实还是偷懒).

bug十分明显,修复方式也十分简单,老老实实的写好了并查集的建立算法,并设置了usable可用该控制,由于关系清除行为对并查集十分不友好,笔者只想到了重算的笨方法,索性并没有这方面的定点爆破,bug成功修复.

hw_10

1.任务分析与架构

本轮迭代在前一轮基础上引入tag接口,需要实现tag相关的一些类增删查改方法,方法描述有JML,不必多聊架构,笔者吧中心放在了前一轮吃亏的性能上.这一轮迭代中有两个明显考察算法的操作,求最短关系中间人路径和最亲密好友对的查找.

最短关系人路径及针对两个有"联系"的person(参见上一轮isCircle方法),需要寻得两人之间最短的"关系路径",笔者基于上一轮实现的并查集,当有联系时在一个关系分支中广度优先搜索,使用hashset存储每一步的可达点集,以hash查找优化性能.

最亲密好友对的查找中首先牵涉了最亲密好友的查找方式,即查找一个person的value最高好友的id.这一步笔者采用了同步更新的策略,在person关系变动过程中实时更新这个最亲密好友,避免查找时的遍历.在此基础上其实最亲密好友对查找就没有那么复杂了,可以循环不重复的检查所有未配对且未检查者是否包含在一个最亲密好友对之中,统计筛查出的好友对数即可.

2.Junit测试

Junit测试方案笔者采取了与上一轮一样的策略,随机数据,影子记录,检查结果正确性和pure问题两个方面.但在测试中出现了检查不完全的问题.

课程组给出了约定,不存在刁钻数据才能测试出的问题,那就是数据的覆盖面不完全,在足够规模的随机数据下仍然无法覆盖的bug让笔者经目光投向了数据的其他性质.由于本轮作业的network可以理解管理为一个以person为节点的图数据结构,测试数据的图特点可能出现在考察范围中.于是笔者从分支数,疏密度和关键路径长度的角度进行尝试,最终发现测试点针对疏密图存在bug,推测是对没有好友的"孤立点"存在误判问题,遂控制network中:

关系数量 * 2 < person数量(离散老师喜闻乐见的抽屉)

便成功通过了测试,可喜可贺.

3.bug修复

本轮的bug依旧是围绕性能问题,本轮的考察点来到了tag中value求和问题.为避免在大量出现求和中调用时消耗大量时间,笔者采用的解决方法为喜闻乐见的记录数据的方式.即设置一个记录数据与一个相伴的脏位,在tag中persons未发生改变时,重复的求和请求其实只需一轮计算,当发生改变时通过脏位标志来重新调用求和方法.方法朴实但有用.

可是意外总是会出现的,由于tag与person之间的双向关联并不必然,当person之间关系value改变时,可能难以通知到tag并修改脏位.笔者确实没有想到好的解决方案,于是在network中设置了tag记录池,通过遍历的笨办法来更新,所幸达到了性能要求.

hw_11

1.任务分析与架构

本轮加入了message系统,person可以执行一对一发消息和按tag群发操作,发送message会产生相应的社会价值,对于redenvelope还会产生相应的money转交.本次实现的操作主要都为增删查改操作,按JML写而已.

2.Junit测试

写完基本代码发现没什么技术含量,这就说明Junit在等着你呢(OO恶心守恒定律),这一次的测试的方法为低热度的表情消息的清除,听起来不复杂但是其可修改的参数及其多且难以检验.

笔者使用的检测方法仍是前面提到过的"影子数据"(嘿嘿,这玩意真好用),数据一式两份避免麻烦的深拷贝问题.test中笔者认为一个最重要的考虑条件便是筛选阈值limit,limit需要保证有emoji热度可以达到,但又有的达不到,为了决定limit值,笔者通过调试测试热度的波动范围.

此外,一些踩过的坑也昨个记录:(1)message不要全发,笔者最终采用发一半的方法,这样才能检验message的删除效果.(2)初始生成关系网时可以适当提高关系数量,以为传消息有一个两人相连的要求,这样可以提高消息发送成功率.

总结

1.关于测试与Junit
  • 黑箱测试与白箱测试.
    • 在以往的作业中,代码架构,方法设计均由我们自己实现,在测试时代码运行状况对我们开发者透明,这便是白箱.白箱测试的内容更多,需要测试每个方法的效果是否符合预期,也需要测试整体的架构是否正确.
    • 而本轮作业中由于JML的使用,每个方法都有着详尽的,实际省去了我们的架构工作,此时的测试我们其实可以采用黑箱测试.黑箱测试意味着隐藏处理流程仅检查操作产生的效果,这一点其实与JML适配性极高,测试思路及尽可能的覆盖所有符合requires的输入,检查所有的ensures与assignable条件.
  • 单元测试、功能测试、集成测试、压力测试、回归测试
    • 单元测试:即划分出小个功能块(如一个方法)进行分别测试,这一测试中由于功能块规模小,其功能会十分明确,测试保证其功能符合预期.
    • 集成测试:建立单元测试基础之上,测试多个单元组成的更大模块的合作工作效果正确性,测试检验更高层次架构的合理性.
    • 功能测试:对于完成的模块,忽略其实现细节,进行黑箱测试,测试围绕:功能是否有效,是否符合文档,是否符合需求等等.
    • 压力测试:在基本测试正确的基础上评估性能,进行更大规模的数据测试,检查运行中的cpu占用率,内存占用率,命令执行效率等等.
    • 回归测试:在测试过程中发现缺陷并修复后进行,重复已经进行过的测试,防止修复行为产生了新的问题.
  • 数据构造策略
    • 在本轮的作业中,Junit测试被纳入了评测之中,为了检查出未知的bug,测试数据的生成策略变得十分重要.
    • 笔者认为,测试数据最重要的就是"全",尤其是在这单元JML把输入要求给的十分明白情况下,只要符合要求的输入情况就要一个不落的覆盖住.
    • 那么如何覆盖呢,首先当然要数据够大,不要想着自己手搓几个能混过去,麻烦还不一定过,循环+random是个好东西,笔者直接几千轮循环抬上,基本已经覆盖了绝大多数情况(反正跑的是评测机,测他丫的).
    • 此外不能忽视的是,random固然的随机,但随机不等于全面,不可以认为随机了就万事大吉了.在随机数据的背后,一些不起眼的参数可能会绕过一些的情况.hw10的测试就是最好的例子,设置的关系与人的数量比例不当则无法产生孤立点.为了解决,笔者将测试方案中的关键参数提出(如人数,关系数,信件数等),将参数也随机化(当然这里不能乱随机,有限制),如此在足够规模的测试轮数下通过测试补成问题.
    • 此外的便是极端情况,任何架构总会有几个特例,极端情况往往在随机测试中出现概率会很低,难以测出,也许只能特设数据测试,方法包括但不限于抓临界情况,卡最大/最小值限制,冲击int极限等等.所幸作业中并没有出现(赞美课程组),笔者做到前两部分就够用了.
2.架构设计与性能问题的思考

JML虽然免去了为设计架构而掉头发,但是课程组留下的设计性能需求会补回来.这轮作业的难点其实是个换皮的数据结构+算法题.

作业中主题为维护一个无向图(network),图中节点之间存在加权的双向联系,且每个节点存在唯一的ID.

首先对于基本的节点存储方式,按照JML写的数组肯定是不够用的,唯一的ID已经可以确定要用ID索引的hashmap存储节点,首先将基本查找复杂度压到O(1).

此外便是针对分区查询操作,实现并查集,将存在联系的节点归入若干个分支,笔者再次基础上顺便做好了各个节点ID到所属分区的hash映射,反正不废多少性能,还能防止大量分支出现是的遍历问题.

至此,算是笔者按照自己的代码习惯完成的全部.但很遗憾并不够用,笔者实现的部分能够解决的仅仅是分区查询与寻路部分性能问题.此外基于Tag的一系列求和,求均值等其实都属于较需要性能的内容,问题与解决方法都很朴素,处理方式为全部设置缓存味与脏位.

说了这么多归结到一起其实就是JML去除了架构问题,但也有可能限制住思考空间,实际实现中更重要的不是照着JML狂抄,而是分析JML到底想描述什么并合理的去将其实现,其中仍然需要性能等考虑

3.学习体会

要说这一单元的体验,是相比之前的几个单元难受的,JML的出现让任务需求变得难以读懂,让原本自由的搓代码变得束手束脚,反而是以往不想认真写的Test成为了每次作业中相对有乐趣的地方.笔者不禁开始怀疑,JML真的有用吗?

于是笔者尝试以JML的形式重新规划第一轮的作业(没错就是那个恶心的多项式).JML比起直接上手java代码要难写很多,想要完整的规划好一个方法需要将条件结果先全部捋清,反观笔者过去写下的若干个极其繁琐的方法,几乎无法用JML描述.

至此,不妨把思路逆转过来(bushi),也许是我们弄反了因果,问题不在JML,而在与笔者自己.当一个方法被痛苦地用JML描述出来时,它并不会像随手写出的方法缺少规划,产生的影响也绝对明确可控.在过去,我们往往难以阅读其他人的代码,甚至连自己的代码都不想阅读,这也许正是缺少了像JML这样的规范.在JML规划下,我们能更好地控制好一个方法的逻辑规模(否则JML的长度几乎指数增长),依靠JML相关工具,我们能够精确化的评估大块代码的功能正确性与分支情形的覆盖情况,甚至可以拥有科学化的方法推理出bug的位置.

如此反观,这一单元也许是让笔者这个java世界的原始人第一次穿上了鞋罢.

...全文
69 2 打赏 收藏 转发到动态 举报
写回复
用AI写文章
2 条回复
切换为时间正序
请发表友善的回复…
发表回复
王畅-22373217 2024-05-16
  • 打赏
  • 举报
回复 1

小黑!

李凌-22373240 学生 2024-05-16
  • 举报
回复
@王畅-22373217 小黑,呜呜,小黑

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧