结对项目-最长英语单词链
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2023北航软件工程 |
这个作业的要求在哪里 | 结对项目-最长英语单词链 |
我在这个课程的目标是 | 帮助我初步建立软件工程敏捷开发的整体流程和概念,初步认识软件工程 |
这个作业在哪个具体方面帮助我实现目标 | 学习结对编程方法,并加以实践 |
1.项目信息
在文章开头给出教学班级和可克隆的 Github 项目地址
- 教学班级:周四下午班
- 项目地址:https://github.com/Wpy12346946/Word-Chain.git
2.PSP
在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的 各个模块的开发上耗费的时间。
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 10 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 10 |
Development | 开发 | 1060 | 1510 |
· Analysis | · 需求分析 (包括学习新技术) | 120 | 180 |
· Design Spec | · 生成设计文档 | 60 | 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 10 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 120 | 160 |
· Coding | · 具体编码 | 400 | 500 |
· Code Review | · 代码复审 | 60 | 90 |
· Test | · 测试(自我测试,修改代码,提交修改) | 240 | 480 |
Reporting | 报告 | 245 | 250 |
· Test Report | · 测试报告 | 180 | 180 |
· Size Measurement | · 计算工作量 | 5 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 | 60 |
合计 | 1335 | 1770 |
3.接口设计原则
看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。
Information Hiding:
在我们的设置中,我们对于计算单词链的功能CORE封装为独立模块core.dll,该模块通过向外暴露了三个接口与本机的GUI和CLI以及另一组的GUi进行交互。
其中CORE模块中有许多私有模块,包括图的构建和遍历等等,图的设计和边的设计用Graph和Edge类封装,不对外开放。对外开放的接口实现在Core中。
Interface Design:
为了使接口具有良好的可读性、可理解性和可维护性,我们仅实现了指导书上CORE的三个接口。
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);
int gen_chains_all(char *words[], int len, char *result[]);
其中words和result参数的内存均有调用者动态申请和释放(例如GUI和CLI申请单词内容),这样做的好处是可以防止操作者的操作不当导致的内存泄漏,同时也能节省内存、
Loose Coupling:
该原则指的是,在设计软件系统时,应该尽量避免模块之间的紧密耦合。因此我们设计的CORE、GUI。CLI均为松耦合,这样可以很方便的和其他模型进行组合。
4.接口设计和实现
**计算模块接口的设计与实现过程。**设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。
CORE接口设计如下:
const int MAX_LENGTH = 20000;
const int WORD_CYCLE_EXCEPTION = 0x80000001;//有环异常
const int TOO_LONG_EXCEPTION = 0x80000002;//链过长异常
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);
int gen_chains_all(char *words[], int len, char *result[]);
MAX_LENGT
:表示result
最长单词链长度,如果超过则报错TOO_LONG_EXCEPTION
:若模块在处理过程中发现最长单词链长度超过MAX_LENGTH
,则报错,直接返回其异常值WORD_CYCLE_EXCEPTION
:若模型在处理gen_chains_all
或者enable_loop=true
的过程中发现单词环,则返回此异常值
gen_chain_word/gen_chain_char/gen_chains_all
的各个参数含义如下
-
输入参数
-
words
输入单词串,由全小写英文单词组成 -
len
输入单词串长度 -
result
空的单词链,用于存储返回单词链 -
head
:头字母限制,表示单词链的首字母必须为head
-
tail
:尾字母限制,表示单词链的尾字母必须为tail
-
reject
:首字母限制,表示单词链中不允许出现首字母为reject
的单词
-
-
返回值
result
:返回一个单词链int
返回值- 未发生异常:
gen_chains_all
返回单词文本中单词链数量,gen_chain_word/gen_chain_char
返回单词链的个数 - 发生异常:返回异常码
TOO_LONG_EXCEPTION
或者WORD_CYCLE_EXCEPTION
- 未发生异常:
CORE模块构成为Core.cpp/Edge.cpp/Graph.cpp
Core
:接口的实现部分,通过调用Graph
来进行对图的操作Edge
:边节点,用于保存边的信息Graph
:图结构,存储了图信息和构造接口方法
具体步骤如下:
- 调用
Graph.MakeGraph
- 将边的信息存储到
Edge
中 - 根据单词之间的关系,生成图
- 将边的信息存储到
- 调用
Graph
中的图论算法,计算结果- 利用拓扑序检测单词环。若检测出单词环或者不允许单词环,则直接返回异常码
WORD_CYCLE_EXCEPTION
未指定-h参数时,利用拓扑序快速筛选出单词链可能的起点。 - 三个接口的图论算法均为
dfs
- 若检测出单词环或者不允许单词环,则直接返回异常码
WORD_CYCLE_EXCEPTION
- 利用拓扑序检测单词环。若检测出单词环或者不允许单词环,则直接返回异常码
- 在图论算法中不断检测最长链,若检测出单词链长度大于
MAX_LENGT
,则直接返回TOO_LONG_EXCEPTION
异常码 - 返回单词链的长度与单词链
5.无警告截图
展示在所在开发环境下编译器编译通过无警告的截图
构建core.dll过程的无警告截图
构建Wordlist.exe过程的无警告截图
构建test单元测试的无警告截图
6.UML图
阅读有关 UML 的内容,画出 UML 图显示计算模块部分各个实体之间的关系(画一个图即可)。
7.性能改进
**计算模块接口部分的性能改进。**记录在改进计算模块性能上所花费的时间,并展示你程序中消耗最大的函数,陈述你的性能改进策略。
对于无隐含环的情况,将指向相同的边合并后只需要常数次的遍历(图的结点数被限制在了26)即可完成,因此基本不需要优化。而有隐含环时,该问题为NP问题。因此只能通过一些方法来降低存在部分特征的图的复杂度。我们采用了如下几种优化来改进性能:
- 使用指针避免拷贝构造
由于C++的stl容器在添加元素时无法使用引用,只能使用拷贝构造。因此,为避免string与自定义的Edge在拷贝构造上的开销,我们在初始化时会创建所有本次调用中会用到的这些对象,后续使用时,容器中用指针保存这些对象的地址,省略了多余的拷贝。
- 分离自环边和非自环边
根据贪心策略,当首次遍历到某个节点时要首先添加所有自环边,然后再dfs非自环边。因此将边细分为自环边集合和非自环边集合后,dfs时可以省略掉对自环边的遍历。
- 强连通分量+拓扑排序+dp
使用tarjan算法计算图中的强连通分量,将结点划分到不同集合中。不同强连通分量可通过拓扑排序得到单词链的可能顺序。对于有-h
或-r
的运行,可以剪枝提前剔除一定不在最终单词链中的强连通分量。强连通分量内的任意两个节点通过暴力dfs计算两点间的最长距离。最后通过拓扑序和dp计算单词链总长。
我们随机生成了单词数为100的测试样例对上述的优化前后效果进行对比。可以看到,在优化前后递归过程中产生的函数调用次数有很显著的减少。
优化前cpu运行时间图:
优化后cpu运行时间图:
可以看到,优化后性能提高了十倍左右
8.Design by Contract,Code Contract
阅读 Design by Contract,Code Contract 的内容,并描述这些做法的优缺点,说明你是如何把它们融入结对作业中的。
Design by Contract:
契约式编程在OO的时候就有所涉及,但还是学的是对于JML方法的形式化描述。简单来说就是构成的契约是一组前置条件、后置条件和不变式的集合,用来描述组件之间的协议。
而在本次作业之中,我们认为本次作业内容相对较为简单,接口的参数和其他的函数光看名字就可以知道是什么意思,因此很难利用契约式编程。因此我们只是利用了这种思想简单的对于错误处理和内存分配进行了前置条件、后置条件和不变式的讨论。
Code Contract是微软为 .NET 提供的一个契约式编程插件。我们本次作业没有使用.NET,因此不需要考虑。
9.单元测试展示
计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效。
CORE模块单元测试(不涉及异常值):
我们手工构造了22个单元测试点:
TEST(test, TestMethod1) {
for (int i = 1; i <= 17; i++) {
coreTest(i);
}
};
同时我们设计了五种针对CORE接口的逻辑测评:
void retCheck(int res, int truth)
- 检查返回单词链个数是否相等
void chainCheck(char *results[], int len)
:此
- 检查返回单词链个数是否越界
- 检查返回单词链中相邻单词是否满足基本约束
void headCheck(char *results[], int len, char head)
- 检查单词链是否为指定首字母
void tailCheck(char *results[], int len, char tail)
- 检查单词链是否为指定尾字母
void rejectCheck(char *results[], int len, char rejectCheck)
- 检查单词链是否出现首字母为
rejectCheck
的单词
void circleCheck(char *results[], int len)
- 检查单词链中是否有单词环
coreTest
具体方法如下:
void coreTest(int index) {
std::string dataFile = "../testfile/core/data" + std::to_string(index) + ".txt";
std::string paramFile = "../testfile/core/param" + std::to_string(index) + ".txt";
std::vector<std::string> wordList;
char **words = loadData(dataFile, wordList);
Config config;
loadConfig(paramFile, config);
char *result[20000];
int res;
switch (config.api) {
case 'n':
res = gen_chains_all(words, wordList.size(), result);
retCheck(res, config.ret);
for (int i = 0; i < res; i++) {
std::string tmp[20000];
int cnt = 0;
for (int j = 0; result[i][j] != '\0'; j++) {
if (result[i][j] == ' ') {
cnt++;
} else {
tmp[cnt] += result[i][j];
}
}
cnt++;
char *tmpRes[20000];
for (int j = 0; j < cnt; j++) {
tmpRes[j] = (char *) tmp[j].c_str();
}
if (cnt >= 0) {
chainCheck(tmpRes, cnt);
circleCheck(tmpRes, cnt);
}
}
break;
case 'w':
res = gen_chain_word(words, wordList.size(), result, config.h, config.t, config.j, config.r);
retCheck(res, config.ret);
if (res >= 0) {
chainCheck(result, res);
headCheck(result, res, config.h);
tailCheck(result, res, config.t);
rejectCheck(result, res, config.j);
if (!config.r) {
circleCheck(result, res);
}
}
break;
case 'c':
res = gen_chain_char(words, wordList.size(), result, config.h, config.t, config.j, config.r);
retCheck(res, config.ret);
if (res >= 0) {
chainCheck(result, res);
headCheck(result, res, config.h);
tailCheck(result, res, config.t);
rejectCheck(result, res, config.j);
if (!config.r) {
circleCheck(result, res);
}
}
break;
}
delete[]words;
}
10.异常处理说明
**计算模块部分异常处理说明。**在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。
ore部分的测试代码沿用了上述**coreTest()**函数。我们在core中定义了两种异常:
const int WORD_CYCLE_EXCEPTION = 0x80000001;//有环异常
const int TOO_LONG_EXCEPTION = 0x80000002;//链过长异常
当没有指定-r参数,但输入的单词中存在隐含单词环时,触发有环异常WORD_CYCLE_EXCEPTION。测试样例如下:
Element
Heaven
Table
Teach
Talk
此时存在隐含环Element-Table。
对于链过长异常TOO_LONG_EXCEPTION,我们使用a*b、b*c、c*d、d*e、e*f构造样例(*保证每组有10组以上互不相同的单词),参数使用-n,此时返回的数量为100000以上,触发异常TOO_LONG_EXCEPTION。
由于接口函数从设计上已经避免了大部分由参数导致的异常,因此其余可能的异常全部在命令行中进行检查。涉及的异常如下:
const int DUPLICATE_FILE_EXCEPTION = 0x80010000;//输入多个文件
const int UNDEFINED_PARAM_EXCEPTION = 0x80010001;//未定义参数
const int READ_FILE_EXCEPTION = 0x80010002;//读文件失败(文件不存在)
const int LACK_PARAM_EXCEPTION = 0x80010003;//缺少必要参数(-n -w -c)
const int CONFLICT_PARAM_EXCEPTION = 0x80010004;//参数冲突
const int PARAM_FORMAT_EXCEPTION = 0x80010005;//参数值格式错误(-h -t -j的参数值)
const int DUPLICATE_PARAM_EXCEPTION = 0x80010006;//参数重复出现
const int LACK_FILE_EXCEPTION = 0x80010007;//缺少输入文件
const int WRITE_FILE_EXCEPTION = 0x80010008;//写入文件失败
和coreTest()
类似,我们定义了cliTest()
和对应的data*.txt、param*.txt来进行测试。测试文件包含了coreTest()
所有的样例。针对命令行中的异常,还包含如下样例:
- DUPLICATE_FILE_EXCEPTION
-w ../testfile/cli/data_cli_exception.txt ../testfile/cli/data_cli_exception.txt
- UNDEFINED_PARAM_EXCEPTION
-w -ht ../testfile/cli/data_cli_exception.txt
- READ_FILE_EXCEPTION
-n ../testfile/cli/data_cli_exception_not_exist.txt
- LACK_PARAM_EXCEPTION
-h a ../testfile/cli/data_cli_exception.txt
- CONFLICT_PARAM_EXCEPTION
-n -w ../testfile/cli/data_cli_exception.txt
- PARAM_FORMAT_EXCEPTION
-w -h ? ../testfile/cli/data_cli_exception.txt
- DUPLICATE_PARAM_EXCEPTION
-w -w ../testfile/cli/data_cli_exception.txt
- LACK_FILE_EXCEPTION
-w -h a
11.界面模块说明
**界面模块的详细设计过程。**在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。
运行展示
我们的图形界面设计如下。根据功能不同,分为输入部分、输出部分、参数控制部分三个部分。
- 输入部分
第一部分为输入部分。用户可直接输入要处理的单词,也可以通过文件资源管理器选择并读取文件中的单词,读取后的单词会显示在输入框中。
- 输出部分
第二部分为输出部分。经过core.dll处理得到的正确结果会显示在输出框中,并可以以.txt格式导出到自定义路径下。
- 参数控制部分
第三部分为参数控制部分。用户可以勾选并设置-h、-t、-j的参数值,也可以选择是否允许隐含单词环。完成参数设置后,点击下方三个按钮,分别计算所有单词链、最长单词数的单词链、最多字母数的单词链。
实现细节
图形化界面通过交互逻辑的约束,极大的简化了命令行中可能产生的异常。
- 参数冲突
为避免使用者因为错误的搭配参数造成参数冲突,我们令上方的四个勾选框和“所有单词链”按钮彼此冲突,当有选项被勾选时,禁用该按钮。部分代码如下:
# 更新allButton(查询所有链)按钮状态
def paramClick(self, state, button, textEdit=None):
if textEdit:
if state == Qt.Checked:
textEdit.setDisabled(False)
textEdit.setText('a')
textEdit.setFocus()
else:
textEdit.setDisabled(True)
textEdit.setText('')
if self.allowHeadButton.isChecked() \
or self.allowTailButton.isChecked() \
or self.allowRejectButton.isChecked() \
or self.allowCircleButton.isChecked():
self.allButton.setDisabled(True)
else:
self.allButton.setDisabled(False)
示例如下:
- 参数格式错误反馈:
对于-h、-t、-j的参数值,我们约束了参数长度最大为1。当参数值为空或非字母字符时,由界面直接反馈,而不用交给core.dll判断。
12.界面模块对接
**界面模块与计算模块的对接。**详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。
UI模块使用pyqt5设计,计算模块使用C++实现,因此我们使用ctypes包进行两个模块的对接。我们使用包内的WinDLL()
实现dll文件的载入,然后将python中参数的数据类型通过ctypes将python中的数据类型包裹为C++中的数据类型,然后传给dll中对应的函数进行计算。返回值如果为异常值,则进行报错,如果为正常值则将返回的单词链解构为python中的数据类型后,输出到UI界面上。示例代码及运行结果如下 :
def split2bytes(s):
wordList = re.compile('[a-zA-z]+').findall(s)
length = len(wordList)
words = (c_char_p * length)()
for p in range(length):
b_str = bytes(wordList[p].encode('utf-8')).lower()
words[p] = b_str
return words, c_int(length)
def bytes2str(words, length):
res = ""
for i in range(length):
res += words[i].decode()
res += "\n"
return res
dll = WinDLL(r".\\core.dll")
words, length = utils.split2bytes(words)
result = (c_char_p * 1000)()
head = c_char(head)
tail = c_char(tail)
reject = c_char(reject)
enable_loop = c_bool(enable_loop)
chainLen = dll.gen_chain_word(words, length, result, head, tail, reject, enable_loop)
if chainLen < 0:
raise WordException(chainLen)
res = utils.bytes2str(result, chainLen)
正常运行结果:
异常报错:
13.结对过程
描述结对的过程,提供两人在讨论的结对图像资料(比如 Live Share 的截图)。关于如何远程进行结对参见作业最后的注意事项。
我们全程采用了线下结对的方法,所有的模块和单元项目的讨论都在一起完成,而构建和修正由二者分别完成,修正之后发送到github上面同时下次先线下结对的过程中进行讨论。
14.结对优缺点
看教科书和其它参考书,网站中关于结对编程的章节,例如:http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html ,说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)。
对编程的优点和缺点
优点
- 结对编程的过程中由二人共同完成,可以相互查缺补漏,提高了代码质量
- 结对编程帮助了结对的两位程序员相互学习,互相补充
- 结对编程能够帮助开发者更好地协同工作,同时也能够提高沟通和协作能力
- 结对编程能够让开发者更高效地完成任务,因为一人编写代码,另一人可以即时提供反馈和建议
缺点
- 结对编程需要考虑队友的需求和期望,因此编程具有更高的压力
个人评价
- 优点
- 理念清晰,上手能力快
- 肯花时间多用于新知识的探索和查找
- 测试能力较强
- 缺点
- C++系统编程能力较弱
队友评价:
- 优点
- 善于构造项目和代码结构
- 对于算法设计和功能实现行动力强
- 善于合作,对接思考能力和沟通能力强
- 缺点
- 开发时讲求完美(也可能是因为我太菜了)
15.小组对接(附)
我们与班上其他两位同学(学号:19374223 19374006)进行了core.dll的互换。因为我们均参照课程组约定的标准接口进行设计,因此在正常调用时可以直接进行替换。不一致之处主要在于异常情况的反馈与处理。问题及解决方案如下:
- 异常返回值不一致
我们将无-r
参数时的有环异常返回的异常码设置为0x80000001
,而另一组将的异常码为-3
。因此在互换后需要将CLI端和GUI端的异常码同步进行修改。
- 缺少异常
我们的设计中,如果长度超过20000,则会提示“链过长异常”,而另一组并未对此进行限制,即使长度超过20000也会全部返回。为满足我们的设计,需要在前端对长度进行一次特判,如果超过长度限制则抛出异常。
我们又和20373779、20373790小组的同学互换了模块。我们都是按照课程组约定的标准接口进行设计,因此模块也可以直接替换。不一致之处在于对于异常的反馈和处理,具体表现为异常返回值不一致:因此解决方案和上面相同