目录
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2023北航敏捷软件工程社区 |
这个作业的要求在哪里 | 结对项目-最长英语单词链 |
我在这个课程的目标是 | 体验规范软件开发流程,积累团队合作经验,同时积累项目经验,提高自身竞争力 |
这个作业在哪个具体方面帮助我实现目标 | 对结对编程项目有了更深的了解 |
1.在文章开头给出教学班级和可克隆的 Github 项目地址
- 教学班级:周四下午
- 队友:19375450-张峻源
- 项目地址:https://github.com/LaLune47/-Longest-English-word-chain
2.开始实现程序之前的 PSP 表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
---|---|---|
Planning | 计划 | 50 |
· Estimate | · 估计这个任务需要多少时间 | 50 |
Development | 开发 | 1700 |
· Analysis | · 需求分析 (包括学习新技术) | 150 |
· Design Spec | · 生成设计文档 | 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 |
· Design | · 具体设计 | 200 |
· Coding | · 具体编码 | 600 |
· Code Review | · 代码复审 | 300 |
· Test | · 测试(自我测试,修改代码,提交修改) | 300 |
Reporting | 报告 | 150 |
· Test Report | · 测试报告 | 60 |
· Size Measurement | · 计算工作量 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 |
合计 | 1900 |
3.接口设计理念与实践
-
信息隐藏(Infromation Hiding):信息隐藏是将敏感的或外部无需访问的信息封装在自身内部,使得外部不可见此类信息。我们的项目的core中,对图相关算法封装了Graph类,对字符串处理的算法封装了CharConverter类,对真正的core计算调用接口封装了WordChainCoreInterface类,外部不可见,使得程序结构更加清晰
-
接口设计(Interface Design):接口设计要求明确接口职责并实现职责的单一性,一个良好的接口及接口文档需要明确接口参数,避免调用歧义。接口设计决定了模块之间沟通的效率和效果。
-
松耦合(loose coupling):松耦合是指模块之间联系较少(尽量仅通过接口连接),每个模块内部的修改不会互相影响。
-
实践:参照课程组提供的接口,我们在代码中规定了核心计算模块core的对应-w -c -n三个功能的接口,实现了测试模块、core计算模块和GUI模块的独立封装
4.计算模块接口的设计与实现
-
外部接口实现:为了方便后续的交叉测试,我们直接使用了课程组建议的外部接口
int gen_chains_all(char* words[], int len, char* result[]); // 需求1:函数返回所有符合定义的单词链(因不要求和其他参数一起使用,故无其他参数) int gen_chain_word(char* words[], int len, char* result[], char head, char tail, char disallowed_head, bool enable_loop); // 需求2:计算最多单词数量的单词链 int gen_chain_char(char* words[], int len, char* result[], char head, char tail, char disallowed_head, bool enable_loop); // 需求3 计算最多单词数量的字母链
-
words: 全小写单词列表
-
len:单词列表长度
-
result:储存结果的单词链
-
head: 首字母,不要求则传入
NULL
/0
-
tail: 尾字母,不要求则传入
NULL
/0
-
disallowed_head: 单词链中不允许出现的首字母,不要求则传入
NULL
-
enable_loop:
true
时允许输入单词文本中含有隐链 -
返回值为单词链个数/单词链中单词的个数
-
-
接口实现步骤
- 单词表处理:调用
CharConverter::ReadFromBytePtrArray
将单词转换为更加方便处理的vector<string>
,使用WordMap::Build
将单词列表映射到int
,便于图算法的开展。 - 建图:节点为所有映射为
int
后的单词节点,边通过单词节点首尾字母相同的联系来建立。 - 单词环异常:如果传入参数不包含-r或者只是为了实现-n功能,则使用
Graph::HasCircle
算法判断并抛出异常。 - 调用图算法:根据不同的参数,调用
Graph
中的图算法计算结果。 - 单词表处理:将
Graph
中的图论算法计算出来的int
链结果,用WordMap
映射回单词,用CharConverter::WriteToBytePtrArray
转换为与接口对应的方式
- 单词表处理:调用
-
图算法实现
- 建图:单词为节点(节点权重默认为1,-c情况权重为单词长度)
- 环路判断:利用拓扑排序的思想来实现
- 遍历所有节点,如果某个节点的入度为 0,则将其加入队列
q
中。 - 当队列不为空时,取出队首元素
node
并将其弹出。然后遍历以node
为起点的所有边,并将这些边的终点对应的入度减一。如果某个终点对应的入度变为 0,则将其加入队列中。 - 当队列为空时,遍历所有节点的入度。如果存在任何一个节点的入度不为 0,则说明图中存在环路,返回 true;否则返回 false。
- 遍历所有节点,如果某个节点的入度为 0,则将其加入队列
- 所有路径 -n:利用DFS的思想实现,
FindAllChains
函数用来寻找图中的所有路径。- 创建了一个
chains
变量来存储所有路径。 - 遍历图中的所有节点,对于每个节点,都创建一个空的
curChain
和chainSet
变量,并调用FindPath
函数来寻找以该节点为起点的所有路径- 在
FindPath
函数中,首先将当前节点u
加入到当前路径curChain
中,并将其加入到集合chainSet
中。然后,如果当前路径长度大于 1,则将其加入到结果集合中(满足路径长度大于等于2的需求)。 - 遍历以当前节点为起点的所有边,并取出边的终点。如果终点不在集合
chainSet
中,则递归调用FindPath
函数来寻找以该终点为起点的所有路径。
- 在
- 最后,在函数返回之前,将当前节点从当前路径和集合中删除
- 创建了一个
- 最长路径 -c -w:通过统一的
GenChainMaxLength
函数实现。区分是最多单词数量链单词节点权重为1、最多字母数量链单词节点权重为单词长度。根据参数是否要求有环,分别由Graph::DagFindLongestChain
函数和Graph::FindLongestChainRecursive
函数实现无环和有环的情况。首字母和尾字母的限制通过作为参数的 lambda 表达式来传递给两个函数。Graph::DagFindLongestChain
:筛选满足head
条件的节点,遍历每个满足条件的节点,对每个节点调用Graph::DagFindLongestChainWithSource
算法来找到该节点作为单词链起点的最长单词链,然后再维护全图最长的单词链并返回Graph::DagFindLongestChainWithSource
BFS+动态规划,与Dijkstra算法类似求单源最长路径(除原点外的初始距离设为INT_MIN
;更新路径的条件不等式反向),筛选出满足条件tail
和条件head_reject
且最长距离不为INT_MIN
的节点,并在这些节点中找到具有最大最长距离的那个作为最长路径的终点,沿着该终点回溯出单源最长距离的路径。
Graph::FindLongestChainRecursive
:筛选满足head
条件的节点,遍历每个满足条件的节点,对每个节点调用Graph::FindLongestChainWithSourceRecursive
算法来找到该节点作为单词链起点的最长单词链,然后再维护全图最长的单词链并返回Graph::FindLongestChainWithSourceRecursive
函数 DFScurChain
当前路径,chainSet
当前路径中包含的节点,curValue
当前路径的权值和,maxValue
最大权值和。- 当当前搜索节点满足最大路径更新条件(
tail
条件,head_reject
条件,当前路径长度不小于2且权值和大于最大权值和),更新路径。 - 递归遍历当前节点的所有出边,并对其指向的节点进行搜索,通过
chainSet
来记录已经访问过的节点来避免重复访问
- 当当前搜索节点满足最大路径更新条件(
5.展示在所在开发环境下编译器编译通过无警告的截图
6.UML 图显示计算模块部分各个实体之间的关系
7.计算模块接口部分的性能改进
这里在加载dll之后函数名已经发生变化,通过将代码中的函数注释的方式/调用分析,发现前两个占比的函数是FindAllChain
和FindLongestChainWIthSourceRecursive
,与直观感受的DFS性能有待提高相对应
改进:通过记录已经遍历过的节点,减少重复子过程,对递归调用进行减枝。
8.Design by Contract,Code Contract
- 概念思考:Design by Contract(契约式设计)是一种软件设计方法,它规定软件设计人员应该为软件组件定义正式、精确和可验证的接口规范,这些规范扩展了抽象数据类型的普通定义,包括前置条件、后置条件和不变量
- 优点:设计更清晰更详尽可靠性更高,组件提供方和使用方各自的义务被表述得更清晰,从而使设计更加系统化、更清楚、更简单;契约式设计可以优化调试工作,便于错误溯源
- 缺点:带来额外的软件设计时间开销,开发者需要付出一定的学习成本
- 作业中的实现:通过约束计算核心core的输入和输出目标来实现接口的准确性;规定被调用者为结果序列开空间…
9.计算模块单元测试展示
我们采用gtest
进行单元测试,在每一个样例中定义输入和期望输出,并使用EXPECT_XX
校验行为正确性。如下图所示
TEST(CoreTest, gen_chains_all_example1) {
int result_num;
char **results = new char *[testResultSize];
// sample 1
int len = 4;
const char *words[] = {"woo", "oom", "moon", "noox"};
int expected_result_num = 6;
const char *expected_results[] = {
"woo oom",
"moon noox",
"oom moon",
"woo oom moon",
"oom moon noox",
"woo oom moon noox",
};
result_num = Core::gen_chains_all(const_cast<char **>(words), len, results);
EXPECT_EQ(expected_result_num, result_num);
results_cmp(expected_results, results, result_num);
for (int i = 0; i < result_num; ++i) {
delete[] results[i];
}
delete[] results;
}
TEST(CoreTest, gen_chains_all_example2) {
// sample 2 no input
char **results = new char *[testResultSize];
int len = 0;
const char *words[] = {};
int result_num = Core::gen_chains_all(const_cast<char **>(words), len, results);
EXPECT_EQ(result_num, len);
delete[] results;
}
我们针对不同情况分别构造了测试数据,包括-h, -t等参数设计,允许循环的全联通网络,以及空输入、单个单词输入等等情况。同时我们还对参数解析,IO接口进行了单元测试,下面是代码覆盖率:
10.计算模块异常处理说明
- 非法循环异常
在主参数为-n
,或者是-w,-c
未指定-r
时。检测到输入单词链中成环时抛出该异常。
throw std::logic_error("===has circle===");
该异常由Core模块内部,在建好图后,对图成环情况检查:
// gen_chains_all
if (graph.HasCircle()) {
throw std::invalid_argument("===has circle===");
}
// gen_chain_word, gen_chain_char
if (!enableLoop && graph.HasCircle()) {
throw std::invalid_argument("===has circle===");
}
在单元测试样例中,我们考虑了这一情况,并用EXPECT_ANY_THROW
进行判断
// sample 3 loop exception
char **results = new char *[testResultSize];
int len = 2;
const char *words[] = {"ab", "ba"};
EXPECT_ANY_THROW(Core::gen_chains_all(const_cast<char **>(words), len, results));
delete[] results;
- 参数非法异常
针对输入参数head
, tail
, dissallowed_head
。应当仅传入字母作为传参、或NULL表示不指定,当用户传入其他非字母字符时,抛出异常。
针对输入参数len
。当且仅当传入整数,当用户传入负数时,抛出异常
throw std::invalid_argument("illegal len only accept >= 0");
throw std::invalid_argument("illegal head only accept a-zA-Z");
throw std::invalid_argument("illegal tail only accept a-zA-Z");
throw std::invalid_argument("illegal disallowed_head only accept a-zA-Z");
单元测样例如下:
EXPECT_ANY_THROW(Core::gen_chain_word(nullptr, -1, nullptr, 0, 0, 0, false));
EXPECT_ANY_THROW(Core::gen_chain_word(nullptr, 0, nullptr, '_', 0, 0, false));
EXPECT_ANY_THROW(Core::gen_chain_word(nullptr, 0, nullptr, 0, '7', 0, false));
EXPECT_ANY_THROW(Core::gen_chain_word(nullptr, 0, nullptr, 0, 0, ')', false));
11.界面模块的详细设计过程
GUI设计总况如图所示
1.主属性选择
包含-n -w -c
三个参数,且三个参数间互斥。
2.附加属性选择
包含-h -t -j -r
,四个参数可选框。其中当主参数-n
选定时,附加属性选择框将不可选择。实现方法为选择按钮指定按钮组:
void MainWindow::functionButton_clicked(int id) {
if (id == 0) {
ui->headButton->setCheckState(Qt::Unchecked);
ui->headButton->setEnabled(false);
ui->tailButton->setCheckState(Qt::Unchecked);
ui->tailButton->setEnabled(false);
ui->jButton->setCheckState(Qt::Unchecked);
ui->jButton->setEnabled(false);
ui->rButton->setCheckState(Qt::Unchecked);
ui->rButton->setEnabled(false);
} else {
ui->headButton->setEnabled(true);
ui->tailButton->setEnabled(true);
ui->jButton->setEnabled(true);
ui->rButton->setEnabled(true);
}
}
3.附加属性填写
对应-h -t -j
三个参数,仅对应附加参数勾选后有效。且运行时会进行参数检查,若输入不合法参数将报错。且设置仅限输入单个字符
4.单词文本输入
支持文件输入和键盘输入,文件输入仅支持*.txt结尾文件,且输入结果将呈现到编辑文本框中供修改。
文件输入采用QT的QFileDialog::getOpenFileName
函数,并使用IO模块的接口完成IOUtil
QString file_name = QFileDialog::getOpenFileName(
this,
tr("open a file"),
QDir::currentPath(),
"Text Files(*.txt)"
);
if (file_name.isEmpty()) {
QMessageBox::warning(this, "Warning!", "Failed to open file!");
} else {
try {
word_num = IOUtil::get_word_from_file(file_name.toLatin1().data(), words);
ui->wordTextEdit->clear();
for (int i = 0; i < word_num; ++i) {
QString string;
string.append(words[i]).append("\n");
ui->wordTextEdit->insertPlainText(string);
}
release_array(words, word_num); // 读完后释放
} catch (std::exception &e) {
QMessageBox::warning(this, "Error!", "Failed to open file!");
}
}
5.结果输出
支持直接显示和文件输出,点击运行按钮后,若计算成功则会将结果展示,点击输出到文件按钮,即可弹出目标文件夹选项,之后会输出到目标文件夹下的solution.txt文件。
文件输入和输出都会因为打开失败而提示报错信息
6.运行时间显示
显示程序运行时间,保留两位小数。
12.界面模块与计算模块的对接
- CLI
CLI对接计算模块时,通过调用Core
接口
switch (function) {
case 'n':
result_num = Core::gen_chains_all(words, word_num, results);
break;
case 'w':
result_num = Core::gen_chain_word(words, word_num, results, head, tail, disallowed_head, enable_loop);
break;
case 'c':
result_num = Core::gen_chain_char(words, word_num, results, head, tail, disallowed_head, enable_loop);
break;
default:
throw std::invalid_argument("Invalid function option");
}
- GUI
GUI中同样如此,在读入每个组件的参数后调用Core
中函数同时会读取时间,计算Core
接口的运行时间
if (ui->nButton->isChecked()) {
printf("-n %c %c %c %d\n", head, tail, d_head, enable_loop);
release_array(results, result_num); // 释放之前的
s = clock();
ui->resultLabel->setText("正在运行");
result_num = Core::gen_chains_all(words, word_num, results);
e = clock();
QString string;
string.append("运行结束, 费时:").append(to_second_string(e - s).c_str());
ui->resultLabel->setText(string);
last_fn = 'n';
} else if (ui->wButton->isChecked()) {
printf("-w %c %c %c %d\n", head, tail, d_head, enable_loop);
release_array(results, result_num); // 释放之前的
s = clock();
ui->resultLabel->setText("正在运行");
result_num = Core::gen_chain_word(words, word_num, results, head, tail, d_head, enable_loop);
e = clock();
QString string;
string.append("运行结束, 费时:").append(to_second_string(e - s).c_str());
ui->resultLabel->setText(string);
last_fn = 'w';
} else if (ui->cButton->isChecked()) {
printf("-c %c %c %c %d\n", head, tail, d_head, enable_loop);
release_array(results, result_num); // 释放之前的
s = clock();
ui->resultLabel->setText("正在运行");
result_num = Core::gen_chain_char(words, word_num, results, head, tail, d_head, enable_loop);
e = clock();
QString string;
string.append("运行结束, 费时:").append(to_second_string(e - s).c_str());
ui->resultLabel->setText(string);
last_fn = 'c';
}
13.结对过程
我们首先在任务前半段通过简短的线上微信交谈沟通了任务大致的实现方式和接口确认,然后又多次在线下新主楼过道和共享咖啡厅进行面对面结对编程。
14.结对编程总结。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)。
优点 | 缺点 |
---|---|
设计思路共享、减少了无意义的低效的沟通、降低了代码理解的难度 | 如果结对的双方脾气不对味或技术观点不相近,就可能陷入很多的争吵,而导致进度停滞不前,甚至影响团队协作 |
队友优点:代码能力强,对git协作操作很熟练,有责任心
队友缺点:话有点少(?)
15.
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 50 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 50 | 30 |
Development | 开发 | 1700 | 2270 |
· Analysis | · 需求分析 (包括学习新技术) | 150 | 300 |
· Design Spec | · 生成设计文档 | 60 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 60 |
· Design | · 具体设计 | 200 | 300 |
· Coding | · 具体编码 | 600 | 1000 |
· Code Review | · 代码复审 | 300 | 300 |
· Test | · 测试(自我测试,修改代码,提交修改) | 300 | 250 |
Reporting | 报告 | 150 | 180 |
· Test Report | · 测试报告 | 60 | 100 |
· Size Measurement | · 计算工作量 | 30 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 | 60 |
合计 | 1900 | 2480 |