软件工程第三次作业 结对项目-最长英语单词链
软件工程第三次作业 结对项目-最长英语单词链
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2023北航敏捷软件工程 |
这个作业的要求在哪里 | 结对项目-最长英语单词链 |
我在这个课程的目标是 | 学习现代软件开发模式与流程,提高个人能力与团队写作能力 |
这个作业在哪个具体方面帮助我实现目标 | 通过参与结对编程,在实践与合作中领悟软件工程真谛 |
1. 项目地址
github地址:pangrj/LongestWordChain: 2023 SoftwareEngineering Course in BUAA (github.com)
2. PSP表格
见第15节
3. 接口设计方法
3.1. Information Hiding
信息隐藏很像面向对象的开发思路,是将计算机程序中最有可能发生变化的设计决策隔离开来的原则,从而保护程序的其他部分在设计决策发生变化时不被广泛修改。这种保护包括提供一个稳定的接口,保护程序的其余部分不受实现(其细节可能会改变)的影响。
本次作业中,从文件的角度实现了信息隐藏,计算核心对外只提供了三个接口,隐藏了这些接口的具体实现,外部函数只能通过调用这些已经规定好的接口来计算,而不知道这些接口的具体实现方式。
3.2. Interface Design
接口用于模块之间的交互。精心设计的接口可以避免“抽象泄露”,提升编码和运行效率,给人良好的开发和使用体验。
本次我们按照课程组定义的接口进行设计,也方便后续松耦合与其他组进行交换测试。
int gen_chains_all(char* words[], int len, char* result[]);
int gen_chain_word(char* words[], int len, char* result[], char head, char tail, char reject, bool enable_loop);
int gen_chain_char(char* words[], int len, char* result[], char head, char tail, char reject, bool enable_loop);
3.3. Loose Coupling
松耦合的设计理念解开了模块之间的紧密联系,既方便对核心模块进行集中测试,在出问题时也只用修改对应的模块,不会牵扯到整个项目。
4. 计算模块实现
首先是图的结构:每一个单词Word作为图的节点,对两个单词a、b,若a的尾字母与b的首字母相同,则在ab之间连一条a指向b的有向边。
计算模块core实现了如下三个API:
int gen_chain_all(char* words[], int len, char* result[]);
int gen_chain_word(char* words[], int len, char* result[], char head, char tail, char reject, bool enable_loop);
int gen_chain_char(char* words[], int len, char* result[], char head, char tail, char reject, bool enable_loop);
其中,words存放输入的单词数组,len为单词数组长度,result用来存放结果单词链,head为规定单词链首字母,tail为规定单词链尾字母,reject为规定单词不允许首字母,以上三个为 '\0' 时表示不做要求,且初始值为 '\0'。enable_loop用来约束是否允许存在环。
我们采用拓扑排序的方式判断有无环,同时求出拓扑序用于后续。
求全部单词链与带环情况采用暴力DFS运算,若不带环即图为DAG,此时采用动态规划快速求解。dp[i]表示以i节点为最后单词的单词链的最长长度,用rec来记录前驱结点。则初始化dp[i]为点权,当word情况下为1,char情况下为单词长度。状态转移方程为 dp[j]=max(dp[j], dp[i] + weight[j]),其中i为j的前驱结点。
head、tail、reject的处理方式则使用noUse数组进行标记。为了最好的提高性能,reject在最开始便处理,标记相应单词noUse为true,相当于降低图的度;head则在开始调用函数时处理,减少了DFS的次数或动态规划的开始点;tail则只能在计算过程中处理。
5. 编译结果
6. 计算模块UML图
我们采用面向过程的求解思路,以下是我们核心计算模块的函数调用UML图
7. 计算模块性能
我们采用运算较为复杂的-c -r来进行测试
可以看到dfs占用了大量的时间,于是我们针对dfschar进行优化,将可以放到循环外的语句外提,减少了循环节运算语句的数量。
8. Design by Contract 与 Code Contract
Design by Contract:要求软件设计者为软件组件定义正式的,精确的并且可验证的接口,这样,为传统的抽象数据类型又增加了先验条件、后验条件和不变式。
- 优点:
- 因为程序的行为已经被规定好了,开发者只需要根据契约进行开发,不需要考虑整个项目,使得开发更简单也更专注
- 因为程序的行为必须要符合契约,所以只需要根据契约设计单元测试即可,这样方便进行测试
- 缺点:
- 契约必须要设计周全,否则得到的程序一定是错误的,并且问题难以靠单元测试发现,这只会浪费设计者和开发者的时间
- 设计契约需要花费很多时间,并且设计时也不一定能考虑到实际开发中可能遇到的问题,可能开发效率不如直接进行开发
本次作业中,我们对core的三个API的设计与参数进行严格定义,但是并未采取形式化的表述,而是通过更为通俗的表述。并且通过维护文档function design.md 进行沟通。
Code Contract是VS提供的一种与语言无关的代码契约插件。能够帮助进行运行时检查、静态检查与文档生成。优点是可以帮助开发人员履行契约,避免了多余精力的耗费。我们编码过程主要在Clion完成,没有使用此查件。
9. 计算模块单元测试
我们使用Google的gtest作为单元测试的模板,分别对计算核心和异常处理进行了测试。
9.1 覆盖率
9.2 计算核心测试与结果
我们给计算核心设计了24个单元测试并全部正确通过
为了方便测试,我将重复的部分提取成函数方便快速设计测试
void test_gen_chain_word(const char* words[], int len, const char* ans[], int ans_len, char head, char tail, char reject, bool enable_loop) {
char** result = (char**)malloc(10000);
int out_len = gen_chain_word(words, len, result, head, tail, reject, enable_loop);
ASSERT_EQ(ans_len, out_len);
for (int i = 0;i < ans_len;i++) {
if (result != nullptr) ASSERT_EQ(strcmp(ans[i], result[i]), 0);
}
free(result);
}
对应的测试单元
TEST(gen_chain_word, example_w) {
const char* words[] = {"algebra", "apple", "zoo", "elephant", "under", "fox", "dog", "moon", "leaf", "trick", "pseudopseudohypoparathyroidism"};
const char* ans[] = {"algebra", "apple", "elephant", "trick"};
test_gen_chain_word(words, 11, ans, 4, 0, 0, 0, false);
}
9.3 异常测试与结果
我们给所有异常情况设计了14个单元测试,覆盖了所有异常,详细情况见下一章节
10. 计算模块异常处理
本程序设计了如下14种异常
异常情况 | 具体异常描述 | 报错输出 |
---|---|---|
没有正确输入参数 | 检测到-n, -w, -c, -h, -j, -r以外的参数 | "this argument is invalid!" |
重复输入参数 | 同一个参数检测到两遍 | "arg: " + arg + " repeat!" |
-n 与其他参数冲突 | 检测到-n与其他参数一同使用 | "-n can't be used with other arguments!" |
-w与-c冲突 | 检测到-c与-w一同使用 | "-w can't be used with -c!" |
-h, -t, -j后续参数错误 | 检测到-h, -t, -j后续参数不是单个字母 | "arg: " + arg + " following argument is wrong!" |
-h, -t, -j后续参数缺失 | 检测到 -h, -t, -j后续没有参数 | "arg: " + arg + " missing following argument!" |
功能参数缺失 | 没有检测到-n, -w, -c | "need -n or -w or -c!" |
没有获取到合法的文件名 | 没有文件名或者文件名不以.txt结尾 | "don't get valid filename!" |
重复输入文件名 | 检测到多个文件名 | "filename is repeat!" |
打开文件失败 | 该文件不存在或不可读 | "opening file fail!" |
输入超过10000个词 | 无环情况下输入超过了10000个词 | "words are more than 10000!" |
有环情况下输入超过100个词 | 有环情况下输入超过了100个词 | "words are more than 100 when chains has circle!" |
输出超过20000行 | result结果大于20000 | "results are more than 20000 chains!" |
在不允许有环的情况下输入有环图 | 没有检测到-r但是图是有环图 | "the chain has circle without -r!" |
以以下几组异常处理逻辑与对应的单元测试为例
该段代码包含了 -h, -t, -j后续参数错误, -h, -t, -j后续参数缺失, 重复输入参数这三个异常的抛出过程
if (is_h == false) {
is_h = true;
int i_next = i + 1;
if (i_next == argc) {
throw invalid_argument("arg: " + arg + " missing following argument!");
}
string arg_next = argv[i_next];
if (arg_next.length() == 1 && isalpha(arg_next[0])) {
h_char = tolower(arg_next[0]);
} else {
throw invalid_argument("arg: " + arg + " following argument is wrong!");
}
i++;
} else {
throw invalid_argument("arg: " + arg + " repeat!");
}
在main函数中捕获异常并处理
int main(int argc,char* argv[]) {
try {
main_serve(argc, argv);
} catch (invalid_argument const& e) {
cerr << e.what() << endl;
}
catch (logic_error const& e) {
cerr << e.what() << endl;
}
catch (runtime_error const& e) {
cerr << e.what() << endl;
}
return 0;
}
对应的单元测试
10.1 -h, -t, -j后续参数错误
TEST(main_serve, example_h_wrong) {
try{
const char* args[] = {"Wordlist.exe", "-w", "-h", "aa", "test.txt"};
main_serve(5, args);
} catch(invalid_argument const &e){
ASSERT_EQ(0, strcmp("arg: -h following argument is wrong!", e.what()));
return;
}
}
10.2 -h, -t, -j后续参数缺失
TEST(main_serve, example_h_miss) {
try{
const char* args[] = {"Wordlist.exe", "-w", "-h"};
main_serve(3, args);
} catch(invalid_argument const &e){
ASSERT_EQ(0, strcmp("arg: -h missing following argument!", e.what()));
return;
}
}
10.3 重复输入参数
TEST(main_serve, example_repeat_arg) {
try{
const char* args[] = {"Wordlist.exe", "-n", "-n", "test.txt"};
main_serve(4, args);
} catch(invalid_argument const &e){
ASSERT_EQ(0, strcmp("arg: -n repeat!", e.what()));
return;
}
}
11. 界面模块设计
界面模块采用vue设计开发,所采用的技术栈如下:
- Vue 3
- Element - Plus
- 注册表脚本reg文件
- XMLHttpRequest
设计风格以简约为主,效果如下:
功能实现:
-
输入可以通过文件导入,也可以手动输入,且两者可同步使用,导入文件后可自动更新输入框。展示如下:
-
功能设定选择通过按钮实现,且选择首字母限制后方可进行输入。基本异常情况也会给出错误提示:
-
结果展示如下:
12. 界面模块与计算模块对接
对接我们采用的vue+cli的方式,熟悉分析选项异常,随后处理好输入输出后,调用程序。调用程序通过html中的a元素的href属性,首先通过reg文件注册url,后通过href调用本地exe文件,达到调用程序的目的。
附加任务- 交换核心:
与我们交换核心的小组学号为20373284、19375201。
运行效果如下:
且我们的API设计完全相同,故较容易实现互换。
13. 结对过程
主要在新主楼F座3楼进行结对编程。
14. 结对编程
14.1. 优点
- 结对编程能使两人互补,弥补自己不会的技术,合力做出高质量产品
- 结对编程还能起到互相督促的作用,防止划水
- 结对编程两人审视一份代码,也更容易发现代码的问题,快速debug
14.2. 缺点
- 结对编程过程中需要不断与队友沟通,有时会导致一定的效率下降
- 结对编程会带来一定的压力
- 在校内开展结对,有场地、时间的限制
14.3. 队员优缺点分析
本次结对的队员为庞睿加同学。
队员 | 优点 | 缺点 |
---|---|---|
庞睿加 | 1. 对基本算法熟悉 2. 对Vue前端相对熟练 |
1. 对于配置环境工作很头疼 2. 高级算法知识相对不够充分, 3. 当陷入难题后工作效率降低 |
朱彦安 | 1.学习新工具较快 2.测试较为全面,发现bug与debug相对熟练 |
1.不懂前端知识 2.比较拖沓 |
15. PSP表
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 60 |
· Estimate | · 估计这个任务需要时间 | 60 | 60 |
Development | 开发 | 2250 | 2300 |
· Analysis | · 需求分析(包括学习新技术) | 300 | 400 |
· Design Spec | · 生成设计文档 | 60 | 60 |
· Design Review | · 设计复审 | 120 | 120 |
· Coding Standard | · 代码规范 | 30 | 30 |
· Design | · 具体设计 | 120 | 120 |
· Coding | · 具体编码 | 900 | 1200 |
· Code Review | · 代码复审 | 240 | 240 |
· Test | · 测试(自我测试,修改代码,提交修改) | 300 | 420 |
Reporting | 报告 | 180 | 180 |
· Test Report | · 测试报告 | 120 | 120 |
· Size Measurement | · 计算工作量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 30 | 30 |
合计 | 2490 | 2760 |