项目 | 内容 |
---|---|
这个作业属于哪个课程 | 北航软工 |
这个作业的要求在哪里 | 最长英语单词链 |
我在这个课程的目标是 | 学会团队协作 |
这个作业在哪个具体方面帮助我实现目标 | 组队项目,使用vs |
0 项目信息
教学班级 | 项目地址 |
---|---|
周四下午 | pair-program |
成员:徐子航,徐楚鸥
预计耗时
psp2.1 | 预估耗时(min) | 实际耗时(min) | |
---|---|---|---|
Planning | 计划 | 90 | 60 |
. Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | 600 | 900 |
. Analysis | · 需求分析 (包括学习新技术) | 60 | 50 |
. Design Spec | · 生成设计文档 | 50 | 60 |
.Design Review | · 设计复审 (和同事审核设计文档) | 30 | 20 |
.Coding Standard | · 代码规范 (为目前的开发制定合适的规范 | 60 | 30 |
.Design | · 具体设计 | 60 | 50 |
.Coding | · 具体编码 | 400 | 300 |
.Code Review | · 代码复审 | 60 | 90 |
.Test | · 测试(自我测试,修改代码,提交修改) | 200 | 180 |
Reporting | 报告 | 120 | 200 |
.Test Report | · 测试报告 | 60 | 40 |
.Size Measurement | · 计算工作量 | 30 | 20 |
.Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 1880 | 2060 |
1 接口设计
1.1 Information Hiding
书中提到的接口设计中要求的Information hiding是为了模块内的主要功能和函数不暴露在外,否则可能会带来安全问题和让程序变得更加复杂。所以有效地隐藏信息和核心函数是十分重要的。所以我们总共留下了4个接口,分别是用来处理核心功能的process()
,用于和GUI交互的三个接口,并且前后端交换数据并不会通过process()
接口,而是将process()
函数封装在三个单独功能的接口里。除此之外,对于不能被外界访问的数据用private
,对于其他可以被外界访问的数据用public
。这样
1.2 Interface Design
对于接口设计的理念是,一个接口就完成一个特定的任务,要对接口也进行模块化,对于其他功能或者是信息在这个接口是不可以接入的,只有找到专用的接口才能访问内部数据,这样可以使得编程更加模块化,系统化,有效防止信息泄露和其他情况。在此次结对编程中我们对外留下了process(char *wordList[], char *result[], int len, int type, bool letterSum, char head, char tail, char j)
,这是一个核心函数。通过这个接口就可以防止内部的信息泄露,较好地完成了模块的封装,也保障了信息的安全。
下面是具体的接口设计,
int gen_chain_word(char *words[], int len, char *result[], char head, char tail, char forbid, bool enable_loop);
int gen_chains_all(char *words[], int len, char *result[]);
int gen_chain_char(char *words[], int len, char *result[], char head, char tail, char forbid, bool enable_loop);
还有核心模块的接口:
int process(char *wordList[], char *result[], int len,
int type, bool letterSum, char head, char tail, char forbid)
1.3 Loose Coupling
结合书中的松耦合的描述,指的是要将功能聚合在一个cpp或者函数内,然后对外交互信息的时候要实现松耦合,尽可能地将要替换一个功能或者应用到别的场景的时候能够最小化要解耦的地方。此次结对编程我们将核心计算部分集中在core.cpp
这个文件中,通过三个既定的接口与GUI/CLI进行交互。这样就能实现Loose Coupling了。这样也方便和其它组别进行互换测试。
2 接口
2.1 接口设计
对于既定的接口,本质都是通过实现process()
接口实现的,根据type
,head
,tail
,forbid
,enable_loop
等参数实现gen_chain_word
,gen_chains_all
,gen_chain_char
这个三个接口。
对于process()
这个接口而言,是先通过init()
函数初始化,若argv
中带有-j
那么直接在wordlist
中删除头字母为限制字母的单词。再经过排序sort()
和erase()
之后,根据type
类型判断是-n
,还是-w
,-c
。调用图如下:
对于process()
而言,通过wordlist和参数构建图,对于允许单词环的,首先检查wordlist中是否有单词环,如果有,但是又参数中没有-r
则应该报错,并且由于是NP-hard
所以没有最优的性能,暴力求解。但是如果没有单词环,则应该构件DAG
之后DP
求解。
gen_chains_all()
:遍历所有的单词进行DFS,求最长链,对于超过要求20000
长度的则按照异常处理gen_chain_word()和gen_chain_char()
只是在构件图的时候边的权重不同而已,在算法部分会详细讲解.
2.2 接口实现
2.2.1 输入预处理
首先我们对输入的单词数量进行去重,然后对于不满足条件的输入报异常,例如小于两个单词的则不需要求解。对于-j
参数我们的处理是在预处理的时候对对应的首字母为制定字母的单词删去,即不参与构图。
2.2.2 构建有权有向图
对于单词a..b
,则创建a和b节点,然后有一条从a到b的有向边。根据需求,如果求的是最多单词数量,则边的权重为1,否则为单词的字母数。以此类推构建一个有向图,此方法的时间复杂度为O(n^2)
.
我们将其划分为两种情况,其一为无隐含的单词环,其二是有隐含的单词环。对于第一种情况,我们可以使用DP求解。在DP的过程中要纪录每个点作为起始点的最长链,最后就可求得最长链的长度了。
获得拓扑序列的目的是对于那些有-h
或者-t
的情况则需要重构图,从而使得最后的重构图能够达到限制条件,时间复杂度为O(n^2+m)
。
对于有单词环的情况,由于没有最优解,所以我们采用getRingChain()
函数进行DFS暴力搜索。
编译无警告的截图:
3 UML图
此次编程项目我们总共写了三个文件,分别是:
main.cpp
用于处理输入,例如单词去重,对单词流的预处理,以及抛出部分异常等core.cpp
用于核心计算功能,包括建图,DFS,DP等核心功能集成在这个文件中,其中getScc()
用于得到所有的额强连通分量,dfsAll()
用于深搜遍历节点core.h
用于对外和GUI/CLI的接口
4 性能改进
首先对于有向无环图中的参数-w
和-c
,由于我们采用了拓扑排序和DP的放大求解,所以能够加快求解速度。对于-j
我们的做法是在单词输入预处理的时候把首字母为指定字母的单词直接删去,不参与构建有向图,所以这也可以有助于性能的改进。
- 简单样例
- 复杂样例
可以看到在出现单词环的时候,主要在dfsRing()
和getRingChain
两个的函数上,
但是对于一些更为复杂的样例,加上具有-r
的含有单词环的情况,程序性能就会急剧下降。
5 Design by Contract & Code Contract
Design by Contract 是契约式设计,例如大二下学期上的OO课程中的JML
就是契约式设计。这种设计的好处在于接口具有不变性,方便人们调试和测试,但是确定契约也是一个繁杂和耗时的磨合的过程.
Code Contract 指的是在过程中涉及到的规则约束和编译检查相关的部分.
本次作业我们对契约式设计体现在接口的前后端规定,即每个方法都要满足前置条件、后置条件和不变式以及其他的状态约束条件.我们在构建有向图的时候就是所谓的前置条件,不变式就是在process()
的过程不破坏图的结构,后置条件就是输出符合要求。
6 测试
测试我们是在Visual Studio2017
内进行调试,后用2022Community
版本查看了代码覆盖率。我们主要分为两个测试程序,分别是unitTest.cpp
和mainTest.cpp
,第一个是用于单元测试,包括参数的测试等,第二个用于主要函数的异常测试。
构造测试数据的思路是:穷尽不同组合的参数,对于不可能出现的参数组合,例如-n -r
要报异常,下面是单元测试中的两个样例,其中test_gen_chain_word
是对gen_chain_word
这个接口进行测试。对于三个接口我们都构造了不同的数据。
unitTest.cpp
的部分代码:
/*
-w -t -j
*/
TEST_METHOD(test_w_t_j) {
char* words[] = { "algebra", "apple", "zoo", "elephant", "under", "fox", "dog", "moon", "leaf", "trick", "pseudopseudohypoparathyroidism" };
char* ans[] = { "elephant", "trick" };
test_gen_chain_word(words, 11, ans, 2, 0, 'k', 'a', false);
}
/*
-w -h -t -j
*/
TEST_METHOD(test_w_h_t_j) {
char* words[] = {"asdac","jiasdnc","qweudasunc","casdowdn","nasdnw" ,"nqwer","rhusad","radqwt","tqwdbf","dqwdf"};
char* ans[] = { "qweudasunc","casdowdn" ,"nqwer" ,"rhusad","dqwdf" };
test_gen_chain_word(words, 10, ans, 5,'q', 'f', 't', false);
}
代码覆盖率
7 异常处理
-
缺少功能参数
Missing function option, please choose -n or -w or -c
功能参数包括 -n -w -c至少要有其一
TEST_METHOD(missing_arguments)
{
try {
char * args[] = { "C:\\Users\\Q\\Desktop\\test.txt" };
main(1, args);
}catch(invalid_argument const &e){
Assert::AreEqual(0, strcmp("Missing function option, please choose -n or -w or -c", e.what()));
return;
}
Assert::Fail();
}
-
功能参数不兼容
Function option -w, -n and -c conflict
-w -n -c 不兼容
TEST_METHOD(duplicated_wnc) {
try {
char * args[] = { "-w", "-n","-c","C:\\Users\\Q\\Desktop\\test.txt" };
main(4, args);
}
catch (invalid_argument const &e) {
Assert::AreEqual(0, strcmp("Function option -w, -n and -c conflict", e.what()));
return;
}
Assert::Fail();
}
-
未定义参数
Undefined option '-x'
出现未定义参数 -x等
TEST_METHOD(undefined_x) {
try {
char * args[] = { "-o", "C:\\Users\\Q\\Desktop\\test.txt" };
main(2, args);
}
catch (invalid_argument const &e) {
Assert::AreEqual(0, strcmp("Undefined option '-x'", e.what()));
return;
}
Assert::Fail();
}
-
参数格式错误
Argument of option '-x' should be a single alphabet
-h -t -j 后的指令不是单个字母, (单个字符 or 单词)
TEST_METHOD(argument_pattern) {
try {
char * args[] = { "-h","word", "C:\\Users\\Q\\Desktop\\test.txt" };
main(3, args);
}
catch (invalid_argument const &e) {
Assert::AreEqual(0, strcmp("Argument of option '-x' should be a single alphabet", e.what()));
return;
}
Assert::Fail();
}
-
-h与-j不兼容
Argument of -h and -j conflict. No answer
-n 与 附加参数 不连用(不要求)
-h 与 -j 指定字母一致则冲突
TEST_METHOD(conflict_hj) {
try {
char * args[] = { "-h","a", "-j","a","C:\\Users\\Q\\Desktop\\test.txt" };
main(5, args);
}
catch (invalid_argument const &e) {
Assert::AreEqual(0, strcmp("Argument of -h and -j conflict. No answer", e.what()));
return;
}
Assert::Fail();
}
-
不支持-n与附加参数连用
-n should be used independantly
-n 与 附加参数 不连用
TEST_METHOD(independent_n) {
try {
char * args[] = { "-n","-r","C:\\Users\\Q\\Desktop\\test.txt" };
main(3, args);
}
catch (invalid_argument const &e) {
Assert::AreEqual(0, strcmp("-n should be used independantly", e.what()));
return;
}
Assert::Fail();
}
-
文件不存在
Can not find file
输入文件不存在
TEST_METHOD(file_notfind) {
try {
char * args[] = { "-n","C:\\Users\\Q\\Desktop\\fileee.txt" };
main(2, args);
}
catch (invalid_argument const &e) {
Assert::AreEqual(0, strcmp("Can not find file", e.what()));
return;
}
Assert::Fail();
}
-
文件不合法
Wrong file format
输入文件格式不合法
TEST_METHOD(wrong_fileformat) {
try {
char * args[] = { "-n","wrong_fileformat.txt" };
main(2, args);
}
catch (invalid_argument const &e) {
Assert::AreEqual(0, strcmp("Wrong file format", e.what()));
return;
}
Assert::Fail();
}
-
隐含单词环
Ring dectected without -r option
没有-r的情况下包含了单词环
TEST_METHOD(ring_detect) {
char* words[101] = { "fddsu", "uasdasf", "ugfl", "laght", "adbon", "tasdu" };
char* result[101];
for (int i = 0; i < 101; i++)
{
result[i] = (char*)malloc(sizeof(char) * 601);
}
try {
gen_chain_word(words, 6, result, 0, 0,0, false);
}
catch (invalid_argument const &e) {
Assert::AreEqual(0, strcmp("Ring dectected without -r option", e.what()));
return;
}
for (int i = 0; i < 101; i++)
{
free(result[i]);
}
Assert::Fail();
}
-
单词链数量过多
Too many word chains
超过20000
TEST_METHOD(wordlen_overflow) {
try {
char * args[] = { "-n","wordlenoverflow.txt" };
main(2, args);
}
catch (invalid_argument const &e) {
Assert::AreEqual(0, strcmp("Too many word chains", e.what()));
return;
}
Assert::Fail();
}
8 GUI界面
我们采用的是QT进行设计。最开始我们采用的是visual studio2017
中的C# windows进行设计,但是由于不懂如何写C++ wrapper,且C#和C++一些类型转换较为复杂,整了很久的char* result[]
的数据传递,但是一直都是null
,所以最后就转用了QT5.12.
开始分析按钮对应:
void Widget::on_begin_clicked()
{
myTimer->setInterval(100);
myTimer->start(100);
startCalc();
myTimer->stop();
}
其中的startCalc()
就是核心计算部分,调用DLL进行计算,对于异常具有输出处理功能。在点击了开始分析
按钮的时候就开始计时,计算结束的时候停止计时。由于对于一些简单的案例所需时间较短,所以为了保留两位数之后数会很小。
与core.dll
对接:
-
基本界面:
-
导入文件:
-
导出文件:
-
正常处理:
-
异常处理:
9 杰对讨论
优缺点
本人:
- 优点:
1.积极沟通,避免了额外的时间开销
2.代码编写规范,阅读时没有障碍0
3.善于前端制作,在前后端对接出现问题时迅速完成重构 - 缺点:
1.不熟悉vs,qt,上手很慢
同伴:
- 优点:
1.积极讨论,善于沟通
2.比较有生产力
3.认真学习之前没接触过的软件缺点: - 缺点:
对报告比较头疼,第一次接触c++项目启动阶段遇到了一些困难
10 互换模块
我们的互换小组是:
20373080 庞睿加,20373384 朱彦安
我们通过互换core.dll
模块进行,我们统一的接口和课程组要求的接口相同.
我们采用了我们的测试模块和他们的core.dll进行测试,没有出现问题,如下是截图:
11 总结
这次两周的结对编程我学习到了很多.首先,我学会了如何进行合理地分配两人的任务,如何有效地沟通和高效地协作.其次,之前从来没有碰过vs,qt软件和c#等编程语言,所以上手很慢,印象深刻的是在写GUI可视化的时候,我最开始用的c#来写,但是由于c++和c#之间的类型的转换不太清楚,对c++ wrapper啥的不太了解,所以最后改用了qt,由于qt也是用c++,所以方便了很多.最后,很感谢结对的队友,十分负责,能够很及时地交流和沟通!