第三单元:规格化设计

前言

第三单元的需要我们读懂JML规格语言,之后基于其给出的规格进行代码实现。经历过三次迭代之后,我的体会是完成作业实现输出正确并不困难(其实根据规格直接编程就能够实现)。但是规格对于我们来说只是一种契约、一种限制,如果我们完全“翻译”规格,会在性能上吃亏(TLE)。所以我们要在编程时注意性能和效率。

很多同学都对第三单元的开设目的不太理解,感觉JML晦涩难懂而且除了oo课程好像其他地方都没有用处。但是在三次作业后,我也发现了“契约式编程”的魅力——高可靠性、高可复用性、便于测试。

第一次作业分析

第一次作业比较简单,通过课程组提供的Network,Person接口,实现一个社交关系网络的维护。

UML类图

图模型构建

从JML规格中我们可以看出有许多查询操作,所以在图构建的容器选择上我选择了HashMap,将id作为key值,这样可以保证查询的时候时间复杂度为O(1)。

维护策略

这次作业中难度比较大的是queryBlockSum()方法,我们需要维护Network类的blockSum的值,也就是关系图中连通子图的个数。这里我使用并查集进行优化,并查集的算法在这里就不多赘述。顾名思义,并查集对于合并和查询十分友好,也即对应到我们需要实现的加边addRelation()和查询是否连通isCircle()操作。但是对于删边操作,并查集就有些吃力。我在这里使用的是局部重建并查集的方法。

将id1和id2之间的边删除之后,通过dfs找出所有仍与id1连通的节点,在此期间局部重建并查集。之后看id2的root节点是不是id1,如果是,则id1与id2仍连通,否则不连通blockSum需要加一。

public int deleteRelation(int id1, int id2) {
        int blockSumChange = 0;
        HashSet<Integer> set = map.get(id1);
        set.remove(id2);
        map.put(id1, set);
        set = map.get(id2);
        set.remove(id1);
        map.put(id2, set);
        pre.put(id2, id2);
        visitSet = new HashSet<>();
        dfs(id1, id1);
        if (pre.get(id2) != id1) {
            blockSumChange = 1;
        }
        visitSet = new HashSet<>();
        dfs(id2, id2);
        return blockSumChange;
    }
​
public void dfs(int id1, int root) {
        visitSet.add(id1);
        pre.put(id1, root);
        for (int i : map.get(id1)) {
            if (!visitSet.contains(i)) {
                dfs(i, root);
            }
        }
    }

对于tripleSum的维护比较简单,在加边和删边的时候,我们只需要寻找与改动的边的两个端点都直接相连的节点,找到一个tripleSum的值也就改变1。

第二次作业分析

第二次作业新增了查询最短路径,将直接相连的好友进行排序,将person加入tag(标签)进行管理等要求。这次作业的难点我感觉主要是在理解tag标签的含义,理解了之后我们就知道应该在哪些操作中需要维护tag的属性,如何去维护tag的属性。

UML类图

维护策略

对于queryBestAcquaintance(),我们需要返回一个person直接相连的边value值最大的person,如果有相等的情况返回id值最小的,这里我使用了TreeMap容器嵌套TreeSet容器进行排序,利用java自带的comparator实现比较,算法复杂度为nlog(n)。

对于queryTagValueSum(),我们需要对tag类的valueSum属性进行维护,这里我们就需要确定哪些方法会改动valueSum的值。首先是addRelation和modifyRelation通过对边的修改改变valueSum。还有就是addPersonToTag和delPersonFromTag对tag中的人进行添加和删除操作,加人或删人意味着会改变tag中包含的边,所以这个地方我们就需要遍历这个人的所有边来维护valueSum。

对于queryTagAgeVar,我们需要维护tag类中的两个属性,ageSum年龄和、ageSquareSum年龄平方和。其中需要注意的是方差的计算方法(涉及到除法取整),我们只需要确保结果符合规格即可。下面给出我的实现方法,即运用方差计算公式,之后将表达式展开。

public int getAgeVar() {
        if (persons.isEmpty()) {
            return 0;
        }
        long sum = 0;
        sum += ageSquareSum;
        sum += (long) persons.size() * (long) getAgeMean() * (long) getAgeMean();
        sum -= 2 * ageSum * getAgeMean();
        return (int) (sum / persons.size());
    }

实现策略

对于getShortestPath()方法,我使用了bfs算法寻找最短路径,bfs的具体算法在这里就不赘述了。

第三次作业分析

第三次作业新增了Message类,需要我们根据规格实现发送信息、新增信息、删除信息等操作。其中还有继承自Message类的EmojiMessage类、RedEnvelopeMessage类、NoticeMessage类,这些信息的属性与Message稍有不同,我们只需要按照规格设计即可。

UML类图

维护策略

其中person类的message还需要维护其加入的顺序,所以这里我使用了linkedlist容器,可以在首尾加入元素,同时保证顺序。

还要注意的是相同的emojiId可以对应不同的messageId,messageId是独一无二的,这里我第一次就写错了,删除低热度emojiId的时候就抛出了nullpointerexception的错误。这里我的解决是使用了一个HashMap嵌套HashSet的容器来存储每个emojiId对应的messageId,同时去维护这个容器。

测试过程与Junit测试

黑箱测试

黑箱测试(Black-box Testing)是一种测试方法,在这种方法中,测试人员不需要了解被测试软件的内部实现或代码结构。测试的重点是通过输入和输出来验证软件的功能和性能是否符合规格说明和用户需求。

我的理解是,黑箱测试就是我们不知道这个代码是怎么写出来的,我们去通过大量随机地生成数据来看我们写的代码是不是符合规格,这个单元中也就是检验结果是不是符合JML规格中的描述。就像是课程组使用强测数据去测试我们的代码,对我们的代码一无所知,只是为了检验正确性和运行时间等。

黑箱测试通常被我使用来进行debug。。

白箱测试

白箱测试(White-box Testing),又称结构化测试或透明盒测试,是一种测试方法,测试人员需要了解被测试软件的内部结构和代码实现,通过对代码的逻辑路径、条件和循环等进行验证。

白箱测试就是我们这个单元新增的junit测试,我们在写完自己的代码后需要写的test方法就是一种白箱测试。我们知道自己代码的运行策略和逻辑,为了检验结果正确。在白箱测试中,我们可以根据代码来考虑测试的覆盖率,使测试更加全面,比如说我们的junit中可以构造一些极限情况、异常情况等。

总之,通过黑箱测试可以验证软件功能是否满足用户需求,通过白箱测试可以确保代码质量和逻辑正确性。

junit测试

  • 第一次作业

方法 generatePerson 和 generateNetwork 分别用来随机生成MyPerson和MyNetwork对象

public static MyPerson generatePerson() {
        //id,name,age
        Random random = new Random();
        String idStr = "";
        for (int i = 0; i < 6; i++) {
            idStr += random.nextInt(10);
        }
        int id = Integer.parseInt(idStr);
        String name = "";
        for (int i = 0; i < 5; i++) {
            name += random.nextInt(10);
        }
        name += "jc";
        int age = random.nextInt(30) + 1;
        MyPerson person = new MyPerson(id, name, age);
        return person;
    }
public static MyNetwork generateNetwork() throws EqualPersonIdException, PersonIdNotFoundException, EqualRelationException {
        MyNetwork network = new MyNetwork();
        ArrayList<Person> persons = new ArrayList<>();
        ArrayList<Integer> ids = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
            MyPerson newPerson = generatePerson();
            while (ids.contains(newPerson.getId())) {
                newPerson = generatePerson();
            }
            ids.add(newPerson.getId());
            persons.add(newPerson);
        }
        for (Person person : persons) {
            network.addPerson(person);
        }
        Random random = new Random();
        for (int i = 0; i < 300; i++) {
            int id1 = random.nextInt(30);
            int id2 = random.nextInt(30);
            int value = random.nextInt(100) + 1;
            if (persons.get(id1).isLinked(persons.get(id2))) {
                continue;
            }
            network.addRelation(persons.get(id1).getId(), persons.get(id2).getId(), value);
        }
        return network;
    }

第九次作业我在数据生成时我创建了一个30个人的有300条边的图,生成图之后之后对于queryTripleSum方法只需要严格按照规格写就可以通过test。

  • 第二次作业

第十次作业和第九次作业的junit测试比较类似,都是针对查询类方法,即无副作用,不能改变数据的方法进行测试。这次作业对关系网络图的要求比较高,我使用第九次作业的图实现规格之后的限制之后直接提交并不能通过所有的case测试。于是,我对我的关系图进行了改进,将generateNetwork拆分成generateDenseNetwork和generateSparseNetwok两种方法,分别生成密集图和稀疏图。密集图为30个人的网络400条边。稀疏图为30个人的网络20条边。

我们发现queryCoupleSum中主要调用了queryBestAcquaintance,所以我们可以对这个方法多加考虑,极限情况为这个人的getAcquaintances为空和这个人的getAcquaintances包含其他所有人,也即对应稀疏图和密集图两种情况。

  • 第三次作业

第十一次作业需要进行测试的方法为删除方法(deleteColdEmoji),需要对原数据集进行删除操作,与前两次只进行查询操作不同。

对于数据生成,由于只对message进行测试,与network的person关系网没有关系,所以这一次我只构造了只有一个三元环关系的图。

这次我还学到了一个根据规格构造数据的方法。我们可以根据deleteColdEmoji方法的规格进行分析:

第一个到第四个ensures主要对emojiIdList和emojiHeatList进行检验,所以我们在构造数据时首先要确保有emojiMessage类型的信息,之后我们可以考虑删除的几种情况:一个也不删除(limit == 0)、部分删除、全部删除(limit很大)。

第五个ensures的含义是,如果原messages中的emojiMessage对象在删除操作后仍然在新的messages中,则要求不能修改这个对象。

第六个ensures的含义是,如果是原messages中不为emojiMessage类的对象,则在删除操作后也要在新的messages中,且不能修改这个对象的信息。对于这个条件,我们就需要构造一些不是emojiMessage类的message数据加入network,如Message类、RedPocketMessage类、NoticeMessage类。并且在判断相等时,可以对对象的属性进行比较类似strictEquals,保证notassigned。

对规格与实现分离的理解

对于规格与实现的分离,我在前言中已经有所提及,规格只是一种契约,一种限制。我们的实现不一定要完全按照规格提供的方法,而且规格提供的方法往往时间复杂度很高,所以我们在编程时要注意性能和效率。

心得体会

  • 本单元,我学会了理解JML规格,并初次接触到了"契约式编程",感受到了按照契约编程的诸多优势——高可靠性、高可复用性、便于测试

  • 本单元的三次作业帮助我复习了很多图论算法——并查集、最小生成树、DFS、BFS等等。对于性能的追求也迫使我见识了很多新的算法。

  • 本单元同样也锻炼了我写数据生成器的能力。为了提高数据的强度,我需要一边看JML规格,一边有针对性地编写数据生成器,而这个过程让我锻炼出了写一个简易的java评测机的能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值