「BUAA OO」第三单元总结

前言

本博客记录了笔者在面向对象编程第三单元的学习过程与相关思考。

  • 复杂度分析使用IDEA的插件MetricsReloaded
  • 代码量分析使用IDEA的插件Statistic
  • UML图绘制使用draw.io绘制

题目说明

面向对象编程第三单元的主题为:基于规格的层次化设计。

  • 第一次迭代:根据 JML 规格实现 PersonNetwork 接口
  • 第二次迭代:根据 JML 规格实现 Tag 接口及与其相关的方法
  • 第三次迭代:根据 JML 规格实现 Message 接口及其三个子类

三次迭代

第九次作业

设计与架构

本次迭代新增:

  • 根据 JML 规格实现 PersonNetwork 接口
  • 通过继承实现四个异常类
图的分支

本次作业中 isCircle 方法查询 Network 中两点的连通性(亦即是否处于同一分支), queryBlockSum 方法查询 Network 中的分支(极大连通子图)数目。直接实现前者(深搜、广搜)似乎可行,但直接实现后者可能会使时间复杂度过高,因此需要维护一个降低该查询操作时间复杂度的数据集合。笔者选择直接维护 Network 的所有分支 communities ,每次添加/删除关系时都对其进行更新:

public void addRelationToCommunities(int id1, int id2) {
    HashMap<Integer, Person> community = new HashMap<>();
    //向community里添加id1和id2的旧community中的键值对
    communities.add(community);
}

private void removeRelationFromCommunities(int id1, int id2) {
    if (/*id1、id2之间已经不再联通*/) {
        communities.add(community1); //添加包含id1的community
        communities.add(community2); //添加包含id2的community
    }
}

维护 communities 的两个方法时间复杂度均为 O ( n ) O(n) O(n) ,而查询方法 queryBlockSumisCircle 时间复杂度则减小到 O ( 1 ) O(1) O(1) (后者的时间复杂度其实和分指数有关)。

三角关系

本次作业的 queryTripleSum 方法查询 Network 中的三角关系(三个点互相相邻)数量。官方给出的 JML 规格如下:

/*@ ensures \result ==
  @         (\sum int i; 0 <= i && i < persons.length;
  @             (\sum int j; i < j && j < persons.length;
  @                 (\sum int k; j < k && k < persons.length
  @                     && getPerson(persons[i].getId()).isLinked(getPerson(persons[j].getId()))
  @                     && getPerson(persons[j].getId()).isLinked(getPerson(persons[k].getId()))
  @                     && getPerson(persons[k].getId()).isLinked(getPerson(persons[i].getId()));
  @                     1)));
  @*/
public /*@ pure @*/ int queryTripleSum();

如果直接按照 JML 中的逻辑来书写这个函数,其时间复杂度将是 O ( n 3 ) O(n^3) O(n3)强测寄喽肯定不行。所以要么维护一些相关信息,要么自己编一个 O ( n ) O(n) O(n) 的算法。前者的难度显然更小一些:

private int tripleCount;

private void addRelationToTripleCount(int id1, int id2) {
    for (/*遍历id1的acquaintance*/) {
        if (p.getId() != id2 && p.isLinked(getPerson(id2))) {
            tripleCount++;
        }
    }
}

private void removeRelationFromTripleCount(int id1, int id2) {
    for (/*遍历id1的acquaintance*/) {
        if (p.getId() != id2 && p.isLinked(getPerson(id2))) {
            tripleCount--;
        }
    }
}
架构图

第九次作业架构图

程序结构分析

代码量分析

本次作业 Source Code 共354行,主要集中在 MyNetwork 类。该类继承了 Network 接口较多的抽象方法。

第九次作业代码量分析

复杂度分析

本次作业复杂度整体不高,主要因为为复杂度较高的方法维护了一些查询相关的数据。

第九次作业复杂度分析

第十次作业

设计与架构

本次迭代新增:

  • 根据 JML 规格实现 Tag 接口方法及与其相关的方法
  • 通过继承实现四个异常类
最佳搭档&情侣

本次作业定义了最佳搭档,best acquaintance:Person 中对应 value 最大值的最小 id ;基于此又定义了情侣,couple:互为最佳搭档的两个 Person 。与二者对应的为 Network 类中的 queryBestAcquaintancequeryCoupleSum 方法。笔者为两个方法分别维护了相关数据,主要涉及两个函数:

//MyPerson:
private void manageBestId(int personId, boolean modify, boolean reduce) {
    if (/*是修改操作 && 修改的是bestId && 修改量为负*/) {
        //重新寻找bestId
        return;
    }
    if (value.containsKey(bestId) && value.containsKey(personId)) {
        if (/*传入的personId为对应value最大值的最小id*/) {
            bestId = personId;
        }
    } else if (!value.containsKey(bestId)) {
        //重新寻找bestId
    }
}

//MyNetwork:
private void manageCoupleSum(int id1, int id2, int oldCp1, int oldCp2) {
    if (/*id1与id2曾经为情侣*/) {
        coupleSum--;
    }
    if (/*id1曾经有一个情侣 && 情侣非id2*/) {
        coupleSum--;
    }
    if (/*id2曾经有一个情侣 && 情侣非id1*/) {
        coupleSum--;
    }
    if (/*id1与id2现在为情侣*/) {
        coupleSum++;
    }
    if (/*id1现在有一个情侣 && 情侣非id2*/) {
        coupleSum++;
    }
    if (/*id2现在有一个情侣 && 情侣非id1*/) {
        coupleSum++;
    }
}
寻找最短路径

queryShortestPath 方法需要寻找两个 Person 之间的最短路径。笔者一开始使用的 bfs,即广度优先算法。该算法最多遍历 Network 图中的所有边和点,时间复杂度为 O ( E + V ) O(E+V) O(E+V)。但这种算法在第十次作业的强测中喜提ctle 复活赛打赢了,笔者遂采用了双向 bfs 的算法。

双向 bfs,或说图的双向搜索算法,是对于广度优先搜索算法的一种改进。bfs 从起点开始由浅入深地遍历,直到找到终点;而双向 bfs 则从起点与终点同时开始遍历,直到遍历的区域相交(说明找到了最短路径)。下图展示了 bfs 与 bidirectional bfs 的遍历区域,可以看出后者的遍历范围恰为前者的一半:

两种算法

架构图

第十次作业架构图

程序结构分析

代码量分析

本次作业 Source Code 共799行,主要集中在 MyNetwork 类。该类继承了 Network 接口较多的抽象方法。

第十次作业代码量分析

复杂度分析

本次作业复杂度相较上次有所增加。其中 MyNetWork 类复杂度较高,主要因为该类包含了复杂度较高的 addPersonToTagmodifyRelation

第十次作业复杂度分析

第十一次作业

设计与架构

本次迭代新增:

  • 根据 JML 规格实现 Message 接口及其三个子类
  • 通过继承实现四个异常类

本次迭代的方法都比较基础,只要按照 JML 规格一步一步来就可以,不涉及到任何的算法。

架构图

第十一次作业架构图

程序结构分析

代码量分析

本次作业 Source Code 共1130行,主要集中在 MyNetwork 类。该类继承了 Network 接口较多的抽象方法。

第十一次作业代码量分析

复杂度分析

本次作业复杂度相较上次有所增加。其中 MyNetWork 类复杂度较高,主要因为该类包含了复杂度较高的 addPersonToTagmodifyRelation 以及 sendMessage 方法。
第十一次作业复杂度分析

测试过程

测试思想

黑盒测试

黑盒测试,把程序看作一个不能打开的黑盒子,在完全不考虑程序内部结构和内部特性的情况下,在程序接口进行测试。黑盒测试着眼于程序外部结构,不考虑内部逻辑结构,主要针对软件界面和软件功能进行测试。
——百度百科,黑盒测试

评测机其实就是一种黑盒测试,它只关心程序是否能产生正确的输出,不关心程序内部是如何运作的。

白盒测试

白盒测试是一种测试用例设计方法,盒子指的是被测试的软件,白盒指的是盒子是可视的,即清楚盒子内部的东西以及里面是如何运作的。“白盒”法全面了解程序内部逻辑结构、对所有逻辑路径进行测试。
——百度百科,白盒测试

本单元要求学生设计的 Junit 测试便是一种白盒测试,它要求被测方法的行为完全符合规格说明,不能产生越界的行为。

测试方法

功能测试是对产品的各功能进行验证,根据功能测试用例,逐项测试,检查产品是否达到用户要求的功能。

单元测试

单元测试(英语:Unit Testing)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法。单元测试的目标是隔离程序部件并证明这些单个部件是正确的。
——Wikipedia,单元测试

本单元的 Junit 也是一种单元测试,它对基于规格实现的方法进行正确性检验。

功能测试

功能测试就是对产品的各功能进行验证,根据功能测试用例,逐项测试,检查产品是否达到用户要求的功能。功能测试也叫数据驱动测试,只需考虑需要测试的各个功能,不需要考虑整个软件的内部结构及代码。
——百度百科,功能测试

功能测试其实就是黑盒测试。

集成测试

集成测试(英语:Integration testing),即对程序模块采用一次性或增值方式组装起来,对系统的接口进行正确性检验的测试工作。整合测试一般在单元测试之后、系统测试之前进行。实践表明,有时模块虽然可以单独工作,但是并不能保证组装起来也可以同时工作。该测试,可以由程序员或是软件品保工程师进行。
——Wikipedia,集成测试

集成测试将几个正常运行的模块组装在一起后测试它们的正确性。

压力测试

压力测试(英语:Stress testing)是针对特定系统或是组件,为要确认其稳定性而特意进行的严格测试。会让系统在超过正常使用条件下运作,然后再确认其结果。
——Wikipedia,压力测试

到目前笔者还没用使用过这种测试方式,但似乎航空航天设计经常会使用这种测试方法(确认系统在什么条件下会损坏,以及安全使用条件)。

回归测试

回归测试是指修改了旧代码后,重新进行测试以确认修改没有引入新的错误或导致其他代码产生错误。自动回归测试将大幅降低系统测试、维护升级等阶段的成本。
——百度百科,回归测试

这个方法也没有使用过,但看起来挺有用的。

数据构造

本单元笔者只写了一个简单的数据生成器。个人认为构造的数据首先应保证测试到 JML 规格中的每一个方法的所有 behavior,也就是方法的正常行为与异常行为;其次可以加入一些捏造的特殊数据来测试一些易错方法的实现(如 addPersonToTag 时 tag 数量超过1111的情况)。由于时间有限,考虑的情况并不是很全面,因此笔者的数据生成器只在本地测试中发挥了一点作用。

本单元各类方法繁多且 JML 规格约束较多,因此白盒测试更适合检查本单元代码实现的正确性。

程序bug

本单元三次作业强测、互测均未出现bug。

性能问题

契约式设计

契约式设计(英语:Design by Contract,缩写为 DbC),一种设计计算机软件的方法。这种方法要求软件设计者为软件组件定义正式的,精确的并且可验证的接口。
——Wikipedia,契约式设计

契约式设计提供了一份类似商业契约的“契约”,该“契约”会约束“客户”(调用方法的模块)以及“供应商”(被调用的方法)的行为并保障它们的权利。这种设计思想可以有效减少代码中的bug数量,提高代码的可靠性与可维护性;但由于契约式设计需要为每个类书写详细的契约(契约长度往往会超过代码本身的长度),这种设计方式会显著地降低开发的速度。因此在实际应用中往往在设计代码的核心模块中使用,以保证核心模块的正确性。

契约式设计的核心在于分离规格设计与代码实现,具体如下图:

设计与实现相分离
由于 JML 是一门非常冷门的建模语言,课程组并未让学生们完成由需求设计规格这一层次,而是将三次迭代的重点放在了由规格实现代码。

Junit测试

当我们完成代码实现后,需要检验自己的代码是否符合规格说明书,这也就引出了 Junit 测试:

Junit测试
Junit 测试由规格设计,旨在验证代码是否符合规格说明书。值得注意的是,使用 Junit 测试的前提是拥有一个完全正确的规格,否则 Junit 测试的正确性完全就是空谈。

数据生成器

数据生成器应该保证其所生成的数据:

  • 充分覆盖 JML 规格中各个 behavior 的 requires:
    • JML规格中已经描述了每个方法可能出现的各种正常、异常情况,我们在生成每个方法对应的指令时,一定要为每种情况设置一定的出现概率。
  • 充分覆盖 JML 规格中的各个 ensures:
    • 方法的具体行为主要被 JML 规格中的 ensures 限定,因此我们构造的数据需要覆盖到方法的具体行为以保证测试强度。

断言

  • 深度检査 JML 规格中 not_assigned 的属性:
    • 保存一份方法执行前的深拷贝,保证各个属性未发生变化。
  • 逐行检查 JML 规格中 requires 与 ensures 语句:
    • 直接按照 JML 的逻辑检查,保证没有越界行为。

心得体会

  • 本单元中,笔者大致掌握了阅读 JML 规格,尝试了 JML 规格的书写;理解了契约式设计的思想,并感受到了其诸多优势。在准备研讨课的展示时,查询了不少相关资料,发现契约式设计应用在谷歌 Chromium 浏览器、微软 .Net 4.0 等诸多开发领域。
  • 本单元对于 Junit 的学习,让我理解了 Junit 的使用方法与其存在的意义。

建议

  • 没有必要让学生实现十二个异常类,可以给出两个接口:OnePersonExc 与 TwoPersonExc,减少无意义的代码书写过程。
  • 可以将 JML 替换为 Contracts For Java,后者似乎没那么冷门。
  • 基于规格的层次化设计有两个层次,一是根据需求设计规格,二是根据规格实现代码。课程组将绝大部分重心都放到了后者之上,个人认为不是很合理。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值