BUAA_2024_OO Unit3 JML规格与社交网络

刘思涵-22371386 2024-05-16 12:12:58

前言

本单元的主要学习任务是基于JML规格的程序设计,我们需要理解体会规格语言对于程序开发带来的帮助。除此之外,我们还需要在规格约束的基础上,进一步优化实现方法,减轻时间复杂度,体会规格和实现的区别。

测试

黑盒测试与白盒测试

在以往的作业中,因为每一个函数的界限不明确,因此我们通常选择黑盒测试,黑盒测试构造简单,但是因为是从整体程序角度进行测试,对单个函数测试的效果较差

相对于在往次作业常用的黑盒测试,在有规格的情况下,使用白盒测试能够强化测试效果,因为规格定义的方法功能十分明确,可以针对规格中的每一条进行白盒测试,而白盒测试是针对方法进行测试,能够更完善的测试每一个通路

除了方法以外,还要注意各位置的变量约束,例如变量是否发生改变,或者变量内部数据是否改变

通过Junit进行每一个方法的独立测试,在测试之前需要先设置好测试数据,数据可以通过合理的方式自动化构造,但是要注意覆盖面避免出现重复上千次的无效测试

测试要考虑到边界条件,例如在值为空,或者接近满的情况下如何应对

对于以下几种测试方法

单元测试:

  • 针对单个函数进行的测试,尽可能覆盖函数的每一个路径

功能测试:

  • 针对规格中定义的一个功能进行测试

集成测试:

  • 在单元测试之后,对于多个单元实现的业务逻辑进行测试

压力测试:

  • 在极端情况下,对一个功能进行测试,检测其性能

回归测试:

  • 对程序进行新的开发之后,测试以往函数的功能,保证新的修改不导致程序已有功能故障

数据生成

以sendMessage为例,数据生成时要充分覆盖到规格的每一个功能

img

上面的Type主要分为0和1两种,代表消息的类型不同,在测试时我们可以假设调用者的预先准备符合requires要求,因此不在其中的数据暂且忽略,以下只分析type == 0的情况

除此之外的要求有,containsMessage和isLinked两个可能报错的条件,因此可以针对这两句,构造少量的数据检验报错是否正常

img

在此处,有对于RedEnvelope类型信息的特判,因此可以构造部分RedEnvelope类型信息进行测试

img

这里还有关于非RedEnvelope类型的特判,因此所有的类型都需要考虑,并且加入到生成的信息种类中

例如:Message,RedEnvelopeMessage,NoticeMessage,EmojiMessage

后续关于EmojiMessage的特判也相同

img

此处提出,接收者的messages数组可能发生变化,因此要考虑数组为空和数组有较多内容的边界情况

除此之外,尽管我们可以通过随机生成得到大量测试点,并且也有得到边界值的可能性,但是概率太低,在有限的测试次数下难以针对边界值进行测试。

所以关于数据生成时,应当采取大规模随机测试点结合手动构造的极端测试点的方法,因为规格的存在,所有潜在的边界条件都有明确的标明,可以减轻我们构造测试点的工作量

规格模版和断言构造

Junit测试中使用assertTrue(),assertEquals()之类断言,检验程序运行结果和预期相同

而规格已经为我们定义了ensures之类断言,我们只需要按照其规定的条件去进行判断即可

但除此之外,也不要忘记not_assigned, assignable, invariant, safe之类,对于数据的保护

以下针对JML规格中可能出现的几种修饰,列出对应ensures的解决方案

\ensures : 直接断言

assertTrue(expr)

\result :

 T result = func()
 assertEquals(func(), expr)

\old(expr) :

oldExpr = get().clone()
func()
Expr = get()
     要注意\old只修饰括号内元素的引用,例如\old(hashMap).get(0)和\old(hashMap.get(0))是不同的,前者表示运行前hashMap地址在运行后get(0),而后者表示运行前hashMap地址在运行前get(0)

\not_assigned 和 \not_modified类似,前者表示被赋值,后者表示被修改

\forall var; condition; expr : 可以用for循环与断言配合解决

 for(var = condition_min; condition; var++) {
     assertTrue(expr)
 }

\exists var; condition; expr : 可以用for循环与flag配合解决

flag = false
for(var = condition_min; condition; var++) {
    if expr then flag = true
}
assertTrue(flag)

\sum var; condition; expr : 可以用for循环解决

sum = 0
for(var = condition_min; condition; var++) {
    sum += expr
}

\product var; condition; expr : 可以用for循环解决

sum = 1
for(var = condition_min; condition; var++) {
    sum *= expr;
}

\max, \min var; condition; expr : 可以用for循环和if解决

ans = 0/MAX
for(var = condition_min; condition; var++) {
    if expr >/< ans then ans = expr
}

\num_of var; condition; expr : 可以用for循环和if解决,可以替换为\sum var; condition && expr; 1

sum = 0
for(var = condition_min; condition; var++) {
    if expr then sum++
}

A <==> B : A等于B,和==相似,但是优先级更低,可以用于判断是否同时相等

(A) == (B)

A <=!=> : 同理

(A) != (B)

A ==> B : 推导关系,可以用if和断言解决

if A then assertTrue(B)
在A不满足时无需考虑
之后结合sendMessage方法举例

img

对于此类直接ensures的断言,可以直接使用assertTrue(!containsMessage(id))

img

此处出现了old修饰

oldMessage = getMessage(id)

oldSocialValue = oldMessage.getPerson1().getSocialValue()

sendMessage(id)

oldMessage.getPerson1().getSocialValue() == oldSocialValue + oldMessage.getSocialValue()

img

此处出现了==>符号,可以使用if (oldMessage instanceof RedEnvelopeMessage) { assertTrue(Expr) }

之类的结构,在不满足条件时,不需要断言

img

img

此处出现的not_assigned和invariant都需要保证数据不变,例如assertEqual(old, new)

img

此处出现的exists修饰,可以使用for循环嵌套flag的结构,即

flag = false

for (int I = 0; I < emojiIdList.length; i++) {

 if (emojiIdList[i] == oldMessage.getEmojiId && 

     emojiHeatList[i] == oldEmojiHeatList[i] + 1) {

     flag = true

 }

}

assertTrue(flag)

img

这里的forall修饰,直接使用for循环的结构即可验证

for(int i = 0; i < oldMessages.size(); i++) {

 assertEquals(oldMessage.getPerson2().getMessages.get(i+1), oldMessages.get(i))

}

图架构设计和维护策略及性能分析

本单元作业中的Network类,本质上是由多个Person组成的一个图,在三次开发中,需要查询图中内容的函数总共以下几种:

  • addPerson:加点
  • addRelation:加边
  • modifyRelation:修改路径长度,删边
  • queryValue:求边长
  • isCircle:检查是否相连
  • queryBlockSum:检查图的联通分支数量
  • queryTripleSum:检查图中的三元环数量
  • addTag:加子图
  • addPersonToTag:将点加入子图
  • queryTagValueSum:子图的路径长度
  • delPersonFromTag:从子图删点
  • delTag:删除子图
  • queryShortestPath:两点之间的最短距离

以下介绍这几个函数的实现方式和优化方式

在具体实现中,我选择通过维护的方式,因为当时还不了解并查集,因此用了其他的方式,而具体需要维护的内容包括各个点所在的联通分支,以及三元环

public void addPerson(Person person) throws EqualPersonIdException {
    int id = person.getId();
    if (!containsPerson(id)) {
        Map map = new Map(person);
        persons.put(id, person);
        maps.put(person, map);
    } else { throw new MyEqualPersonIdException(id); }
}

此处的addPerson,向图中增加了一个点,但因为与其他点不相连,因此也是新增了一个联通分支,即person及其对应的分支map,这个函数复杂度约为O(1)

public void addRelation(int id1, int id2, int value) throws
        PersonIdNotFoundException, EqualRelationException {
    Person person1 = getPerson(id1);
    Person person2 = getPerson(id2);
    if (person1 != null && person2 != null && !getPerson(id1).isLinked(getPerson(id2))) {
        ((MyPerson) person1).link(person2, value);
        ((MyPerson) person2).link(person1, value);
        maps.get(person1).newLink();
        ((MyPerson) person1).updateValue(person2);
        ((MyPerson) person2).updateValue(person1);
        if (!maps.get(person1).equals(maps.get(person2))) {
            Map map1 = maps.get(person1);
            Map map2 = maps.get(person2);
            map1.putAll(map2);
            for (Person person : map2.keySet()) { maps.replace(person, map1); }
        }
        for (Person person : ((MyPerson) person1).getLink()) {
            if (person2.isLinked(person) && !person.equals(person2)) {
                Triple triple = new Triple(person1, person2, person);
                tripleMap.put(triple, true);
            }
        }
    } else {
        if (!containsPerson(id1)) { throw new MyPersonIdNotFoundException(id1); }
        else if (!containsPerson(id2)) { throw new MyPersonIdNotFoundException(id2); }
        else { throw new MyEqualRelationException(id1, id2); }
    }
}

此处增加了一个边,如果两个点本就在同一个联通分支,那么加边后仍不变,否则两个图会被添加到同一个分支中,因此需要将其中的一部分全部替换为另一个点的分支图

除此之外,因为增加了value,因此子图路径长度之和也会发生改变,需要进行维护,以及三元边,通过遍历两个点共同所有的点,即可找到对应新增的三元边,这个函数的复杂度约为O(n)

public void modifyRelation(int id1, int id2, int value) throws
        PersonIdNotFoundException, EqualPersonIdException, RelationNotFoundException {
    Person person1 = getPerson(id1);
    Person person2 = getPerson(id2);
    if (person1 != null && person2 != null && id1 != id2 && person1.isLinked(person2)) {
        if (person1.queryValue(person2) + value > 0) {
            ((MyPerson) person1).modifyValue(person2, value);
            ((MyPerson) person2).modifyValue(person1, value);
            ((MyPerson) person1).updateValue(person2);
            ((MyPerson) person2).updateValue(person1);
        } else {
            ((MyPerson) person1).updateValue(person2);
            ((MyPerson) person2).updateValue(person1);
            ((MyPerson) person1).removeLink(person2);
            ((MyPerson) person2).removeLink(person1);
            maps.get(person1).removeLink();
            if (!maps.get(person1).fullLinked()) {
                if (!preMap.containsKey(person1)) { preMap.put(person1, true); }
                if (!preMap.containsKey(person2)) { preMap.put(person2, true); }
                if (!maps.get(person1).isDirty()) { maps.get(person1).setDirty(); }
                dirty = true;
            }
            for (Person person : ((MyPerson) person1).getLink()) {
                if (person2.isLinked(person) && !person.equals(person2)) {
                    Triple triple = new Triple(person1, person2, person);
                    tripleMap.remove(triple);
                }
            }
        }
    } else {
        if (person1 == null) { throw new MyPersonIdNotFoundException(id1); }
        else if (person2 == null) { throw new MyPersonIdNotFoundException(id2); }
        else if (id1 == id2) { throw new MyEqualPersonIdException(id1); }
        else { throw new MyRelationNotFoundException(id1, id2); }
    }
}

这个函数可能会导致删边,因此这也是维护最复杂的函数,首先是修改长度时,可能导致queryValueSum的值发生改变,需要进行维护。而删边之后可能导致联通分支断开,也可能导致queryValueSum值发生改变,甚至离开Tag,以及三元环断开,因此要进行多处判断。关于删边导致联通分支断开,可能需要重新建图,复杂度较高,因此在此设置脏位,将所有的变化保存下来,直到查询时再重新建图。除此之外对于值的修改和三元环的修改可以当场处理。因此此处复杂度约为O(n)

public int queryValue(int id1, int id2) throws
        PersonIdNotFoundException, RelationNotFoundException {
    Person person1 = getPerson(id1);
    Person person2 = getPerson(id2);
    if (person1 != null && person2 != null && person1.isLinked(person2)) {
        return person1.queryValue(person2);
    } else if (person1 == null) { throw new MyPersonIdNotFoundException(id1); }
    else if (person2 == null) { throw new MyPersonIdNotFoundException(id2); }
    else { throw new MyRelationNotFoundException(id1, id2); }
}

求边长,直接从点找到另一个点即可,复杂度O(1)

public boolean isCircle(int id1, int id2) throws PersonIdNotFoundException {
    Person person1 = getPerson(id1);
    Person person2 = getPerson(id2);
    if (person1 != null && person2 != null) {
        if (dirty) {
            dirty = false;
            updateMap();
        }
        return maps.get(person1).contains(person2);
    } else if (person1 == null) { throw new MyPersonIdNotFoundException(id1); }
    else { throw new MyPersonIdNotFoundException(id2); }
}

查询是否联通,在有脏位情况下,需要重新建图,通过bfs方式进行,因此复杂度为O(n^2),而如果没有脏位,则可以直接查询,复杂度为O(1)

public int queryBlockSum() {
    if (dirty) {
        dirty = false;
        updateMap();
    }
    int sum = 0;
    HashSet<Map> temp = new HashSet<>();
    for (Map map : maps.values()) {
        if (!temp.contains(map)) {
            sum++;
            temp.add(map);
        }
    }
    return sum;
}

同理,如果脏位,则复杂度O(n^2),否则直接遍历点寻找不同的图,复杂度O(n)

public int queryTripleSum() { return tripleMap.size(); }

此处三元环受到维护,因此直接查询即可,复杂度O(1)

public void addTag(int personId, Tag tag) throws
        PersonIdNotFoundException, EqualTagIdException {
    if (containsPerson(personId)  && !getPerson(personId).containsTag(tag.getId())) {
        getPerson(personId).addTag(tag); }
    else if (!containsPerson(personId)) { throw new MyPersonIdNotFoundException(personId); }
    else if (getPerson(personId).containsTag(tag.getId())) {
        throw new MyEqualTagIdException(tag.getId()); }
}

添加子图,直接添加即可,复杂度O(1)

public void addPersonToTag(int personId1, int personId2, int tagId) throws
        PersonIdNotFoundException, RelationNotFoundException,
        TagIdNotFoundException, EqualPersonIdException {
    if (containsPerson(personId1) && containsPerson(personId2) &&
            personId1 != personId2 && getPerson(personId2).isLinked(getPerson(personId1)) &&
            getPerson(personId2).containsTag(tagId) &&
            !getPerson(personId2).getTag(tagId).hasPerson(getPerson(personId1)) &&
            getPerson(personId2).getTag(tagId).getSize() <= 1111) {
        getPerson(personId2).getTag(tagId).addPerson(getPerson(personId1));
    } else if (!containsPerson(personId1)) {
        throw new MyPersonIdNotFoundException(personId1);
    } else if (!containsPerson(personId2)) {
        throw new MyPersonIdNotFoundException(personId2);
    } else if (personId1 == personId2) {
        throw new MyEqualPersonIdException(personId1);
    } else if (!getPerson(personId2).isLinked(getPerson(personId1))) {
        throw new MyRelationNotFoundException(personId1, personId2);
    } else if (!getPerson(personId2).containsTag(tagId)) {
        throw new MyTagIdNotFoundException(tagId);
    } else if (getPerson(personId2).getTag(tagId).hasPerson(getPerson(personId1))) {
        throw new MyEqualPersonIdException(personId1);
    }
}

此处将点加入子图,在加入后,子图需要更新queryValueSum,因此复杂度为O(n)

public int queryTagValueSum(int personId, int tagId) throws
        PersonIdNotFoundException, TagIdNotFoundException {
    if (containsPerson(personId) && getPerson(personId).containsTag(tagId)) {
        return getPerson(personId).getTag(tagId).getValueSum();
    } else if (!containsPerson(personId)) { throw new MyPersonIdNotFoundException(personId); }
    else { throw new MyTagIdNotFoundException(tagId); }
}

queryValueSum受到维护,因此复杂度为O(1)

public void delPersonFromTag(int personId1, int personId2, int tagId) throws
        PersonIdNotFoundException, TagIdNotFoundException {
    if (containsPerson(personId1) && containsPerson(personId2) &&
            getPerson(personId2).containsTag(tagId) &&
            getPerson(personId2).getTag(tagId).hasPerson(getPerson(personId1))) {
        getPerson(personId2).getTag(tagId).delPerson(getPerson(personId1));
    } else if (!containsPerson(personId1)) {
        throw new MyPersonIdNotFoundException(personId1);
    } else if (!containsPerson(personId2)) {
        throw new MyPersonIdNotFoundException(personId2);
    } else if (!getPerson(personId2).containsTag(tagId)) {
        throw new MyTagIdNotFoundException(tagId); }
    else { throw new MyPersonIdNotFoundException(personId1); }
}

从子图删除点,需要更新queryValueSum,因此复杂度O(n)

public void delTag(int personId, int tagId) throws
        PersonIdNotFoundException, TagIdNotFoundException {
    if (containsPerson(personId) && getPerson(personId).containsTag(tagId)) {
        getPerson(personId).delTag(tagId);
    } else if (!containsPerson(personId)) {
        throw new MyPersonIdNotFoundException(personId);
    } else { throw new MyTagIdNotFoundException(tagId); }
}

删除子图,复杂度O(1)

public int queryShortestPath(int id1, int id2) throws
        PersonIdNotFoundException, PathNotFoundException {
    if (containsPerson(id1) && containsPerson(id2)) {
        int path = dijkstra(getPerson(id1), getPerson(id2));
        if (path >= 0) { return path; }
        else { throw new MyPathNotFoundException(id1, id2); } }
    else if (containsPerson(id1)) { throw new MyPersonIdNotFoundException(id2); }
    else { throw new MyPersonIdNotFoundException(id1); }
}

求无权值的最短路径,我这里选择的方式是dijkstra算法,复杂度约为O((V+E)logV)

总而言之,因为删除边的可能会导致重新建图,时间复杂度很难降到O(n^2)以下,所以其他算法复杂度在O(n^2)以下的不影响压力测试的结果。而主要消耗时间的在于MR函数的删边操作,所以仅对该操作进行脏位设置,并且通过图论全联通的(n-1)(n-2)/2判断其联通可能,减少脏位的设置情况,尽可能简化运算。

本次实验中,除了实时维护的操作以外,脏位也是优化时间复杂度的一个有效工具

由此可以看出,例如三元环函数,规格实现为三个for循环嵌套,这样的复杂度十分高,导致超时。而我们选择在运行过程中维护,复杂度最高的操作是全部为mr操作,假设共n个点m个数据,则复杂度一共为O(nm),而如果使用for循环嵌套,则复杂度最高为全部为查询函数qts,复杂度O(mn^3),差距显著,所以我们使用了其他的设计方式,而保证函数规格的功能正常实现,体现了规格与实现分离

体会

通过十数周的OO学习,我们已经逐渐理解层次化设计对于我们程序开发的帮助,但仍有缺点需要解决

  • 接口定义不明确
  • 协作成本高
  • 尚未标准化
  • 程序测试复杂

我们在第三单元学习了规格设计,这种方法使得我们能够将程序分工合同化,明确层次之间的分界线

规格主要作用是,描述软件架构,为开发团队提供了对所需构建的软件系统的清晰理解和一致的认识

无规格约束:

img

规格约束:

img

规格为软件架构划清了界限,规格通过对接口函数提出明确的功能要求,分配调用者和被调用者的权限要求,对层次化设计做出了功能和性能上的补全

  • 接口定义:

    •给出详细的函数功能要求,明确准备工作,减轻学习成本

    •不需要完全阅读函数实现方式,安全的按照规格要求调用该函数

  • 协作成本:

    •因为有了明确的责任分摊,无需顾虑对方的特殊需求,只满足规格要求即可。

    •程序出现问题时,可以分清问题责任

  • 标准化:

    •整体程序实现符合规格定义的标准,使得整体程序遵循一种模式

  • 程序测试:

    •有了明确的函数职责要求后,可以针对规格直接设计单一函数的单元测试

    •通过白箱测试的方法保证函数符合规格要求

规格语言为我们开发程序提供了极大地便利,无论在开发,拓展,协作或是测试等等方面,都得到一定的优化。

因此尽管在最初构造规格时可能需要一些功夫,但总的来说,磨刀不误砍柴工

...全文
42 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

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

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