2022年北航面向对象程序设计第三单元总结

2022年北航面向对象程序设计第三单元总结

本单元是一个快乐单元,作为OO的受难者,迄今第一次体会到”迭代开发“的感觉,并且JML的规格很详细,写起来非常快,为辛苦写JML的助教xgg们点赞~

作业内容概述

这一单元的工作任务其实在指导书中写的并不详细,可能算是单元特色吧,实际的工作任务全都写在了JML里,或许不写这个博客我都不知道这一单元写的是个什么东西\doge

其实是JML写太好了(手动狗头),任务工作在JML里面的表述要比从工作内容上讲更加易于写代码,所以这里就大致讲一下好了,下面会用JML举例

本单元的主要内容是建立一个社交网络体系,主要有Person类,代表社交网络的个体;Group类,代表一个社交团体;Network类,作为社交网络的主要承载类和操作类;Message类,表示社交个体中的成员发送消息

从整体形式上,依旧是一个操作的大模拟,这很OO~

关于JML

\[有一门代码语言规格用来指导代码的实现,写起来比实际的代码都长~ \]

我目前感受到的特点就是这样的,本单元官方包JML中的JML的书写带给人一种离散数学上的严格的规格定义下的规范,感觉非常地严谨,第一周的时候还没有开始做作业就进了上机实验,因为没看过JML,上机差点寄了,那会儿觉得JML好高大上,后来看了几条之后习惯了就觉得套路还是比较明显的,实现难度也不是很高

比如:

对于add等(以addMessage举例)

JML中是这样的

/*@ public normal_behavior
  @ requires !(\exists int i; 0 <= i && i < messages.length; messages[i].equals(message)) &&
  @           (message instanceof EmojiMessage) ==> containsEmojiId(((EmojiMessage) message).getEmojiId()) &&
  @            (message.getType() == 0) ==> (message.getPerson1() != message.getPerson2());
  @ assignable messages;
  @ ensures messages.length == \old(messages.length) + 1;
  @ ensures (\forall int i; 0 <= i && i < \old(messages.length);
  @          (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
  @ ensures (\exists int i; 0 <= i && i < messages.length; messages[i].equals(message));
  @ also
  @ public exceptional_behavior
  @ signals (EqualMessageIdException e) (\exists int i; 0 <= i && i < messages.length;
  @                                     messages[i].equals(message));
  @ signals (EmojiIdNotFoundException e) !(\exists int i; 0 <= i && i < messages.length;
  @                                       messages[i].equals(message)) &&
  @                                       (message instanceof EmojiMessage) &&
  @                                       !containsEmojiId(((EmojiMessage) message).getEmojiId());
  @ signals (EqualPersonIdException e) !(\exists int i; 0 <= i && i < messages.length;
  @                                     messages[i].equals(message)) &&
  @                                     ((message instanceof EmojiMessage) ==>
  @                                     containsEmojiId(((EmojiMessage) message).getEmojiId())) &&
  @                                     message.getType() == 0 && message.getPerson1() == message.getPerson2();
  @*/
public void addMessage(Message message) throws
        EqualMessageIdException, EmojiIdNotFoundException, EqualPersonIdException;

实现起来是这样的

@Override
public void addMessage(Message message) throws
        EqualMessageIdException, EmojiIdNotFoundException, EqualPersonIdException {
    if (messages.containsKey(message.getId())) {
        throw new MyEqualMessageIdException(message.getId());
    } else if (message instanceof EmojiMessage &&
            !emojiHeatList.containsKey(((EmojiMessage) message).getEmojiId())) {
        throw new MyEmojiIdNotFoundException(((EmojiMessage) message).getEmojiId());
    } else if (message.getType() == 0 && message.getPerson1().equals(message.getPerson2())) {
        throw new MyEqualPersonIdException(message.getPerson1().getId());
    }
    messages.put(message.getId(), message);
}

翻译以下这段JML就是:需要找的找不到或者已经有一个现在重复了就抛异常,其他情况正常进行,需要确保原来有的现在都要有,新的比原来的多了一个现在加入的

其实用一句话概括这段JML就是:需要找的找不到或者已经有一个现在重复了就抛异常,其他情况就把这个直接添加进所用的容器中

掌握了指导书中的JML的规格写起来真的飞快,毕竟写的比代码还要详细~

一些tips

  1. 在同学们互相讨论的时候也听过很多同学提到过新旧比对的过程(因为指导书都在不停地更新),所以判断当前的要求与已经做完的部分有无差异还是很有必要的,我第二周作业的时候是用的在线的文本比较工具在线文本差异对比,文本比对、文本比较工具 (jq22.com)(老CO人了),第三周的时候用的同学推荐的IDEA插件image-20220605222438331,个人感觉还是比较有意义的
  2. 对于JML的测试,听指导书和助教说的Junit和OpenJML测试,个人感觉效率上实在很难保证,我通过官方给的案例分析,感觉我们的项目的复杂度还远不及需要单一模块测试的程度,and 还有一个非常重要的点就是,直接通过JML实现出来的某些部分是无法完全满足测试要求的,这部分代码也非常不易于使用JML测试,而这部分却通常是测试的重点,因此我在本单元的测试精力并没有放在JML测试上
  3. JML阅读,在IDEA里面其实不太方便,特别是括号匹配的上的高亮,感觉在阅读过程中对提高效率很有帮助,IDEA里面查看的话除非找一些插件加载(或者像配置vim一样手写脚本?),但相比与直接复制到VS Code中去掉注释符,我还是感觉后者比较方便
  4. 刚开始做JML的时候可以多看看《JML Level 0手册》,摆个链接,需要可以自取(链接:https://pan.baidu.com/s/1Iw0CQPOqwJ_fCs2-XyRazA 提取码:6666 ,里面还有一些其他的资料,但最推荐先阅读的还是JML Level 0手册)

homework~

模型构建和维护策略

除了Person中的message为了更好地契合JML的要求,选用了LinkedList之外,其他一律选择了Map作为容器(毕竟直接getId实在是太方便了),为了更好地对输出进行toString方式的断点测试,我所有的Map都用的TreeMap,但实际应该还是HashMap更快一点,在这里还是推荐HashMap(我以前一直以为HashMap的寻找是O(n)的,最近才发现是O(1)的,之前只是想用一个稳定的方案,后面知道了也因为调试的关系不想改了)

各个版本的迭代和维护其实今年个人感觉课程组很良心,没有那种设坑型的诱导开发,感觉每周的工作基本都是独立的,比如第一周的并查集,只考虑添加和查询,并不需要删除,后两周也没有相关的删除操作,在实现上非常地舒服,只需要保证往期的bug都修复了就基本没什么问题了

算法

本单元的形式大概就是通过JML实现绝大多数的类方法的开发+1个图论基础算法的考察,总的来看还是非常轻松的,任务也很明确,第一单元是考察并查集,第二单元是考察最小生成树,第三单元是考察最短路,都是图论最基础的算法,整体的实现也非常地简单

  1. 并查集

    相比于裸的遍历查找,裸的并查集查找可以限制搜索区域仅在自己的祖先团体中,但这二者的复杂度仍停留在\(O(n)\),而并查集可以通过对合并优化或路径压缩变成复杂度小于\(O(log_2n)\)的算法,所以并查集的优化非常重要

    并查集的优化是可以二选其一的,相比于合并优化的冗长代码+一个height数组(或其他容器)的引入,路径压缩的实现会非常优雅(使用递归的方式甚至只比原非路径压缩多1行代码,避免爆栈使用非递归的方式也会比合并优化简单),所以这里就直接讲路径压缩了(这会儿找不到之前学的时候看的那个资料了,只记得这个结论)

    实现中主要的方法为下面两个,即合并和查询(这里的查询已经使用了路径压缩)

    private final TreeMap<Integer, Integer> disjointSet;
    
    private void unionSet(int id1, int id2) {
        int idn1 = findSet(id1);
        int idn2 = findSet(id2);
        if (idn1 != idn2) {
            disjointSet.put(idn1, disjointSet.get(idn2));
        }
    }
    
    private int findSet(int id) {
        if (id != disjointSet.get(id)) {
            disjointSet.put(id, findSet(disjointSet.get(id)));
        }
        return disjointSet.get(id);
    }
    

    除此之外,其他需要加入并查集逻辑的就是在新添加一个Person的时候需要在该并查集中加入其并查集的元素(key和value都设置为其ID)

    @Override
    public void addPerson(Person person) throws EqualPersonIdException {
        for (Integer id : people.keySet()) {
            if (people.get(id).equals(person)) {
                throw new MyEqualPersonIdException(id);
            }
        }
        people.put(person.getId(), person);
        disjointSet.put(person.getId(), person.getId()); // Here
    }
    

    然后在addRelation的时候进行合并操作即可

    @Override
    public void addRelation(int id1, int id2, int value)
        throws PersonIdNotFoundException, EqualRelationException {
        if (!people.containsKey(id1)) {
            throw new MyPersonIdNotFoundException(id1);
        } else if (!people.containsKey(id2)) {
            throw new MyPersonIdNotFoundException(id2);
        } else if (people.get(id1).isLinked(people.get(id2))) {
            throw new MyEqualRelationException(id1, id2);
        }
        ((MyPerson) people.get(id1)).addRelation(id2, people.get(id2), value);
        ((MyPerson) people.get(id2)).addRelation(id1, people.get(id1), value);
        for (Integer id : groups.keySet()) {
            ((MyGroup) groups.get(id)).addRelation(id1, id2, value);
        }
        unionSet(id1, id2); // Here
    }
    

    接着再补充一下路径压缩的方法,先放上三个查询的优化的代码

    // 无优化版查询
    private int findSet(int id) {
        return id == disjointSet.get(id) ? id : findSet(disjointSet.get(id));
    }
    
    // 递归版路径压缩
    private int findSet(int id) {
        if (id != disjointSet.get(id)) {
            disjointSet.put(id, findSet(disjointSet.get(id)));
        }
        return disjointSet.get(id);
    }
    
    //非递归版路径压缩
    private int findSet(int id) {
        int r = id;
        while (disjointSet.get(r) != r) {
            r = disjointSet.get(r);
        }
        int i = id, j;
        while (i != r) {
            j = disjointSet.get(i);
            disjointSet.put(i, r);
            i = j;
        }
        return r;
    }
    

    因为本单元测试中的公测和互测的数据量都不足以爆栈,我更推荐递归版的路径压缩算法

    (我在写这篇博客的时候听同学说可以合并秩优化+路径压缩,但我改了TreeMap之后没用合并秩也比他快,感兴趣的同学可以在写这里的时候做一下两种写法的测试,合并秩+路径压缩可以参考我的hxd的blogOO 第三单元总结 - cywuuuu - 博客园 (cnblogs.com)

  2. 最小生成树

    经过了大量的数据测试,本单元的数据强度对于person量的依赖度非常高,以互测的5000上限为例,至少需要600人才能达到有效的测试Hack(实际生成的时候,最适的个人预估在1200个Person上下),而在这种的人数限制下,5000条数据对于图的构建,必然是一个稀疏图,因此Kruskal算法肯定是要优于Prim的(两个实现起来都不是很难,有兴趣的同学可以做一下测试)

    首先摆一下我的qlc的处理算法

    private TreeMap<Integer, Integer> kruskal;
    
    private int findAdd(int id) {
        if (id != kruskal.get(id)) {
            kruskal.put(id, findAdd(kruskal.get(id)));
        }
        return kruskal.get(id);
    }
    
    @Override
    public int queryLeastConnection(int id) throws PersonIdNotFoundException {
        if (!people.containsKey(id)) {
            throw new MyPersonIdNotFoundException(id);
        }
        ArrayList<Edge> edges = new ArrayList<>();
        TreeMap<Integer, TreeSet<Integer>> hasAdd = new TreeMap<>();
        kruskal = new TreeMap<>();
        kruskal.put(id, id);
        for (Integer personId : people.keySet()) {
            if (findSet(personId) == findSet(id) && personId != id) {
                TreeMap<Integer, Person> acquaintance =
                    ((MyPerson) people.get(personId)).getAcquaintance();
                TreeMap<Integer, Integer> value = ((MyPerson) people.get(personId)).getValue();
                for (Integer acquaintanceId : acquaintance.keySet()) {
                    if ((hasAdd.get(personId) != null &&
                         hasAdd.get(personId).contains(acquaintanceId)) ||
                        hasAdd.get(acquaintanceId) != null &&
                        hasAdd.get(acquaintanceId).contains(personId)) {
                        continue;
                    }
                    if (hasAdd.get(personId) == null) {
                        TreeSet<Integer> hasAddAcquaintance = new TreeSet<>();
                        hasAddAcquaintance.add(acquaintanceId);
                        hasAdd.put(personId, hasAddAcquaintance);
                    } else {
                        hasAdd.get(personId).add(acquaintanceId);
                    }
                    Edge edge = new Edge(personId, acquaintanceId,
                                         value.get(acquaintanceId), false);
                    edges.add(edge);
                }
                kruskal.put(personId, personId);
            }
        }
        Collections.sort(edges); // Attention!
        // kruskal
        int ans = 0;
        for (int i = 0; i < edges.size(); i++) {
            int b = findAdd(edges.get(i).getStart());
            int c = findAdd(edges.get(i).getEnd());
            if (b == c) {
                continue;
            }
            kruskal.put(c, kruskal.get(b));
            ans += edges.get(i).getWeight();
        }
        return ans;
    }
    

    首先还是需要一个Map当作并查集使用,操作方式跟第一周的相同,这里直接把合并操作写在了代码中

    可以看到kruskal的核心代码非常少,主要的操作都是在处理并查集(qlc的集合的建立),实际的实现只需要从小到大加入边,不成环的就进行加入即可(实际还可以加一个变量进行计数,到n-1时即可break,但感觉这样的剪枝优化有限,主要是没那么优雅,而且也不会导致超时,遂没写

    这里提一个可能被忽略的点:即代码中标出的Collections.sort(edges)

    使用Kruskal算法需要生成一个用于表达边的类,先摆上自己的代码来讲(因为不算太长,就全摆了,强迫症狂喜

    package run;
    
    public class Edge implements Comparable<Edge> {
        private final int start;
        private final int end;
        private final int weight;
    
        public Edge(int start, int end, int weight, boolean flag) {
            this.start = flag ? start : Math.min(start, end);
            this.end = flag ? end : Math.max(start, end);
            this.weight = weight;
        }
    
        @Override
        public int compareTo(Edge edge) {
            if (this.weight != edge.getWeight()) {
                return this.weight < edge.getWeight() ? -1 : 1;
            } else if (this.start != edge.getStart()) {
                return this.start < edge.getStart() ? -1 : 1;
            } else {
                return this.end < edge.getEnd() ? -1 : 1;
            }
        }
    
        @Override
        public boolean equals(Object o) {
            if (o == null || !(o instanceof Edge)) {
                return false;
            }
            return (this.start == ((Edge) o).getStart()) && (this.end == ((Edge) o).getEnd());
        }
    
        public int getStart() {
            return start;
        }
    
        public int getEnd() {
            return end;
        }
    
        public int getWeight() {
            return weight;
        }
    }
    

    这里主要是compareTo这个点需要注意

    /* 错误写法 */
    @Override
    public int compareTo(Edge edge) {
        return this.weight < edge.getWeight() ? -1 : 1;
    }
    
    /* 正确写法 */
    @Override
    public int compareTo(Edge edge) {
        if (this.weight != edge.getWeight()) {
            return this.weight < edge.getWeight() ? -1 : 1;
        } else if (this.start != edge.getStart()) {
            return this.start < edge.getStart() ? -1 : 1;
        } else {
            return this.end < edge.getEnd() ? -1 : 1;
        }
    }
    

    在C/C++中,使用STL或qsort的时候写cmp的函数经常会使用类似上面的第一种写法,但是在java中这种写法会导致Collections.sort()中出现监视器异常的错误。这两种写法的区别是在weight相等的时候,如果集合中有两个相同的元素,第一个的执行的结果判断仅与当前取出的有关,比如有Object1和Object2他们的weight相同,如果取出的是Object1那么Object1就会排在Object2后面,如果取出的是Object2那么Object1就会排在Object2前面,而如果一次排序中出现了两次或以上Object1和Object2的比较,且二者都有取出过,那么java就会认为未定义这种情况的排序规则,就会抛出相应的异常,因此使用Collections.sort()的时候一定需要保证两个对象在比较的过程中的大小关系是固定的(第二种写法是因为start、end和weight不会完全相同,所以可以用这种写法分开)

  3. 最短路

    最短路算法我是直接莽了一个无优化的dijkstra,强测CTLE了一个点,悲,懒是万恶之源,这里就给大家做错误示范了

    (我自测的时候使用随机数据几乎不可能Hack到这个点,后面我会讲这个点的Hack数据)

    private int dijkstra(Message message) {
        int start = message.getPerson1().getId();
        dis.put(start, 0);
        PriorityQueue<Node> queue = new PriorityQueue<>();
        queue.add(new Node(start, dis.get(start)));
        while (!queue.isEmpty()) {
            Node u = queue.poll();
            if (done.get(u.getId())) {
                continue;
            }
            done.put(u.getId(), true);
            ArrayList<Edge> neighboor = nodeEdges.get(u.getId());
            for (Edge y : neighboor) {
                if (done.get(y.getEnd())) {
                    continue;
                }
                if (dis.get(y.getEnd()) > y.getWeight() + u.getDis()) {
                    dis.put(y.getEnd(), y.getWeight() + u.getDis());
                    queue.add(new Node(y.getEnd(), dis.get(y.getEnd())));
                    preNode.put(y.getEnd(), u.getId());
                }
            }
        }
        return dis.get(message.getPerson2().getId());
    }
    

    (正确写法推荐参考下ywxgg的blog,OO 第三单元总结 - cywuuuu - 博客园 (cnblogs.com)

测试思路与互测策略

本单元的JML限制下,特别是add与查询在指令里面是共享总数据量的限制的,因此全部指令直接随机生成的数据强度非常有限,只能做一些基础的指令测试

自从厌倦与追寻,我已学会一觅即中,自从一股逆风袭来,我已能御八面来风驾舟而行,自从前两个单元都借了ygg和ywgg的数据生成器,我已学会了在测试前不经思考要不要自己写数据生成器而是直接去问ygg和ywgg有没有写数据生成器

我的作用就是提一点数据生成建议和问题反馈(

但二位又是同以前一样,都没有写自己的数据生成思路,不过还是摆摆blog吧,苟富贵,勿相忘,\dogeOO_Unit3_JML - 青衫染墨 - 博客园 (cnblogs.com), OO 第三单元总结 - cywuuuu - 博客园 (cnblogs.com))

下面讲下我的数据生成测试吧,或许我的作用又多了一个

框架呢,当然是直接用二位xgg的python函数,对于三周公共数据构造方向,首先是add与查询与算法测试分离,先保证已有一定的数据量之后再进行查询,并做查询与add的混合测试,然后再全部的add构建完后进行算法测试(因为互测的数据限制,重建图或者重搜索添加的优化没做的也Hack不到,因此没必要做add与算法的混合测试),所以总的数据情况大致生成为3000-4500条add,然后加入各查询进行测试,此外对于组内测试,仅生成一个组更利于组内的功能复杂度测试,对于异常测试,不通过上述规律,直接进行可控异常数据占比的随机数据的分布测试效果更优

对于每周的互测情况

我第一周的房间里全部都没bug,评测姬忙乎了一整天一个也没测出来,最终就是和平房,整个房间都没人Hack成功过

第二周我出了个非常沙雕的错误

/* bug */
@Override
public void print() {
    System.out.printf("emi-%d, %d-%d", count, id, EXCEPTIONS.get(id));
}

/* 正确写法 */
@Override
public void print() {
    System.out.printf("emi-%d, %d-%d\n", count, id, EXCEPTIONS.get(id));
}

于是第一次进了C房,and 给大家讲讲C房的状况奥,首先只有6个人,然后只有6个人,最后只有6个人

所以不要相信中测。。。我这学期只有这一周没有搭评测姬,直接用的ywgg的评测姬,他使用的数据比对的方式没有测\n的问题,哭哭o(╥﹏╥)o

C房真的就是随便交大一点的数据就能乱杀。。。但我实在不好意思接着Hack下去了,只交了5次,中了20个就run了,最后也是改了俩字符就bugfree了(

这里讲个手捏送给同学的测试案例吧(这个数据是受杰哥提示的\doge)

首先就是Group有的一个1111的限制,这个1111的限制中测和强测都没测到,因此ABC通杀

然后就是JML的qgvs指令,如果按照JML来写,就是两次循环求和,如果针对性的构造卡住Group的数据,并使用1000+的qgvs指令就可以直接Hack到TLE,这一个也是中测强测都没测到,依旧ABC通杀

因此可以混合两个测试点构造如下

ap 1 1 152
ap 2 2 21
ap 3 3 68
ap 4 4 160
ap 5 5 0
ap 6 6 12
ap 7 7 86
ap 8 8 5
''' 共1112个
ag 1
atg 1 1
atg 2 1
atg 3 1
atg 4 1
atg 5 1
atg 6 1
atg 7 1
''' 共1112个
qgvs 1
qgvs 1
qgvs 1
qgvs 1
''' 然后qgvs到数据上限

需要注意的是需要卡一下ap 1112的qgvs要与 ap 前1111的不同,不然中不了1111的bug

也可以直接一手2400人塞进去,然后直接qgvs来Hack 1111的bug

第三周是随机数据杀了一波(以及1111的bug我又中了一个人),然后又Hack了一下sim的超时bug

随机数据如上面的数据的公共特点中所述,1111如2中的数据

sim的数据构造如下

''' 创建1111个人
ag 1
''' 把1111个人加入group1中
ar 1 138 536
ar 1 595 852
ar 1 632 678
ar 1 634 197
ar 1 836 797
ar 1 950 656
ar 2 718 120
ar 2 769 25
''' 然后随机addRelation,排序输出
am 1 -869 0 2 718
am 2 528 0 4 29
am 3 -171 0 4 930
am 4 845 0 4 1075
am 5 1000 0 6 284
am 6 777 0 7 837
am 7 -180 0 7 1089
am 8 -101 0 8 156
am 9 612 0 9 671
am 10 -335 0 10 208
''' 随机add 500条message
sim 1
sim 2
sim 3
sim 4
sim 5
sim 6
sim 7
sim 8
sim 9
sim 10
''' 对500条message分别sim

这个数据我中了4个人,估计都是跟我一样ddl前夜懒得优化的(

Network JML拓展

拓展要求

假设出现了几种不同的Person

  • Advertiser:持续向外发送产品广告
  • Producer:产品生产商,通过Advertiser来销售产品
  • Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
  • Person:吃瓜群众,不发广告,不买东西,不卖东西

如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)

我的设计

首先约定一下需要新加的属性

对于广告发布过程,需要引入一个新的Message子类,暂且命名为Advertisement

对于购买过程,同样需要引入一个新的Message子类,暂且命名为Purchase

由于要实现Customer通过货物的喜好程度进行购买,Producer通过传递来的购买信息确认货物信息进行增加收入

所以首先需要添加一个货物类,加入一个货物ID(或名称)、货物吸引力和货物价格,并加入一个货物来源

已存在类添加/修改:

Person类需要添加

  1. 四种类型的人的标识(约定每个人只能扮演一种角色,可以用继承来实现)
  2. 货物列表(仅Producer类使用并负责初始化)和其当前的销售额
  3. 添加货物的方法
  4. 筛选最喜欢的货物决定是否发送购买信息的方法
  5. 帮助发送购买信息的方法
  6. 将自己的某个货物请求广告商做宣传的方法
  7. 向与自己联系的所有顾客发布货物广告的方法(假定给与自己联系的所有人都发,这样比较符合广告商的身份)

Network里需要添加或修改

  1. 添加货物ID与销售情况的Map关系(下面新建了一个Sales类和Trace类,用来记录销售情况和销售路径)
  2. 添加货物的方法
  3. 添加令某一个顾客开始挑选是否有喜欢的货物的方法,如果有则生成一个Purchase的Message
  4. 添加令生产商决定push某个货物去做宣传的方法,并生成一个Advertisement的Message
  5. 修改sendMessage方法,添加Purchase和Advertisement的发送的执行

新引入:

Product类需要包含

  1. 货物ID(或名称)
  2. 货物价格
  3. 货物吸引力
  4. 货物来源(生产商ID)
  5. 各属性的get方法

Purchase类需要包含

  1. 购买的货物ID

Advertisement类需要包含

  1. 货物

Sales类需要包含

  1. 销售额
  2. Trace的列表

Trace类需要包含

  1. Producer
  2. Advertiser
  3. Customer
  4. isSold (标记该货物是否以及被售卖)

整个流程可以大致描述如图

image-20220606014837423

感觉这里面最核心的几个分别是:

  1. 添加令某一个顾客开始挑选是否有喜欢的货物的方法,如果有则生成一个Purchase的Message
  2. 添加令生产商决定push某个货物去做宣传的方法,并生成一个Advertisement的Message
  3. 修改sendMessage方法,添加Purchase和Advertisement的发送的执行

所以这里就以这三个为例书写在我的约定下的JML

首先对于让顾客开始挑选喜欢的货物:

方法定义:boolean pickTheGoods(int personId, int limit)

思路为:输入personId,和确定为喜欢的货物的下限,在该person的receiveMessage(至多前三个)中搜索Advertise的货物的吸引力有没有超过limit的,如果有则取吸引力最高的,确定取的情况下最高吸引力若相同则取id小的,以此生成购买信息

对应的JML如下:

	/*@ public normal_behavior
      @ requires contains(personId)
      @ assignable messages, productSales;
      @ ensures contains(personId) && (\exist int i; 0 <= i && i < people.get(personId).getReceivedMessages.length && 
      @          (people.get(personId).getReceivedMessages[i] instanceof Advertisement && 
      @          ((Advertisement)people.get(personId).getReceivedMessages[i]).getProduct().getAttractive() >= limit) ==> 
      @            messages.length == \old(messages.length) + 1 && (\forall int i; 0 <= i && i < \old(messages.length); 
      @            !(\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))) ==> 
      @              messages[i] instanceof PurchaseMessage && ((PurchaseMessage)messages[i]).getProductId == people.get(personId).getMaxAttributeId()) && 
      @            product.length == \old(product.length) + 1 && (\forall int i; 0 <= i && i < \old(product.length); 
      @            (\exists int j; 0 <= j && j < product.length; product[j].equals(\old(product[i])))) && products.get(((Purchase)messages[i]).getProductId()) != null && 
      @            productSales.getProductSale(people.get(personId).getMaxAttributeId()).getProducer() == products.get(((Purchase)messages[i]).getProductId()).getProducer() && 
      @            (\exist int i; 0 <= i && i < people.get(personId).getAcquaintance().length; people.get(personId).getAcquaintance()[i].isLinked(people.get(personId)) && 
      @            people.get(personId).getAcquaintance()[i].isLinked(people.get(productSales.getProductSale(people.get(personId).getMaxAttributeId()).getProducer().getId())) ==> 
      @            productSales.getProductSale(people.get(personId).getMaxAttributeId()).getAdvertiser() == people.get(personId).getAcquaintance()[i]) && 
      @          productSales.getProductSale(people.get(personId).getMaxAttributeId()).getCustomer() == people.get(personId));
      @ ensures contains(personId) && !(\exist int i; 0 <= i && i < people.get(personId).getReceivedMessages.length && (people.get(personId).getReceivedMessages[i] instanceof Advertisement && 
      @          ((Advertisement)people.get(personId).getReceivedMessages[i]).getProduct().getAttractive() >= limit) ==> (\not_assigned(messages[*]) && \not_assigned(productSales[*]));
      @ also
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) !contains(personId);
      @*/
	boolean pickTheGoods(int personId, int limit) throws PersonIdNotFoundException;

对于生产商push货物做宣传:

方法定义:void advertiseGoods(int personId, int productId)

思路为:输入personId,在该Advertiser下对其相连的People进行遍历,对每个Customer以输入的productId为依据生成一个Advertisement

对应的JML如下:

	/*@ public normal_behavior
      @ requires contains(personId) && people.get(personId) instanceof Advertiser && containProducts(productsId) && (Advertiser)people.get(personId).isLinked(products.get(productId).getProducer())
      @ assignable messages;
      @ ensures messages.length == \old(messages.length) + (\count int i; 0 <= i && i < people.get(personId).getAcquaitance().length; people.get(personId).getAcquaitance()[i] instanceof Customer);
      @ ensures (\forall int i; 0 <= i && i < \old(messages.length); (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
      @ ensures (\forall int i; 0 <= i && i < messages.length; !(\exists int j; 0 <= j && j < \old(messages).length; \old(messages[j]).equals(messages[i])) ==> messages[i] instance of Advertisement && 
      @         ((Advertisement)messages[i]).getPerson1 == people.get(personId) && (\exist int i; 0 <= i && i < people.get(personId).getAcquaitance().length; 
      @           people.get(personId).getAcquaitance()[i] instanceof Customer && ((Advertisement)messages[i]).getPerson2 == people.get(personId).getAcquaitance()[i]) && 
      @         ((Advertisement)messages[i]).getProduct() == products.get(productId));
      @ also
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) !contains(personId);
      @ signals (PersonTypeNotCorrespondingException e) contains(personId) && !(people.get(personId) instanceof Advertiser);
      @ signals (ProductIdNotFoundException e) contains(personId) && people.get(personId) instanceof Advertiser && !containProducts(productsId);
      @ signals (AdvertiserNotLinkedException e) contains(personId) && people.get(personId) instanceof Advertiser && containProducts(productsId) && 
      @                                          !(Advertiser)people.get(personId).isLinked(products.get(productId).getProducer())
      @*/
	void advertiseGoods(int personId, int productId) throws PersonIdNotFoundException, PersonTypeNotCorrespondingException, ProductIdNotFoundException, AdvertiserNotLinkedException;

对于sendMessage引入Purchase和Advertisement

思路为:

对Purchase信息:需要修改其中的货物销售额,和isSold信息,并使生产商获得相当于该货物销售额的money,使消费者减少相当于该货物销售额的money

对Advertisement信息:无特殊处理

对应的JML如下:

/*@ public normal_behavior
      @ requires containsMessage(id) && getMessage(id).getType() == 0 &&
      @          getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
      @          getMessage(id).getPerson1() != getMessage(id).getPerson2();
      @ assignable messages, emojiHeatList;
      @ assignable getMessage(id).getPerson1().socialValue, getMessage(id).getPerson1().money;
      @ assignable getMessage(id).getPerson2().messages, getMessage(id).getPerson2().socialValue, getMessage(id).getPerson2().money;
      @ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 &&
      @         (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id;
      @         (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
      @ ensures \old(getMessage(id)).getPerson1().getSocialValue() ==
      @         \old(getMessage(id).getPerson1().getSocialValue()) + \old(getMessage(id)).getSocialValue() &&
      @         \old(getMessage(id)).getPerson2().getSocialValue() ==
      @         \old(getMessage(id).getPerson2().getSocialValue()) + \old(getMessage(id)).getSocialValue();
      @ ensures (\old(getMessage(id)) instanceof RedEnvelopeMessage) ==>
      @         (\old(getMessage(id)).getPerson1().getMoney() ==
      @         \old(getMessage(id).getPerson1().getMoney()) - ((RedEnvelopeMessage)\old(getMessage(id))).getMoney() &&
      @         \old(getMessage(id)).getPerson2().getMoney() ==
      @         \old(getMessage(id).getPerson2().getMoney()) + ((RedEnvelopeMessage)\old(getMessage(id))).getMoney());
      @ ensures (\old(getMessage(id)) instanceof PurchaseMessage) ==>
      @         (\old(getMessage(id)).getPerson1().getMoney() ==
      @         \old(getMessage(id).getPerson1().getMoney()) - ((PurchaseMessage)\old(getMessage(id))).getProduct().getPrice() &&
      @         \old(getMessage(id)).getPerson2().getMoney() ==
      @         \old(getMessage(id).getPerson2().getMoney()) + ((PurchaseMessage)\old(getMessage(id))).getProduct().getPrice());
      @ ensures (!(\old(getMessage(id)) instanceof RedEnvelopeMessage) && !(\old(getMessage(id)) instanceof Purchase)) ==> (\not_assigned(people[*].money));
      @ ensures (\old(getMessage(id)) instanceof EmojiMessage) ==>
      @         (\exists int i; 0 <= i && i < emojiIdList.length && emojiIdList[i] == ((EmojiMessage)\old(getMessage(id))).getEmojiId();
      @         emojiHeatList[i] == \old(emojiHeatList[i]) + 1);
      @ ensures (!(\old(getMessage(id)) instanceof EmojiMessage)) ==> \not_assigned(emojiHeatList);
      @ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().getMessages().size());
      @          \old(getMessage(id)).getPerson2().getMessages().get(i+1) == \old(getMessage(id).getPerson2().getMessages().get(i)));
      @ ensures \old(getMessage(id)).getPerson2().getMessages().get(0).equals(\old(getMessage(id)));
      @ ensures \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1;
      @ also
      @ public normal_behavior
      @ requires containsMessage(id) && getMessage(id).getType() == 1 &&
      @           getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1());
      @ assignable people[*].socialValue, people[*].money, messages, emojiHeatList;
      @ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 &&
      @         (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id;
      @         (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
      @ ensures (\forall Person p; \old(getMessage(id)).getGroup().hasPerson(p); p.getSocialValue() ==
      @         \old(p.getSocialValue()) + \old(getMessage(id)).getSocialValue());
      @ ensures (\forall int i; 0 <= i && i < people.length && !\old(getMessage(id)).getGroup().hasPerson(people[i]);
      @          \old(people[i].getSocialValue()) == people[i].getSocialValue());
      @ ensures (\old(getMessage(id)) instanceof RedEnvelopeMessage) ==>
      @          (\exists int i; i == ((RedEnvelopeMessage)\old(getMessage(id))).getMoney()/\old(getMessage(id)).getGroup().getSize();
      @           \old(getMessage(id)).getPerson1().getMoney() ==
      @           \old(getMessage(id).getPerson1().getMoney()) - i*(\old(getMessage(id)).getGroup().getSize() - 1) &&
      @           (\forall Person p; \old(getMessage(id)).getGroup().hasPerson(p) && p != \old(getMessage(id)).getPerson1();
      @           p.getMoney() == \old(p.getMoney()) + i));
      @ ensures (\old(getMessage(id)) instanceof RedEnvelopeMessage) ==>
      @          (\forall int i; 0 <= i && i < people.length && !\old(getMessage(id)).getGroup().hasPerson(people[i]);
      @           \old(people[i].getMoney()) == people[i].getMoney());
      @ ensures (!(\old(getMessage(id)) instanceof RedEnvelopeMessage)) ==> (\not_assigned(people[*].money));
      @ ensures (\old(getMessage(id)) instanceof EmojiMessage) ==>
      @         (\exists int i; 0 <= i && i < emojiIdList.length && emojiIdList[i] == ((EmojiMessage)\old(getMessage(id))).getEmojiId();
      @          emojiHeatList[i] == \old(emojiHeatList[i]) + 1);
      @ ensures (!(\old(getMessage(id)) instanceof EmojiMessage)) ==> \not_assigned(emojiHeatList);
      @ also
      @ public exceptional_behavior
      @ signals (MessageIdNotFoundException e) !containsMessage(id);
      @ signals (RelationNotFoundException e) containsMessage(id) && getMessage(id).getType() == 0 &&
      @          !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()));
      @ signals (PersonIdNotFoundException e) containsMessage(id) && getMessage(id).getType() == 1 &&
      @          !(getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()));
      @*/
    public void sendMessage(int id) throws
            RelationNotFoundException, MessageIdNotFoundException, PersonIdNotFoundException;

(new一个id的问题假定可以通过指令限制下按序构造来保证ID的稳定)

心得体会

本单元感觉OO的压力一下子就小了,感谢课程组的贴心关怀,以及“迭代开发”的体验真不错,yysy,在期末OS压力起来的时候,莫名其妙每周多出了10多个小时的时间,那么一种关爱感油然而生

posted @ 2022-06-06 13:35  Oh_so_many_sheep  阅读(179)  评论(0编辑  收藏  举报