项目 | 内容 |
---|---|
这个作业属于哪个课程 | https://bbs.csdn.net/forums/buaa-ase2023 |
这个作业的要求在哪里 | https://bbs.csdn.net/topics/613883108 |
我在这个课程的目标是 | 了解现代软件工程的流程、规范,以清晰高效的方法论投入实践;提升团队合作能力,在锻炼中共同进步。 |
这个作业在哪个具体方面帮助我实现目标 | 通过结对编程实践极限编程的思想,学会高效的两人合作。 |
1. 基本信息
教学班级 | 周四班 |
---|---|
组员 | 20373737陈灿宇,20373965史泽宇 |
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
Information Hiding(信息隐藏)原则指出一个模块(或类)应该只暴露出必要的信息给外部。我们将图封装为
Grpah
类,单词封装为Edge
类,输入参数封装为InConfig
和Config
类,对外提供getter和setter,保证调用者只需了解暴露的接口而不必关心细节。 -
Interface Design
Interface Design(接口设计)指出一个良好的接口一定要明确接口中每一个参数,调用者在调用时不应该产生歧义,并且调用时能快速得到结果。接口中包含的参数应该是最简单的,不应该把多个任务放在同一个接口中。我们的接口已经做到参数最小、任务单一。
-
Loose Coupling
Loose Coupling(松耦合)原则指出系统中的组件应该相互独立,互不影响。这样可以使得系统更加可靠、可维护和可扩展。在接口设计中,松耦合可以通过尽可能地减少组件之间的依赖关系来实现。
- 整体架构见UML图部分。Config和Error模块功能相似,但为了松耦合我们在CLI和core分别实现。
- Core模块内部,早期各模块存在复杂的循环依赖,我们经过重构保证了各模块的依赖关系形成DAG。
这是排查MinGW编译出的DLL不可用时做的重构,目的是对文件拓扑排序、逐个加入DLL检查是否可用。但无奈的是这样最终排查的结果是:STL不可用(?)。此后偶然发现VS工具链就能用了。
4. 计算模块的设计与实现
4.1 计算模块的设计
分析:仔细阅读结对编程文档,我们不难发现这是一个图论问题,我们不妨将这几个图论问题的解决过程一步步分解为子问题来梳理。
-
如何建图?
开始时我们想的是一个单词是一个点,每加入一个单词便要将他与相关联的所有单词加上连接关系,但是我们经过讨论发现,这样的方法会导致图中点的个数非常大,边的个数也很多,对于求解过程的速度与效率限制非常大。经过讨论交流我们发现不妨将每个字母抽象为一个点,单词则是从首字母所在点到尾字母的一条边(单词即边),这样我们成功的将图的点数化为仅有26个,将问题简单化。 -
如何存储?
- 单词类(边类):由上述的分析我们知道,一个单词就是一条边。所以我们在单词类中存储单词的两个端点,该单词的完整字符串,该单词的长度信息。
- 图类:图中共有26个点,我们在这个类中存储图的邻接列表,边集,入度列表,出度列表,每个点的自环数,每个点的自环的字母总数。其中这些信息有的是图的基本信息,有的是为了方便后续使用额外存储的附加信息。
-
如何解决6个需求
4.2 计算模块的实现
需求一:产生所有的单词链
这个问题比较简单,建好图以后,是一个dfs算法,我们利用对边染色的dfs,从每个起点出发,枚举从该起点出发的所有路径,将这些路径整理成字符串列表进行输出,简而言之就是如下的流程图。
需求二:产生单词最多的单词链
无环:对于无环的情况,首先建图,然后求出拓扑序,然后按照拓扑序进行动态规划。有效出发点被初始化为该点的自环数目,转移方程为
d
p
[
i
]
=
max
d
p
[
j
]
+
1
+
s
e
l
f
_
l
o
o
p
_
c
n
t
(
i
)
dp[i] = \max{dp[j]}+1+self\_loop\_cnt(i)
dp[i]=maxdp[j]+1+self_loop_cnt(i)
需求三:产生字母数最多的单词链
无环:对于无环的情况,首先建图,然后求出拓扑序,然后按照拓扑序进行动态规划。有效出发点被初始化为该点的自环上的字母总数,转移方程为
d p [ i ] = max d p [ j ] + w o r d _ c o u n t [ j ] + s e l f _ l o o p _ w o r d s _ c n t ( i ) dp[i] = \max{dp[j]}+word\_count[j]+self\_loop\_words\_cnt(i) dp[i]=maxdp[j]+word_count[j]+self_loop_words_cnt(i)
需要注意的是,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公式略有变化,以最长单词数为例。
d p [ i ] = d p [ j ] + 1 + s e l f l o o p c n t [ i ] + m a x D i s t [ m − > i ] dp[i] = dp[j]+1+ self_loop_cnt[i]+maxDist[m->i] dp[i]=dp[j]+1+selfloopcnt[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 是一种由 Bertrand Meyer 提出的面向对象设计原则,它主张在程序中使用契约来描述模块之间的交互。契约包括前置条件、后置条件和不变式。前置条件指定了调用者必须满足的条件;后置条件指定了被调用者必须满足的条件;不变式则指定了类或模块在运行过程中保持不变的条件。通过在程序中显式地定义这些契约,开发者可以更好地理解程序行为,并能够在代码中直接验证它们。
-
Code Contract 是 Microsoft .NET 平台上实现的一种代码契约机制,它主要由三个组件组成:前置条件、后置条件和不变式。开发者可以使用 Code Contract 中提供的语法来定义这些契约。Code Contract 还提供了一组 API,可以在运行时检查这些契约,从而提高程序的健壮性和可靠性。
-
优点:
- 显式地定义契约可以帮助开发者更好地理解程序行为,从而能够更加容易地调试和维护程序。
- 通过定义契约,开发者可以更加容易地发现潜在的错误和问题,从而提高程序的健壮性和可靠性。
- 使用契约可以帮助开发者更好地规范代码的使用方式,从而提高代码的可读性和可维护性。
-
缺点:
- 定义和验证契约需要额外的开销,这可能会降低程序的性能。
- 如果契约定义得不好,可能会导致程序逻辑复杂化,从而降低代码的可读性和可维护性。
- 契约本身可能会出现错误,从而导致程序的行为不可预测,这需要开发者对契约进行仔细的验证和测试。
在结对作业中,我们尝试在部分代码的注释中实现简单的DBC,尤其是不同模块间的交互部分,对复杂形参(如类型为vector<vector<string>> &result
等)在函数中的行为注释说明。
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(int argc, char *argv[]) {
std::vector wordList;
int r = execPreprocess(argc, argv, wordList);
if (r < 0) {
return r;
}
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 功能与特色
-
支持两种导入单词文本的方式✔
-
提供可交互按钮,供【选择任务】和【可选参数】;限制用户可选项,保证不出现输入参数错误。✔
-
在【提示信息】反馈丰富的各类异常。✔
- 输入相关:找不到文件;输入多个文件;输入中不存在单词;…
- DLL相关:加载失败;找不到DLL;…
- 参数相关:未指定任务;参数矛盾;…
- 计算模块抛出的异常:未指定-r参数但隐含环;结果超过20000行;…
经各种异常操作测试GUI均正常运行,不会崩溃。
-
输出结果,提供【导出文件】到指定位置。✔
-
在【提示信息】中显示运行计时,以【毫秒】为单位。✔
-
支持选择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() # 连接信号槽
实现细节:
- 布局
以【可选参数】模块为例
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)
早期布局草图:
- 信号槽
以【选择文件】为例:
self.pushButton_import.clicked.connect(self.handleSelectFile)
# 其中self.handleSelectFile:
def handleSelectFile(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[], int len, char *result[]);
int __declspec(dllexport) gen_chain_word(char *words[], int len, char *result[], char head, char tail, char reject, bool enable_loop);
int __declspec(dllexport) gen_chain_char(char *words[], int len, char *result[], char head, char tail, char reject, bool enable_loop);
}
需要特别声明的是,有些错误只能由计算模块产生,由于我们对异常采用一致的错误码设计而非try-catch-throw设计,且接口返回值被规定不能用作错误码,对于以下错误我们做了特殊处理:
- 对于错误NOT_EXIST_RING_PARA,即输入中有隐含环但未指定-r参数。我们将错误信息写进result[0],返回值设为0。
- 对于错误TOO_LONG_RESULT,即结果超过20000行。我们将错误信息写进result[0],返回值不改变。
对于GUI,以上错误的抛出由界面模块处理;对于CLI,由Controller输出时处理。
12.2 界面模块
加载DLL:
def loadDll(self, path=None):
# 加载默认DLL,位于./lib/core.dll
if path is None:
try:
self.core_dll = ctypes.CDLL(self.default_dll)
self.lineEdit_dll.setText(self.default_dll)
self.widget_tips.setText("")
except OSError as e:
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("")
except OSError as e:
self.loadDll()
self.widget_tips.setText(f"[Error]: 未能加载指定DLL,将重新加载默认DLL:{e}")
return
调用接口:
def commitConfig(self):
# Config检查与异常处理
error_code, words = self.genWordsFromStr() # words:str
len_w = len(words)
c_words = (ctypes.c_char_p * len_w)()
for i in range(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
if self.buttonGroup_task.checkedButton() == self.radioButton_n:
ret = self.core_dll.gen_chains_all(c_words, c_len, result)
# ...
输出:
# 将结果转换为Python类型
if ret > 20000 or (result[0] is not None and ret == 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') for i in range(ret)]
ans = str(ret) + '\n' + '\n'.join(res)
self.textBrowser_output.setText(ans)
最后展示我们与其他组(学号20373228叶颜函&20373290刘子楠)交换核心模块与界面模块:
- 他们的GUI和我们的模块
- 我们的GUI和他们的模块:
13. 结对过程
本次结对过程大部分时间在线下完成,线下交流并进行结对编程的过程,以下是我们线下结对编程的图片。
14. 结对优缺点
- 优点:
- 提高代码质量。两人一起审查代码、测试和调试,从而减少错误和漏洞,减少Debug耗时;
- 增强团队合作和沟通,提高团队的工作效率和凝聚力;
- 提高代码的可读性和可维护性;
- 缺点:
- 两人需要共同完成一个任务,存在时间、空间的协调问题,这可能会导致进度变慢;
- 综合两人的意见,可能导致过度设计,增加代码的复杂性;
- 如果两个人意见不同,可能会产生冲突,这会影响他们的工作效率和合作关系。
陈灿宇 | 史泽宇 | |
---|---|---|
优势 | 熟悉多种前端方案;擅于优化;代码风格和规范好。 | 工程架构设计优秀;熟悉工具链;测试关注细节,精益求精。 |
劣势 | 算法设计能力较弱 | 代码能力稍弱 |
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 |