北航面向对象设计与构造第三单元课程设计总结

杨曜铖22371281 学生 2024-05-18 14:40:35

北航面向对象设计与构造第三单元课程设计总结

免责声明:为便于理解,本文有具体代码出现,请自觉不要照搬使用。

一、再看测试方法论:工程化工具与测试的“灵感”

在经过课程内容较长时间的编码、测试和调试训练之后,我对软件测试和调试有了一些新的理解,特总结如下。

我们在初学“测试”的工程化方法的部分时,往往会陷入一种错觉,以为软件测试就仅仅是将工程的测试方法在整个软件上实施一遍,就可以保证至少较大程度上软件没有错误。然而,在先导课程使用了这样的方法之后,我曾苦恼地发现写了大量的单元测试,但是仍然没有发现或是不能发现全部的错误,也即测试工作的效率并不高。

这样的测试其实陷入了一个理解上的误区。测试的工程化方法给我们提供了工具,但也仅仅是提供了可供我们选择的一套质量保障工具包。覆盖式地使用所有工具,或者进行完全覆盖式的测试,并不是最优的测试方案,因为测试希望达到某种效率要求,即用尽可能小的工作量覆盖大部分可能发生错误的位置。正如韦伯在《以学术为业》中说明的,“如果有人以为数学家只要坐在书桌前,把弄米尺、计算器等,就能得到有学术价值的成果,这是很幼稚的想法。”——同样的情形也出现在软件测试中。

对于一些测试方法中的所谓“覆盖”,例如因果图分析,其本身也仅是可供我们选择的工具之一:即在我们认为错误不是出在常规的、容易发现的逻辑错误,而是很可能处于边界情况时,可以选择的一种工具。这种“覆盖”,即便由测试自身的原则来说明,也是不可能保证对正确性的完全保证——“测试具有不可穷尽性”。如果我们对于所有,包括那些本来可以用简单的、符合直觉的代码审查方法就能发现的错误,都用这样的方法来实现,那么大部分测试工作都将成为无用的累赘。

其他基础内容见先导课程第三次讨论区“软件测试笔记”。

二、功能架构

存储图信息的数据结构及维护

使用一个名为UnionFindSet的类实现了一个建立在图节点上的并查集,但由于封装的缘故,将其他一些动态规划的变量也封装在内(这里仅有一个tripleNum)。同理,虽然每个结点的类型名称为UfsEntry,但是他们同时也是图的结点。

并查集实际上完全可以实现为一个“多个无序集合的集合”的结构,也就是有多个无序集合(HashSet),每个代表一个最大连通图,这样便于理解和维护。实现为树结构,并且在搜索时才压缩路径,实际上是一种懒惰优化——每次加入的操作复杂度o(1),直到查询其归属(根节点)才进行路径压缩而不是每次加入就搜索并加入到父节点上;那些加入后从不被查询的节点永远不会被执行路径压缩,从而节省了这一部分的复杂度。

定义内部类UfsEntry,因为这一类与并查集结构高度绑定,外部不需要使用;定义为静态类是其仅为一个结点数据结构,不使用外部类的方法。

这个并查集附带的种种高度特定程序相关的信息使之成为一个在此程序之外没有复用价值的数据结构,其实完全可以不使用泛型指定Map的key类型,而直接进行硬编码;这里使用泛型就是一个反面示例。

// MyNetwork.java:
public class MyNetwork implements Network {
    private final UnionFindSet<Integer> unionFindSet = new UnionFindSet<>();
    /* ... */
}

// UnionFindSet.java:
public class UnionFindSet<E> {
    private static class UfsEntry {
        private int groupSize;
        private UfsEntry father;
        private final HashSet<UfsEntry> graphLinks;

        UfsEntry(UfsEntry father) {
            this.groupSize = 1;
            this.father = father;
            graphLinks = new HashSet<>();
        }
    }

    private final HashSet<UfsEntry> roots = new HashSet<>(); // Representative roots
    private final HashMap<E, UfsEntry> entries = new HashMap<>(); // E elm -> UfsEntry
    private int tripleNum = 0;

    public void addRoot(E element) {
        /* ... */
    }

    public void addLink(E element1, E element2) {
        /* ... */
    }

    public void disLink(E element1, E element2) {
        /* ... */
    }

    // Wrapper for recursive function pathExist().
    private boolean pathExist(UfsEntry ufsEntry1, UfsEntry ufsEntry2) {
        return pathExist(ufsEntry1, ufsEntry2, new HashSet<>());
    }

    private boolean pathExist(UfsEntry curEntry, UfsEntry tarEntry, HashSet<UfsEntry> checked) {
        /* ... */
    }

    public int shortestPathLength(E id1, E id2) {
        /* ... */
    }

    public boolean sameGroup(E element1, E element2) {
        return findRoot(entries.get(element1)) == findRoot(entries.get(element2));
    }

    public int numOfGroup() {
        return roots.size();
    }

    public int getTripleNum() {
        return tripleNum;
    }

    // Wrapper for recursive function rerootGroup().
    private int rerootGroup(UfsEntry startingEntry) {
        return rerootGroup(startingEntry, startingEntry, 0, new HashSet<>());
    }

    private int rerootGroup(UfsEntry curEntry, UfsEntry startingEntry, int curTravelCount,
                            HashSet<UfsEntry> checked) {
        /* ... */
    }

    private UfsEntry findRoot(UfsEntry ufsEntry) {
        // Find the root.
        UfsEntry trying = ufsEntry;
        while (trying.father != null) {
            trying = trying.father;
        }

        // Compress searching path.
        if (ufsEntry.father != null) {
            ufsEntry.father = trying;
        }

        return trying;
    }
}

三、性能优化

规格与实现是分离的,体现在两个方面:

  1. 在规格描述的方法中,规格与实现是分离的,方法实现只要满足规格约束即可。
  2. 在规格描述的方法之外,可以引入其他数据结构以及算法的内容,来满足规格的约束。即规格只规定“接口方法”,应该尽可能把主要复杂业务逻辑的方法放在非接口方法即其他实现方法中。

1. 使用并查集的优化(见上文,略)

2. 使用简单动态规划的优化

对于比较多的方法都可使用简单动态规划进行优化。以bestAcquaintance为例:

public class MyPerson implements Person {
    private Person bestAcquaintance = null;
    
    private void rebuildBestAcquaintance() {
        int maxValue = -1;
        if (values.isEmpty()) {
            bestAcquaintance = null;
            return;
        }
        for (Person p: values.keySet()) {
            int value = values.get(p);
            if (
                    value > maxValue ||
                    (value == maxValue && p.getId() < bestAcquaintance.getId())
            ) {
                bestAcquaintance = p;
                maxValue = value;
            }
        }
    }

    public void addLink(Person person, int value) {
        /* ... */
        // Modify best acquaintance.
        if (
            bestAcquaintance == null ||
            value > values.get(bestAcquaintance) ||
            (value == values.get(bestAcquaintance) &&
             person.getId() < bestAcquaintance.getId())
        ) {
            bestAcquaintance = person;
        }
    }
    
    public void modifyLink(Person person, int valueModAmount) {
        /* ... */
        // Modify best acquaintance.
        assert bestAcquaintance != null;

        if (person == bestAcquaintance) {
            rebuildBestAcquaintance();
        } else {
            final int value = values.get(person);
            if (
                    value > values.get(bestAcquaintance) ||
                    (value == values.get(bestAcquaintance) &&
                     person.getId() < bestAcquaintance.getId())
            ) {
                bestAcquaintance = person;
            }
        }
    }

    public void deleteLink(Person person) {
        /* ... */
        // Modify best acquaintance.
        if (person == bestAcquaintance) {
            rebuildBestAcquaintance();
        }
    }

    /* ... */
}

四、规格模板测试

1. 理解规格

阅读规格时,我们不需要再进行证明,应该快速捕捉语义成分,结合对方法语义的初步理解而快速划分区域,在此基础上具体理解每一部分规格的语义。不要被逻辑和证明的细节阻碍了对语义的理解。以deleteColdEmojis为例,其规格为:

// Emojis.
// 1. All emojis with heat(org) >= limit still exist.
ensures (\forall int i; 0 <= i && i < \old(emojiIdList.length);
        (\old(emojiHeatList[i] >= limit) ==>
        (\exists int j; 0 <= j && j < emojiIdList.length; emojiIdList[j] == \old(emojiIdList[i]))));
// 2. old --deletion--> cur, no new emj added.
ensures (\forall int i; 0 <= i && i < emojiIdList.length;
        (\exists int j; 0 <= j && j < \old(emojiIdList.length);
        emojiIdList[i] == \old(emojjiIdList[j]) && emojiHeatList[i] == \old(emojiHeatList[j])));
// 3. length = num of emojis with heat(org) >= limit
ensures emojiIdList.length ==
        (\num_of int i; 0 <= i && i < \old(emojiIdList.length); \old(emojiHeatList[i] >= limit));
ensures emojiIdList.length == emojiHeatList.length;

// Messages.
// 1. Still exist and not assigned to: not Emoji / Emoji but containsEmojiId(now).
ensures (\forall int i; 0 <= i && i < \old(messages.length);
        (\old(messages[i]) instanceof EmojiMessage &&
         containsEmojiId(\old(((EmojiMessage)messages[i]).getEmojiId()))  ==> \not_assigned(\old(messages[i])) &&
         (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i])))));
ensures (\forall int i; 0 <= i && i < \old(messages.length);
        (!(\old(messages[i]) instanceof EmojiMessage) ==> \not_assigned(\old(messages[i])) &&
         (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i])))));
// 2. Length.
ensures messages.length == (\num_of int i; 0 <= i && i < \old(messages.length);
        (\old(messages[i]) instanceof EmojiMessage) ==>
         (containsEmojiId(\old(((EmojiMessage)messages[i]).getEmojiId()))));

2. 编写验证测试

根据分析得到的语义,将验证方法分为执行前处理,执行中,返回值检验和执行后,按规格并注意代码风格(良好体现语义的命名)实现即可。

@Test
public void deleteColdEmoji_ok() {
    // Emj 1.
    HashSet<Integer> emojiIdsBiggerthanLimit = new HashSet<>();
    for (int i = 0; i < network.getEmojiHeatList().length; i++) {
        if (network.getEmojiHeatList()[i] >= limit) {
            emojiIdsBiggerthanLimit.add(network.getEmojiIdList()[i]);
        }
    }
    // Emj 2.
    int[] oldEmojiIdList = network.getEmojiIdList();
    // Emj 3.
    int newEmojiIdListLength = emojiIdsBiggerthanLimit.size();
    // Msg 1.
    HashSet<Message> notAssignedMsgs = new HashSet<>(Arrays.asList(network.getMessages()));

    // Execute
    final int ret = network.deleteColdEmoji(limit);
    // Return value.
    Assert.assertEquals(network.getEmojiIdList().length, ret);

    // Emj 1.
    for (int emojiId: network.getEmojiIdList()) {
        emojiIdsBiggerthanLimit.remove(emojiId);
    }
    Assert.assertTrue(emojiIdsBiggerthanLimit.isEmpty());
    // Emj 2.
    HashSet<Integer> newEmojiIdList = new HashSet<>();
    for (int now: network.getEmojiIdList()) { newEmojiIdList.add(now); }
    for (int prev: oldEmojiIdList) { newEmojiIdList.remove(prev); }
    Assert.assertTrue(newEmojiIdList.isEmpty());
    // Emj 3.
    Assert.assertEquals(newEmojiIdListLength, network.getEmojiIdList().length);
    // Emj 4.
    Assert.assertEquals(network.getEmojiIdList().length, network.getEmojiHeatList().length);
    // Msg 1.
    notAssignedMsgs.removeIf(msg->
        (msg instanceof EmojiMessage)
        && !network.containsEmojiId(((EmojiMessage) msg).getEmojiId()));
    HashSet<Message> newMsgs = new HashSet<>(Arrays.asList(network.getMessages()));
    Assert.assertTrue(newMsgs.containsAll(notAssignedMsgs));
    notAssignedMsgs.forEach(msg -> Assert.assertTrue(msgStrictEquals(msg, network.getMessage(msg.getId()))));
    // Msg 2.
    Assert.assertEquals(notAssignedMsgs.size(), newMsgs.size());
}

3. 修改数据生成

如上文所述,应该把较容易触发这一方法发生错误的数据情况考虑到数据生成中,因此对数据生成进行部分修改。

...全文
70 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
学员在中科院学习期间独立完成制作ARM开发板、开发触摸屏驱动等36个嵌入式专题实验项目,1-3个大型项目。其他实验项目如:智能机器人等可在结业后完成。 教学周期:10个月,其中第一学期3个月,第二学期5个月,课程实训2个月。 课程编号 教学单元 教学内容 就业岗位 第一学期 教学课时3个月 ZKQ090101 网络原理及linux服务 网络概述;数据通信基础;网络体系结构与协议;局域网;网络互联与TCP/IP协议;Internet及其应用;网络连接设备与技术 •Linux下C开发人员 •面向C嵌入式开发人员 ZKQ090102 Linux安装 正确安装Linux操作系统 ZKQ090103 Linux 开发环境与应用程序设计 Linux C 编程基础,gdb调式器,Makefile文件概述;Linux系统调用原理;基于系统调用的文件I/O操作;文件上锁,程序机制与功能,特殊文件的操作;串口编程,串口通信的实现;Linux多线程编程 ZKQ090104 嵌入式Linux系统基础 Linux的进程管理,特殊进程的操作;Linux进程间的通信(一):管道通信;Linux进程间的通信(二):信号;Linux进程间的通信(三):消息队列与共享内存 ZKQ090105 FPGA1/CPLD EDA技术简介以及CPLD/FPGA 基础知识,QuartusII软件入门;硬件描述语言( VHDL)基本语法和实践;硬件描述语言( Verilog HDL)基本语法和实践;FPGA实验、DSP入门;Altium Designer 6.x电路原理图与PCB设计 ZKQ090106 Linux内核裁剪与移植 嵌入式简介;Linux 基础知识;交叉编译;Linux 内核配置;Linux 内核移植;根文件系统制作;Boot Loader 制作。 第二学期 教学课时5个月 ZKQ090201 基于ARM7开发平台设计 ARM技术简介以及基础知识,ARM应用入门;ARM启动代码分析、链接脚本讲解;μC/OS II V2.85在ARM的移植;μC/OS II内核精讲;ARM经典实验。 •高级嵌入式开发工程师 •Linux设备驱动开发工程师 •嵌入式系统工程师 •嵌入式技术支持工程师 •嵌入式软件开发工程师 •嵌入式硬件开发工程师 •ARM平台嵌入式开发工程师 ZKQ090202 基于ARM9开发平台设计 嵌入式linux开发平台简介及开发流程;嵌入式Linux开发环境的建立;多线程应用程序设计;串行端口程序设计;A/D接口实验;D/A接口实验;简单的嵌入式WEB服务器实验;RS-485通讯实验;直流与步进电机实验;内核驱动设计入门――模块方式驱动实验;内核驱动设计实验――触摸屏驱动;内核驱动设计――网卡驱动移植及实验;内核驱动设计――LCD驱动移植;音频驱动及实应用实验;USB接口试验 ZKQ090203 FPGA2/SOPC 基于NIOSII嵌入式软处理器的SOPC(可编程片上系统)系统的开发基础和设计技巧以及FPGA最小系统设计方法;SOPC实验。 ZKQ090204 Linux网络编程 Socket 套接字(TCP、UDP);原始套接字;多进程、多进程网络编程 ZKQ090205 TCP/IP协议编程 TCP/IP协议分析;构造数据包。 ZKQ090206 C++QT图形编程 Qt开发环境安装与配置;C++面向对象基础;Qt内置组件应用与自定义组件开发;Qt信号与槽机制;Qt模型应用(树、表、栈);Qt多线程与网络;Qt图形开发;Qt内置数据仓库技术(mysql、sqlite);Qt解析XML。 ZKQ090207 嵌入式Linux系统原理 Linux内核简介;进程管理及调度;中断及中断处理程序;下半部和工作队列;内核同步方法;定时器和时间管理;内存管理;进程地址空间;内核调试技术;内核可移植性概述。 ZKQ090208 嵌入式Linux驱动开发 设备驱动及内核模块概述;构造和运行模块;编写字符设备驱动程序I;编写字符设备驱动程序II;高级字符驱动程序;与硬件通信;中断处理;Linux设备模型;内存映射操作;DMA技术及应用;块设备驱动程序;网络设备驱动程序;贞缓冲设备驱动;PCI设备驱动程序;USB驱动程序。 课程实训 实训课时2个月 3-5名学员组成一个项目小组,项目自选,项目小组提交项目报告,经审核同意后拨付项目经费,在项目指导教师指导下项目实施,项目完成应有成型产品,组织召开项目完成报告会,项目小组提交项目完成报告书,项目结束。

301

社区成员

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

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