2023软工结对项目-最长单词链

项目

内容

这个作业属于哪个课程

https://bbs.csdn.net/forums/buaa-ase2023

这个作业的要求在哪里

https://bbs.csdn.net/topics/613883108

我在这个课程的目标是

学习软件开发,软件测试以及团队工作、大型项目开发

这个作业在哪个具体方面帮助我实现目标

通过结对编程实践极限编程的思想,学会高效合作。

1. 基本信息

教学班级

周四班

组员

陈灿宇,史泽宇

Gitee项目地址

https://gitee.com/chen-canyu/word-list-project

2 PSP预计时间

PSP2.1

Personal Software Process Stages

预估耗时(分钟)

实际耗时(分钟)

Planning

计划

90

· Estimate

· 估计这个任务需要多少时间

90

Development

开发

2090

· Analysis

· 需求分析 (包括学习新技术)

60

· Design Spec

· 生成设计文档

60

· Design Review

· 设计复审 (和同事审核设计文档)

150

· Coding Standard

· 代码规范 (为目前的开发制定合适的规范)

20

· Design

· 具体设计

120

· Coding

· 具体编码

900

· Code Review

· 代码复审

240

· Test

· 测试(自我测试,修改代码,提交修改)

540

Reporting

报告

170

· Test Report

· 测试报告

90

· Size Measurement

· 计算工作量

20

· Postmortem & Process Improvement Plan

· 事后总结, 并提出过程改进计划

60

合计

2350

3. 阅读资料

Information Hiding(信息隐藏)是一种软件设计原则,它的目标是将实现细节隐藏在接口之后,以便更容易地对系统进行修改和扩展。在结对编程中,这种原则可以通过定义抽象接口来实现。接口应该只公开必要的信息,而不是向外部公开所有的实现细节。例如本项目我们的core.dll只暴露了必要的三个接口,其他的实现细节隐藏。

Interface Design(接口设计)是指设计程序模块之间的接口,使得它们可以有效地通信。在结对编程中,可以使用面向对象设计模式,如策略模式或工厂模式,来定义接口并将其与具体实现分离。在结对过程中,我负责core模块,而队友负责异常模块,我们提前设计好core内嵌error的接口以便完成后,顺利快速的对接。

Loose Coupling(松耦合)是指模块之间的依赖关系应该尽可能的少,以减少代码的耦合度。这有助于减少代码的维护成本和改变一个模块时对其他模块的影响。在结对编程中,最后我们与其他组互换dll,进行GUI与dll的松耦合测试。

4. 计算模块的设计与实现

4.1 计算模块的设计

分析:仔细阅读结对编程文档,我们不难发现这是一个图论问题,我们不妨将这几个图论问题的解决过程一步步分解为子问题来梳理。

  • 如何建图?开始时我们想的是一个单词是一个点,每加入一个单词便要将他与相关联的所有单词加上连接关系,但是我们经过讨论发现,这样的方法会导致图中点的个数非常大,边的个数也很多,对于求解过程的速度与效率限制非常大。经过讨论交流我们发现不妨将每个字母抽象为一个点,单词则是从首字母所在点到尾字母的一条边(单词即边),这样我们成功的将图的点数化为仅有26个,将问题简单化。

  • 如何存储?

  • 单词类(边类):由上述的分析我们知道,一个单词就是一条边。所以我们在单词类中存储单词的两个端点,该单词的完整字符串,该单词的长度信息。

  • 图类:图中共有26个点,我们在这个类中存储图的邻接列表,边集,入度列表,出度列表,每个点的自环数,每个点的自环的字母总数。其中这些信息有的是图的基本信息,有的是为了方便后续使用额外存储的附加信息。

4.2 计算模块的实现

需求一:产生所有的单词链

这个问题比较简单,建好图以后,是一个dfs算法,我们利用对边染色的dfs,从每个起点出发,枚举从该起点出发的所有路径,将这些路径整理成字符串列表进行输出,简而言之就是如下的流程图。

需求二:产生单词最多的单词链

无环:对于无环的情况,首先建图,然后求出拓扑序,然后按照拓扑序进行动态规划。有效出发点被初始化为该点的自环数目,转移方程为

需求三:产生字母数最多的单词链

无环:对于无环的情况,首先建图,然后求出拓扑序,然后按照拓扑序进行动态规划。有效出发点被初始化为该点的自环上的字母总数,转移方程为

需要注意的是,gen_chain_char有一些干扰答案的边必须去掉,这些类型的边上如果字母数恰好比较多,会被我们的算法选择出来构成不符合要求的单词链。这些类型的边有如下两种:

  • 孤立的自环边:一个点上只有一个自环,没有其他边与该点相联。

  • 孤立的树边:边的起点终点都没有自环,起点入度0,终点出度0.这两类边字母数再多也无法构成单词链,需要去除,以免影响算法输出正确答案。

流程图与需求二基本一致,唯一多了的步骤是提前去除孤立自环,孤立树边,动态规划初始化数组要初始化为该点上的自环的总字母数。

需求四:指定首尾字母

  • 指定首字母:对于这个需求只需要我们在动态规划dp初始化的时候做一点“手脚”,即指定首字母字母的dp值为自环数目(对于gen_chain_char是自环上的字母总数目),其余点的初始值设置为-1。

  • 指定尾字母:对于这个需求只需要在最后寻找最佳dp值时直接定位到指定尾字母的dp结果即可。需求五:指定所有单词开头都不允许出现的首字母

对于这个需求,由于我们建图之前已经知道当前需求,所以如果指定所有单词都不允许出现的首字母,建图时遇到以该字母打头的单词,我们跳过他,不把他加入边集中。

需求六:允许包含隐含环

该需求是6个需求中比较难实现的最后一个需求,为了延续之前的思路即toposort+dp的思路。由于toposort是针对DAG的算法,因此我们想到将有环图利用tarjan算法分级为许多强连通分量,将各连通图缩点后正常进行toposort和dp。当然dp时,各连通块内部各点需要额外维护。我们的dp思路如下:

首先getScc求出所有强连通分量(连通块),接着进行缩点操作,缩点后得到的图不含环,可以进行topo排序,topo排序后沿用之前的dp操作,初始值依然初始化为每个点的自环数(或者每个点的自环字母总数),dp公式略有变化,以最长单词数为例。

dp[i] = dp[j]+1+ self_loop_cnt[i]+maxDist[m->i] (m是j指向A块的入口点)

j,i满足的条件如下:

1.i属于连通块A,j属于连通块B,top[A] > top[B]

2.存在边j->m从B指向A

3.每个点的初始化为块内以这个点为结尾的最长路径

5. 开发环境下编译器无警告的截图

可以看到我们编写的代码没有警告,其中optparse是我们引用的开源命令行解析模块,只有这个模块有警告。项目来源:https://github.com/liu2guang/optparse

为什么会使用optparse而非手写?因为最初我们使用getopt.h,在MinGW下可以无缝导入。但MinGW工具链编译出的Dll无法被调用(这段经历说来话长),后续换VS工具链才步入正轨。这时我们才知道getopt.h并非每个编译器都有,遂引入其更安全的一个实现,即optparse.h。

6. UML图

4.2 计算模块的实现”一节已经清晰描述了计算模块内部的算法流程。

我们的设计是面向过程的,这里展示描述模块交互的示意图。

7.性能分析与计算模块的性能改进

根据第4节设计的算法分析,拓扑排序和DP是线性复杂度,无法进一步优化;求SCC内部所有点之间最大距离需要大量DFS,理论上是我们主要的优化目标。

采用perf性能分析工具进行性能分析,寻找性能瓶颈。

性能改进:

以一组随机数据(参数-c -r)为例,v0.1未优化时达到惊人的:

v0.2:利用perf进行性能分析,发现主要性能瓶颈的确在于dfsGraph,原方法是从每个点开始都要dfs,采取边染色的方法,这意味着每一条路径我们都需要遍历出来。但在连通块内部寻找任意两点之间的最长距离,我们只需要一个数值,以及该数值对应的一条路径。我们意识到:

  • 最长路径不止一条,但我们需要的最长路径值是唯一的,输出时我们也只需要选择一条即可

  • 重边(包括自环)遍历时是全排列,dfs所遍历出的路径数将产生指数级爆炸

于是我们用两点间的剩余可遍历次数(int)代替访问标记(bool),固定重边遍历顺序,以排序+贪心的方式保证先遍历字符串较长的单词,使遍历路径数是原先的(1/n!)。

该结果来自yyh组的测试。

v0.3: 我们意识到dfsGraph的数据结构仍有优化空间,将dfsGraphFast中当成栈使用的vector改为deque,使push和pop操作更快。此时测试阶段基本结束,我们开启了release构建选项,终于享受到心心念念的完整的编译器优化。

v0.4:此前,dfsGraphFast函数中,每次进入都会写pathList,这也意味着每条字单词链都会完整写入pathList,占用大量内存和时间。我们为dfsGraphFast增加参数curMaxLen[26],记录从start到i最长路径的数值(分别适配-w和-c参数),结果(-c参数见chainLen形参)大于记录值才会写pathList,大大减少写操作和后续排序复杂度。

优化后同组数据debug版本:

release版本:

优化后性能分析:

8.阅读 Design by Contract,Code Contract 的内容

Design by Contract(DBC)和Code Contract是两种常见的软件开发方法,它们都可以用于确保代码的正确性和可靠性。DBC是由Bertrand Meyer在20世纪80年代提出的一种面向对象程序设计的方法,它基于三个核心概念:前置条件、后置条件和类不变式。Code Contract是微软提出的一种.NET平台的实现,它也基于相似的概念,但提供了更强大的功能。

优点:

  • 提高代码质量:使用DBC和Code Contract可以确保代码满足一定的规范和标准,减少错误和bug的出现,提高代码质量和可靠性。

  • 易于维护:DBC和Code Contract将程序的功能和需求进行了明确的描述,使得程序更加易于维护和修改。

缺点:

  • 增加了开发成本:使用DBC和Code Contract需要额外的工作量和成本,包括代码编写、测试和维护。

  • 可能影响性能:DBC和Code Contract需要在运行时检查代码的条件和约束,可能会对程序的性能产生一定的影响。如何将它们融入结对作业中:

在结对作业中,可以使用DBC和Code Contract来规范代码实现和保证代码正确性。具体做法如下:

  • 在代码实现前,让一方先阅读需求文档,并使用DBC或Code Contract的方式将需求中的条件和约束进行明确的描述。

  • 一方在编写代码时,按照DBC或Code Contract的方式实现需求中的条件和约束。另一方在编写代码时,可以根据DBC或Code Contract的描述来评估代码的正确性,并提出修改建议。

  • 在代码实现完成后,双方可以使用DBC或Code Contract的工具进行代码检查和测试,以确保代码满足条件和约束。

  • 通过使用DBC或Code Contract,结对作业可以更加高效地完成,同时也可以保证代码的质量和可靠性。

9.单元测试

计算模块core的单元测试构造,单元测试时我们同时验证以下几点。

  • 返回值是否符合预期

  • 如果返回单词链,单词链是否合法

单元测试序号

构造思路

Test(core,1)

无环的简单测试 针对gen_chains_all

Test(core,2)

有环简单测试,针对gen_chains_all是否正常报错有环

Test(core,3)

无环简单测试针对gen_chain_word的各种简单组合情况

Test(core,4)

无环简单测试针对gen_chain_word的各种复杂组合情况,有重复单词

Test(core,5)

无环简单测试针对gen_chain_char的各种简单情况

Test(core,6)

无环简单测试针对gen_chain_char的各种复杂组合情况,有重复单词,查看是否正确去除孤立自环边、孤立树边

Test(core,7)

有环复杂测试 针对gen_word各种情况 -h,-t,-j

Test(core,8)

有环复杂测试 针对gen_char各种情况

Test(coreerror,1)

-h -t设置字母相同但是没有指定-r

Test(coreerror,2)

-h -j设置同一个字母矛盾

Test(coreerror,3)

检查有环但是没有设置-r是否报错

//无环的简单测试 针对gen_chains_all
TEST(core,1) {
    const char *words[] = {"ab","bcb","bd","bc","cbcbc","cf","ce"};
    char* result[20000];
    int l = gen_chains_all(const_cast<char **>(words),7,result);
    EXPECT_EQ(l,29);
}
//有环简单测试,针对gen_chains_all是否正常报错
TEST(core,2) {
    const char *words[] = {"ab","bcb","bcbcb","bd","bc","cbcbc","cf","ce"};
    char* result[20000];
    int l = gen_chains_all(const_cast<char **>(words),7,result);
    EXPECT_TRUE(coreErrorDectect(result,l,NOT_EXIST_RING_PARA));
}
//无环简单测试针对gen_chain_word的各种简单组合情况
TEST(core, 3) {
    const char *words[] = {"ab", "bcb", "bd","cd","dbd"};
    //-w
    char * result[20000];
    int l = gen_chain_word(const_cast<char **>(words), 5, result, 0, 0, 0, false);
    EXPECT_TRUE(chainIsValid(l, result, 0, 0, 0));
    EXPECT_EQ(l, 4);
    //-w -h b
    char * result2[20000];
    int l2 = gen_chain_word(const_cast<char **>(words), 5, result2, 'b', 0, 0, false);
    EXPECT_TRUE(chainIsValid(l2, result2, 'b', 0, 0));
    EXPECT_EQ(l2, 3);
    //-w -t b
    char * result3[20000];
    int l3 = gen_chain_word(const_cast<char **>(words), 5, result3, 0, 'b', 0, false);
    EXPECT_TRUE(chainIsValid(l3, result3, 0, 'b', 0));
    EXPECT_EQ(l3, 2);
    //-w -t b -j a
    char * result4[20000];
    int l4 = gen_chain_word(const_cast<char **>(words), 5, result4, 0, 'b', 'a', false);
    EXPECT_TRUE(chainIsValid(l4, result4, 0, 'b', 'a'));
    EXPECT_EQ(l4, 0);
    ccout(result4,l4);
}
//无环简单测试针对gen_chain_word的各种复杂组合情况,有重复单词
TEST(core,4){
    const char *words[] = {"ab","ab","bcb", "bcb", "bd","cd","dbd"};
    //-w -h a -t c
    char * result[20000];
    int l = gen_chain_word(const_cast<char **>(words), 7, result, 'a', 'c', 0, false);
    EXPECT_TRUE(chainIsValid(l, result, 0, 0, 0));
    EXPECT_EQ(l, 0);
​
    //-w -h b -t d -j a
    char * result2[20000];
    int l2 = gen_chain_word(const_cast<char **>(words), 7, result2, 'b', 'd', 'a', false);
    EXPECT_TRUE(chainIsValid(l2, result2, 'b', 'd', 'a'));
    EXPECT_EQ(l2, 3);
}
//无环简单测试针对gen_chain_char的各种简单情况
TEST(core,5) {
    const char *words[] = {"ab","bcbcb", "bd","cd","dbd","cfffffffff","fffffffffff"};
    //-c
    char * result[20000];
    int l = gen_chain_char(const_cast<char **>(words), 7, result, 0, 0, 0, false);
    EXPECT_TRUE(chainIsValid(l, result, 0, 0, 0));
    EXPECT_EQ(l, 2);
    //-c -h b
    char * result2[20000];
    int l2 = gen_chain_char(const_cast<char **>(words), 7, result2, 'b', 0, 0, false);
    EXPECT_TRUE(chainIsValid(l2, result2, 0, 0, 0));
    EXPECT_EQ(l2, 3);
    //-c -t b
    char * result3[20000];
    int l3 = gen_chain_char(const_cast<char **>(words), 7, result3, 0, 'b', 0, false);
    EXPECT_TRUE(chainIsValid(l3, result3, 0, 'b', 0));
    EXPECT_EQ(l3, 2);
    //-c -j c
    char * result4[20000];
    int l4 = gen_chain_char(const_cast<char **>(words), 7, result4, 0, 0, 'c', false);
    EXPECT_TRUE(chainIsValid(l4, result4, 0, 0, 'c'));
    EXPECT_EQ(l4, 4);
}
//无环简单测试针对gen_chain_char的各种复杂组合情况,有重复单词,查看是否正确去除孤立自环边、孤立树边
TEST(core, 6) {
    const char *words[] = {"ab", "bcbcb", "bd", "bd", "cd", "dbd", "cfffffffff", "fffffff",
                           "xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx"};
    //-c
    char *result[20000];
    int l = gen_chain_char(const_cast<char **>(words), 8, result, 0, 0, 0, false);
    EXPECT_TRUE(chainIsValid(l, result, 0, 0, 0));
    EXPECT_EQ(l, 2);
    //-c -t d -h b -j a
    char *result2[20000];
    int l2 = gen_chain_char(const_cast<char **>(words), 8, result2, 'b', 'd', 'a', false);
    EXPECT_TRUE(chainIsValid(l2, result2, 'b', 'd', 'a'));
    EXPECT_EQ(l2, 3);
    //-c
    const char *words2[] = {"ab","kkkkkk","xbbbbbbbb"};
    char *result3[20000];
    int l3 = gen_chain_char(const_cast<char **>(words2), 3, result3, 0, 0, 0, false);
    EXPECT_EQ(l3, 0);
}
//-------------------------以下是有环测试-----------------------------
//有环复杂测试 针对gen_word各种情况 -h,-t,-j
TEST(coreloop, 7) {
    const char *words[] = {"ababa", "adoctb", "boxa",
                           "cx", "aac", "cotb", "bard", "dddddddddddddddddddddddddddddd",
                           "cec", "ce", "ef", "fg", "ge", "cff", "fgg", "gee", "f", "ff",
                           "fff", "ffff"};
    char * result[20000];
    int l = gen_chain_word(const_cast<char **>(words), 20, result, 0, 0, 0, true);
    EXPECT_TRUE(chainIsValid(l, result, 0, 0, 0));
    EXPECT_EQ(l,15);
    char * result2[20000];
    int l2 = gen_chain_word(const_cast<char **>(words),20,result2,'c',0,0, true);
    EXPECT_TRUE(chainIsValid(l2,result2,'c',0,0));
    EXPECT_EQ(l, 15);
}
//有环复杂测试 针对gen_char各种情况
TEST(coreloop,8) {
    const char *words[] = {"Aa","af","afff","ffff","ff","fb","be",
                           "bd","ebe","ebebe","cf","bc","cd"};
    //-c -r
    char * result[20000];
    int l = gen_chain_char(const_cast<char **>(words), 13, result, 0, 0, 0, true);
    EXPECT_TRUE(chainIsValid(l, result, 0, 0, 0));
    EXPECT_EQ(l,8);
    //-c -r -h a -t f
    char * result2[20000];
    int l2 = gen_chain_char(const_cast<char **>(words), 13, result2, 'a', 'f', 0, true);
    EXPECT_TRUE(chainIsValid(l2, result2, 'a', 'f', 0));
    EXPECT_EQ(l2,7);
    //-c -r -h e -t e
    char * result3[20000];
    int l3 = gen_chain_char(const_cast<char **>(words), 13, result3, 'e', 'e', 0, true);
    ccout(result3,l3);
    EXPECT_TRUE(chainIsValid(l3, result3, 'e', 'e', 0));
    EXPECT_EQ(l3,2);
}
//-----------------以下针对仅在core内部设计的2种异常进行测试
//针对core内部设计的3种error进行测试
//-h -t设置字母相同但是没有指定-r
TEST(coreerror,1){
    //-h -t设置字母相同但是没有指定-r
    const char *words[] = {"Aa","af","ac"};
    char * result[20000];
    int l = gen_chain_char(const_cast<char **>(words), 3, result, 'a', 'a', 0, false);
    EXPECT_TRUE(coreErrorDectect(result,l,HEAD_TAIL_LIMIT_SAME));
​
    char * result2[20000];
    int l2 = gen_chain_word(const_cast<char **>(words), 3, result2, 'a', 'a', 0, false);
    EXPECT_TRUE(coreErrorDectect(result2,l2,HEAD_TAIL_LIMIT_SAME));
​
}
//-h -j设置同一个字母矛盾
TEST(coreerror,2){
    const char *words[] = {"Aa","af","ac"};
    char * result[20000];
    int l = gen_chain_char(const_cast<char **>(words), 3, result, 'a', 'a', 'a', true);
    EXPECT_TRUE(coreErrorDectect(result,l,HEAD_LIMIT_CONTRADICTION));
    char * result2[20000];
    int l2 = gen_chain_word(const_cast<char **>(words), 3, result2, 'a', 'a', 'a', true);
    EXPECT_TRUE(coreErrorDectect(result2,l2,HEAD_LIMIT_CONTRADICTION));
}
//检查有环但是没有设置-r是否报错
TEST(coreerror,3) {
    const char *words[] = {"Aa","af","ac","aba","ca"};
    char * result[20000];
    int l = gen_chain_char(const_cast<char **>(words), 5, result, 0, 0, 0, false);
    EXPECT_TRUE(coreErrorDectect(result,l,NOT_EXIST_RING_PARA));
​
    char * result2[20000];
    int l2 = gen_chain_word(const_cast<char **>(words), 5, result2, 0, 0, 0, false);
    EXPECT_TRUE(coreErrorDectect(result2,l2,NOT_EXIST_RING_PARA));
}

覆盖率解释与说明

情况说明:

行覆盖率已经达到较高水平,但是分支覆盖率始终无法得到提升。上图可以看到一些一定会覆盖到的分支显示黄色(未覆盖),经检查我们发现有的分支一定会被覆盖到但是并没有被覆盖率分析工具识别。经过讨论与检查,我们使用了clion上导入gtest进行覆盖率测试,gtest对于编译器自动插入的分支(多为STL或者优化),无法识别,尝试加入屏蔽编译器优化的设置也无法达到预期效果,经与助教交流后尝试手动说明gtest未覆盖到的分支一定被覆盖到。

  • API.cpp该文件一共8个函数,三个指定接口函数,三个接口函数调用的内核函数,2个core内部异常抛出函数。

  • 异常函数均与有环未指定-r有关,被coreerror_3测试点覆盖。-

  • 三个接口函数为设置Config,然后调用内核函数,这些过程必须经过,只要调用接口一定会被覆盖。

  • 三个内核函数内部分支相同,判断当前是有环还是无环,上述测试点有环、无环情况均有测试,一定覆盖。

  • Calc.cpp

  • findFirstOccurrence、dsfCycle、hasCycle一定被检测有环报错的测试样例覆盖

  • dssGraph一定被gen_chains_all的样例全部覆盖

  • 剩余函数是关于gen_chain_word以及gen_chain_char,分支很少,主要在于处理计算,一定会被测试样例中的gen_chain_word以及gen_chain_char的没有测试异常的样例覆盖。

  • Config.cpp

  • 我们使用单例模式,这个文件主要对Config进行设置,如果这个文件中任何一个分支没有覆盖,相关的样例无法执行,也不会得到结果。

  • CoreError.cpp

  • CoreError中的几个简单异常均在coreerror中得到测试

  • Graph.cpp

  • 该文件主要是对Graph类的成员函数进行撰写,这些函数我们均在建图以及使用图中使用到。调用gen_chains_all,gen_chain_word,gen_chain_char一定会覆盖到所有的这些成员函数。

  • Utils.cpp

  • 主要撰写了两个输出中间函数,只要有输出一定会覆盖到这两个函数。

  • Word.cpp

  • 主要包含Word类的成员函数,调用gen_chains_all,gen_chain_word,gen_chain_char一定会覆盖到所有的这些成员函数。

10.异常处理

10.1 异常设计

关于异常处理,我们一共设计了17种异常,每一种异常都做了单元测试,以下是部分单元测试的代码以及异常介绍。

下表是输入模块能处理的异常(共15种)。

异常名称

说明

NOT_EXIST_FILE

找不到文件

ILLEGAL_FILE

文件不合法(非.txt)

LACK_OF_FILE

输入缺少文件

MORE_THAN_ONE_FILE

输入超过一个文件

NOT_EXIST_PARAMETER

输入不存在的参数(在指定7个参数之外)

DUPLICATE_PARAMETER

重复输入参数

MORE_THAN_ONE_CHAR

参数的值多于一个字符(-h,-t,-j)

LACK_OF_CHAR

参数缺少值(-h,-t,-j)

MORE_THAN_ONE_STATE

指派多于一个任务(-n,-w,-c)

LACK_OF_STATE

未指派任务(-n,-w,-c)

ILLEGAL_PARAMETER_VALUE

参数的值不是英文字符(-h,-t,-j)

UNEXPECTED_PARAMETER

参数不应带值(-r)

NOT_COMPATIBLE_PARAMETER

-n和可选参数同时出现

HEAD_TAIL_LIMIT_SAME

无-r参数时,-h,-t限制首尾字母相同

HEAD_LIMIT_CONTRADICTION

-h和-j首字母限制矛盾

下表是计算模块能处理的异常(共5种,2种独有)。

异常名称

说明

NOT_COMPATIBLE_PARAMETER

-n和可选参数同时出现

HEAD_TAIL_LIMIT_SAME

无-r参数时,-h,-t限制首尾字母相同

HEAD_LIMIT_CONTRADICTION

-h和-j首字母限制矛盾

NOT_EXIST_RING_PARA

无-r参数时输入包含隐含环

TOO_LONG_RESULT

结果超过20000行

为什么会出现3种计算模块和输入模块都能处理的异常?这完全是为交换模块考虑的,我们的界面模块已经排除了这三种错误出现的可能。

11.2 异常抛出

对于异常抛出,我们采用统一的错误码设计,通过返回值传递错误码到“上层”抛出,而非try-catch-throw。

对于CLI的输入模块能处理的错误:

int main(intargc, char*argv[]) {
    std::vectorwordList;
    intr=execPreprocess(argc, argv, wordList);
    if (r<0) {
        returnr;
    }
    execCalculation(wordList);
}

execPreprocess()是输入模块的顶层调用者。判断出异常后将层层向上抛出,直至退出程序。这样写做单元测试比较方便。对于CLI和GUI的计算模块能处理的错误,我们保证不将接口返回值用作错误码,而是直接将报错信息写入result[0]并退出,供输出模块直接输出。

11.3 异常的单元测试
//ILLEGAL_FILE (-2)
TEST(inputerror, 1) {
    const char *argv[] = {"develop.exe", "-n", "IOtest1.tx"};
    vector<string> result;
    int r = execPreprocess(3, const_cast<char **>(argv), result);
    //cout << r << endl;
    EXPECT_EQ(r, ILLEGAL_FILE);
}
//NOT_EXIST_FILE (-1)
TEST(inputerror, 2) {
    const char *argv[] = {"develop.exe", "-w", "Not_exist.txt"};
    vector<string> result;
    int r = execPreprocess(3, const_cast<char **>(argv), result);
    //cout << r << endl;
    EXPECT_EQ(r, NOT_EXIST_FILE);
}
//LACK_OF_FILE (-98)
TEST(inputerror, 3) {
    const char *argv[] = {"develop.exe", "-w"};
    vector<string> result;
    int r = execPreprocess(2, const_cast<char **>(argv), result);
    //cout << r << endl;
    EXPECT_EQ(r, LACK_OF_FILE);
}
//MORE_THAN_ONE_FILE (-97)
TEST(inputerror, 4) {
    const char *argv[] = {"develop.exe", "-w", "t1.txt", "t2.txt"};
    vector<string> result;
    int r = execPreprocess(4, const_cast<char **>(argv), result);
    EXPECT_EQ(r, MORE_THAN_ONE_FILE);
}
//NOT_EXIST_PARAMETER (-3)
TEST(inputerror, 5) {
    const char *argv[] = {"develop.exe", "-s", "IOtest1.txt"};
    vector<string> result;
    int r = execPreprocess(3, const_cast<char **>(argv), result);
    EXPECT_EQ(r, NOT_EXIST_PARAMETER);
}
//DUPLICATE_PARAMETER (-4)
TEST(inputerror, 6) {
    const char *argv[] = {"develop.exe", "-w", "IOtest1.txt", "-r", "-r"};
    vector<string> result;
    int r = execPreprocess(4, const_cast<char **>(argv), result);
    EXPECT_EQ(r, DUPLICATE_PARAMETER);
}
//MORE_THAN_ONE_CHAR (-5)
TEST(inputerror, 7) {
    const char *argv[] = {"develop.exe", "-w", "IOtest1.txt", "-h", "ab"};
    vector<string> result;
    int r = execPreprocess(5, const_cast<char **>(argv), result);
    EXPECT_EQ(r, MORE_THAN_ONE_CHAR);
}
//LACK_OF_CHAR (-6) //done
TEST(inputerror, 8) {
    const char *argv[] = {"develop.exe", "-w", "IOtest1.txt", "-h"};
    vector<string> result;
    int r = execPreprocess(4, const_cast<char **>(argv), result);
    EXPECT_EQ(r, LACK_OF_CHAR);
}
//MORE_THAN_ONE_STATE (-7)
TEST(inputerror, 9) {
    const char *argv[] = {"develop.exe", "-w", "IOtest1.txt", "-c"};
    vector<string> result;
    int r = execPreprocess(4, const_cast<char **>(argv), result);
    EXPECT_EQ(r, MORE_THAN_ONE_STATE);
}
//LACK_OF_STATE (-8) //done
TEST(inputerror, 10) {
    const char *argv[] = {"develop.exe", "-h", "r",};
    vector<string> result;
    int r = execPreprocess(3, const_cast<char **>(argv), result);
    EXPECT_EQ(r, LACK_OF_STATE);
}
//NOT_EXIST_RING_PARA (-9)
TEST(inputerror, 11) {
    const char *argv[] = {"develop.exe", "-w", "data_with_ring.txt"};
    vector<string> result;
    execPreprocess(3, const_cast<char **>(argv), result);
    int r = execCalculation(result);
    EXPECT_EQ(r, NOT_EXIST_RING_PARA);
}
//ILLEGAL_PARAMETER_VALUE (-10)
TEST(inputerror, 12) {
    const char *argv[] = {"develop.exe", "-c", "IOtest1.txt", "-h", "%"};
    vector<string> result;
    int r = execPreprocess(5, const_cast<char **>(argv), result);
    EXPECT_EQ(r, ILLEGAL_PARAMETER_VALUE);
}
//UNEXPECTED_PARAMETER (-96)
TEST(inputerror, 13) {
    const char *argv[] = {"develop.exe", "-n", "IOtest1.txt", "-r", "h"};
    vector<string> result;
    int r = execPreprocess(5, const_cast<char **>(argv), result);
    EXPECT_EQ(r, UNEXPECTED_PARAMETER);
}

//NOT_COMPATIBLE_PARAMETER (-11) //not to implement
TEST(inputerror, 14) {
    const char *argv[] = {"develop.exe", "-n", "IOtest1.txt", "-h", "a"};
    vector<string> result;
    execPreprocess(5, const_cast<char **>(argv), result);
    int r = execCalculation(result);
    EXPECT_EQ(r, NOT_COMPATIBLE_PARAMETER);
}
//HEAD_TAIL_LIMIT_SAME (-12)
TEST(inputerror, 15) {
    const char *argv[] = {"develop.exe", "-w", "IOtest1.txt", "-h", "a", "-t", "a"};
    vector<string> result;
    execPreprocess(7, const_cast<char **>(argv), result);
    int r = execCalculation(result);
    EXPECT_EQ(r, HEAD_TAIL_LIMIT_SAME);
}
//HEAD_LIMIT_CONTRADICTION (-13)
TEST(inputerror, 16) {
    const char *argv[] = {"develop.exe", "-w", "IOtest1.txt", "-h", "a", "-j", "a"};
    vector<string> result;
    execPreprocess(7, const_cast<char **>(argv), result);
    int r = execCalculation(result);
    EXPECT_EQ(r, HEAD_LIMIT_CONTRADICTION);
}

11.界面模块

本项目的界面模块使用基于MVC设计模式的PyQt框架开发,界面如下:

11.1 功能与特色
  1. 支持两种导入单词文本的方式✔

  1. 提供可交互按钮,供【选择任务】和【可选参数】;限制用户可选项,保证不出现输入参数错误。✔

  1. 在【提示信息】反馈丰富的各类异常。✔

  1. 输入相关:找不到文件;输入多个文件;输入中不存在单词;...

  1. DLL相关:加载失败;找不到DLL;...

  1. 参数相关:未指定任务;参数矛盾;...

  1. 计算模块抛出的异常:未指定-r参数但隐含环;结果超过20000行;...经各种异常操作测试GUI均正常运行,不会崩溃。

  1. 输出结果,提供【导出文件】到指定位置。✔

  1. 在【提示信息】中显示运行计时,以【毫秒】为单位。✔

  1. 支持选择DLL文件加载,方便交换模块。✔

11.2 关键实现
为什么是PyQt而非我们更熟悉的Electron+Vue?Electron为了调用DLL而引入ffi-napi和ref-napi时,我们遇到了极其离谱的错: Error in native callback at ref-napi\lib\ref.js,捣鼓了两天,在互联网上并未找到合适的解决方案。一些搜索结果表明可能是Nodejs和Electron版本太高不兼容,但我们偏好Vite带来的开发体验,排斥降版本换webpack,遂转用PyQt。 整体架构:

View: MainWindow手动切分为左中右三个QWidget,每个QWidget内部通过QGridLayout布局;

Controller: 使用信号槽机制为按钮绑定点击事件;

Model: 使用Ctypes调用DLL。

MainWindow类构造函数【抽象后】结构如下:

def__init__(self, *args, **kwargs):
    super(MainWindow, self).__init__(*args, **kwargs)
    self.initAllWidgets() # 初始化所有组件
    self.setupUI() # 设置组件大小和布局
    self.setupStyleSheet() # 加载QSS样式表
    self.loadDLL() # 加载DLL
    self.setConfigConstraint() # 设置参数约束
    self.loadConnect() # 连接信号槽

实现细节:

  1. 布局以【可选参数】模块为例

grid_2_base.addWidget(self.label_4, 5, 0, 1, 0)
grid_2_base.addWidget(self.widget_middle, 6, 0, 4, 0)
grid_2_middle = QGridLayout(self.widget_middle)
grid_2_middle.addWidget(self.label_h, 0, 0, 1, 2)
grid_2_middle.addWidget(self.comboBox_h, 0, 2)
grid_2_middle.addWidget(self.label_t, 1, 0, 1, 2)
grid_2_middle.addWidget(self.comboBox_t, 1, 2)
grid_2_middle.addWidget(self.label_j, 2, 0, 1, 2)
grid_2_middle.addWidget(self.comboBox_j, 2, 2)
grid_2_middle.addWidget(self.checkBox_r, 3, 0, 1, 3)

早期布局草图:

  1. 信号槽以【选择文件】为例:

self.pushButton_import.clicked.connect(self.handleSelectFile)
​
# 其中self.handleSelectFile:
defhandleSelectFile(self):
    options = QFileDialog.Options()
    options |= QFileDialog.DontUseNativeDialog
    file, _ = QFileDialog.getOpenFileNames(self, "选择文件", "", "文本文件(*.txt)",
                                           options=options)
    # 异常处理...
11.3 打包与发行

在使用一种很新的打包方式:https://github.com/skywind3000/PyStand

发行地址:https://gitee.com/chen-canyu/word-list-gui/releases

解压后点击WordList-GUI直接运行。

12.界面模块与计算模块对接

12.1 计算模块

定义接口,声明导出,指定编译时函数名为C风格。这样界面模块可以直接使用接口。

extern"C" {
int__declspec(dllexport) gen_chains_all(char*words[], intlen, char*result[]);
int__declspec(dllexport) gen_chain_word(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);
int__declspec(dllexport) gen_chain_char(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);
}

需要特别声明的是,有些错误只能由计算模块产生,由于我们对异常采用一致的错误码设计而非try-catch-throw设计,且接口返回值被规定不能用作错误码,对于以下错误我们做了特殊处理:

  • 对于错误NOT_EXIST_RING_PARA,即输入中有隐含环但未指定-r参数。我们将错误信息写进result[0],返回值设为0。

  • 对于错误TOO_LONG_RESULT,即结果超过20000行。我们将错误信息写进result[0],返回值不改变。对于GUI,以上错误的抛出由界面模块处理;对于CLI,由Controller输出时处理。

12.2 界面模块

加载DLL:

defloadDll(self, path=None):
    # 加载默认DLL,位于./lib/core.dll
    ifpathisNone:
        try:
            self.core_dll = ctypes.CDLL(self.default_dll)
            self.lineEdit_dll.setText(self.default_dll)
            self.widget_tips.setText("")
        exceptOSErrorase:
            self.core_dll = None
            self.widget_tips.setText(f"[Error]: 未能加载DLL:{e}")
    else:
        try:
            self.core_dll = ctypes.CDLL(path)
            self.lineEdit_dll.setText(path)
            self.widget_tips.setText("")
        exceptOSErrorase:
            self.loadDll()
            self.widget_tips.setText(f"[Error]: 未能加载指定DLL,将重新加载默认DLL:{e}")
    return

调用接口:

defcommitConfig(self):
    # Config检查与异常处理
    error_code, words = self.genWordsFromStr() # words:str
    len_w = len(words)
    c_words = (ctypes.c_char_p*len_w)()
    foriinrange(len_w):
        c_words[i] = words[i].encode('utf-8')
    c_len = ctypes.c_int(len_w)
    result = (ctypes.c_char_p*20000)()
    c_h = ctypes.c_char(alphabet[self.comboBox_h.currentIndex()].encode('utf-8'))
    c_t = ctypes.c_char(alphabet[self.comboBox_t.currentIndex()].encode('utf-8'))
    c_j = ctypes.c_char(alphabet[self.comboBox_j.currentIndex()].encode('utf-8'))
    c_r = ctypes.c_bool(self.checkBox_r.isChecked())
    ret = 0
    ifself.buttonGroup_task.checkedButton() == self.radioButton_n:
        ret = self.core_dll.gen_chains_all(c_words, c_len, result)
        # ...

输出:

# 将结果转换为Python类型
ifret>20000or (result[0] isnotNoneandret == 0):
    # 在异常处理中已说明:错误信息写回result
    res = result[0].decode('utf-8')
    # [提示信息] 打印运行失败
    self.widget_tips.setText(
        f"[Failed]: at [{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]\n 运行失败\n"
        +res)            
else:
    # [提示信息] 打印运行用时
    t = ((t_end-t_start) *1000)
    self.widget_tips.setText(
        f"[Success]: at [{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 运行成功\n用时{t}毫秒")
    # 输出运行结果
    res = [result[i].decode('utf-8') foriinrange(ret)]
    ans = str(ret) +'\n'+'\n'.join(res)
    self.textBrowser_output.setText(ans)
最后展示我们与其他组(学号20373228叶颜函&20373290刘子楠)交换核心模块与界面模块:
  • 他们的GUI和我们的模块

  • 我们的GUI和他们的模块:

13.结对过程

本次结对过程大部分时间在线下完成,线下交流并进行结对编程的过程,以下是我们线下结对编程的图片。

14.结对优缺点

结对编程是一种软件开发实践,它涉及两个程序员一起工作,共同编写代码并分享任务。我们认为的结对编程的优缺点如下:

优点:

  1. 提高代码质量:两个程序员一起编写代码可以发现和纠正彼此的错误,提高代码的质量。

  1. 减少错误:由于有两个人在工作,所以可以减少在编写代码时犯的错误,从而减少代码调试和修复的时间。

  1. 知识分享:通过结对编程,程序员可以分享他们的知识和技能,以便更好地理解和应用这些知识。

缺点:

  1. 需要更多的人力资源:结对编程需要两个程序员一起工作,这意味着需要更多的人力资源来完成任务。

  1. 可能会影响工作效率:如果两个程序员之间的合作不够良好,结对编程可能会影响工作效率,甚至导致工作延误。

  1. 可能会影响个人创造力:某些程序员可能更喜欢独立编写代码,而不是与另一个人共同编写代码。

团队优缺点分析

陈灿宇

史泽宇

优势

代码能力强,对前端非常熟悉

对cpp比较熟悉,测试比较熟悉

劣势

对cpp不太熟悉

代码能力弱

15.PSP实际花费

PSP2.1

Personal Software Process Stages

预估耗时(分钟)

实际耗时(分钟)

Planning

计划

90

90

· Estimate

· 估计这个任务需要多少时间

90

90

Development

开发

2090

2420

· Analysis

· 需求分析 (包括学习新技术)

60

60

· Design Spec

· 生成设计文档

60

30

· Design Review

· 设计复审 (和同事审核设计文档)

150

100

· Coding Standard

· 代码规范 (为目前的开发制定合适的规范)

20

30

· Design

· 具体设计

120

300

· Coding

· 具体编码

900

1100

· Code Review

· 代码复审

240

300

· Test

· 测试(自我测试,修改代码,提交修改)

540

500

Reporting

报告

170

140

· Test Report

· 测试报告

90

90

· Size Measurement

· 计算工作量

20

20

· Postmortem & Process Improvement Plan

· 事后总结, 并提出过程改进计划

60

30

合计

2350

2650

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值