文章目录
1. 项目地址
社区 | 2022年北航敏捷软件工程社区-优快云社区云 |
---|---|
作业要求 | 结对编程项目-最长英语单词链-优快云社区 |
我在这个课程的目标 | 合作开发一个优秀的软件 |
这个作业在哪个具体方面帮助我实现目标 | 设计并实现一个小项目 |
项目地址 | xxqwbjfy/pair_gramming (gitee.com) |
班级 | 周五班 |
2. PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 900 | 1440 |
Development | 开发 | 600 | 720 |
· Analysis | · 需求分析 (包括学习新技术) | 200 | 420 |
· Design Spec | · 生成设计文档 | 60 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 10 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 10 |
· Design | · 具体设计 | 60 | 60 |
· Coding | · 具体编码 | 180 | 360 |
· Code Review | · 代码复审 | 60 | 120 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 180 |
Reporting | 报告 | 120 | 180 |
· Test Report | · 测试报告 | 60 | 30 |
· Size Measurement | · 计算工作量 | 60 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 | 60 |
total | 合计 | 1670 | 2270 |
3. UML图
项目采用c++开发,UML图如下
首先是基本函数的实现:
然后是核心模块core,其中实现了一个内存释放函数以及课程组给定接口:
4. 模块接口的设计与实现过程
约定
首先是对core.dll参数的约定
参数 | 意义 |
---|---|
words | 一个字符指针数组,要求使用new和delete进行内存的申请和调用,必须传入没有重复单词的words |
len | words的长度,最大为10000 |
results | 结果存储数组,要求使用new和delete进行内存的申请和调用,最大长度为20000 |
head | 单词链首字母,必须为字母 |
tail | 单词链尾字母,必须为字母 |
enable_loop | 是否允许隐藏单词环 |
返回值 | 单词链的数目(gen_chains_all)或者单词链中单词的数量(其他) |
然后是基础函数bfs参数的约定
参数 | 意义 |
---|---|
first_latter | vector数组,根据首字母的不同存储单词 |
flag | set数组,防止单词链中出现重复单词 |
ans | vector,用于存储结果 |
chain | 用于记录当前单词链 |
one_n | 若为1,统计该单词文本中共有多少条单词链,包含嵌套单词链 |
two_w | 若为1,计算最多单词数量的英语单词链 |
three_m | 若为1,计算首字母不同的单词数量最多单词链 |
four_c | 若为1,计算字母最多的英语单词链 |
five_h | 指定单词链的首字母,若希望首字母为a,请传入1,z请传入26 |
five_t | 指定单词链的首字母,若希望尾字母为a,请传入1,z请传入26 |
six_r | 若为1,表示允许单词文本隐含单词环 |
返回值 | 如果不允许单词环却存在单词环,返回-1,否则返回0 |
实现
首先调用devide_words对words中的字母进行分类,根据首字母的不同拆分为vector数组first_latter。
然后对words中的每一个单词调用bfs(广度优先遍历),将结果保存在ans中
最后,通过anstoresult将ans中的内容保存到result中,anstoresult的返回值即为core中指定接口的返回值。
5. 参考资料中 Information Hiding、Interface Design、Loose Coupling章节,说明这些方法在接口设计中的实际运用
5.1 Information Hiding
在原本失败的版本中,以类Word_set为例,Word_set中包含一个Vector和一个index属性,其中储存了所有单单词,使用者无法直接对这两个私有属性进行访问,而只能通过实现定义好的public 方法来传入单词,这样保证了类中的内容能够以开发者约定好的方式进行修改,而不是随意访问造成数据错误的问题。
在后面的新版本代码中,由于数据结构比较简单,并没有刻意使用类进行封装。
5.2 Interface Design
通过接口设计实现相应的方法,可以方便使用者进行调用,也能够保证开发者和测试者能够正确对接。core.cpp里面的gen_chain_word、gen_chains_all、gen_chain_word_unique、gen_chain_char就是课程组指定的接口,这些接口相较于bfs方法传入的参数更少,更加方便使用。
5.3 Loose Coupling
松耦合的基本概念是:允许改变或者当问题发生在“电线的一端时”来避免影响到其他的端点。也就是说,改变或者供应者或者服务的问题不能影响到用户----或者用户的问题不应影响到供应者或者服务。例如,为了方便代码实现bfs,我使用vector来保存单词链的求解结果,但是对使用者来说,只需要按约定传入result指针数组即可获得结果,而不用担心内部是如何保存和实现的。
6.模块接口部分的性能改进
性能分析
通过以下数据对Wordlist进行性能分析(Wordlist -r -n input.txt),结果如下
ba ab
ca ac cb bc
da ad db bd dc cd
从图中可以看出,bfs占据了绝大部分cpu。
改进思路
因为bfs是一个递归函数,所以会调用很多次。
首先是减少bfs的调用数量,实现更加严格的退出条件,因为有时候已经检测到程序出错了,就不需要继续进行bfs。通过设置特定的返回值,告诉主程序已经出现已经出现错误(例如,input.txt中的单词不合法)来防止主程序继续对words中的单词进行循环调用bfs,在检测到错误后,直接用break跳出循环。
其次增加bfs的速度,通过首字母对单词进行分类,这样以后,bfs在寻找单词链的下一个单词的时候就不需要遍历words,从而提高bfs的运行速度。
7. Design by Contract,Code Contract
Design by Contract
契约式设计
在这种设计中,调用者和调用者地位平等,双方必须彼此履行义务,才可以行驶权利。调用者必须提供正确的参数,被调用者必须保证正确的结果和调用者要求的不变性。双方都有必须履行的义务,也有使用的权利,这样就保证了双方代码的质量,提高了软件工程的效率和质量。
在代码中的体现,以gen_chains_all为例,调用者必须传入正确的参数,程序才会将正确的结果保存在result指针数组中。否则程序会返回对应的错误信息。
缺点是一旦启用契约,在实际应用时难以对契约进行修改或者停用。
Code Contract
代码契约
代码契约提供了在 .NET Framework 代码中指定前置条件、后置条件和对象固定的方法。 前置条件是输入方法或属性时必须满足的要求。 后置条件描述在方法或属性代码退出时的预期。 对象固定描述处于良好状态的类的预期状态。主要运用于c#,本次作业中体现不明显。
8、单元测试展示
对core中四个函数进行单元测试,构造测试的主要思想为对四个方法的每一个参数进行测试,部分测试用例如下:
TEST_METHOD(TestMethod3)//测试出head或者tail传入0的情况
{
int len = 4;
char* words[] = { "ab", "bc", "bd", "dc" };
int num = 3;
char* ans[] = {
"ab\nbd\ndc\n"
};
char** result = new char* [MAXANS];
int _num = gen_chain_word(words, len, result, 'a', 0, false);
Assert::AreEqual(num, _num);
vector<string> _ans, _result;
for (int i = 0; i < 1; i++) {
_ans.push_back(ans[i]);
_result.push_back(result[i]);
}
sort(_ans.begin(), _ans.end());
sort(_result.begin(), _result.end());
for (int i = 0; i < 1; i++) {
Assert::AreEqual(_ans[i], _result[i]);
}
free_result(result, 1);
delete[]result;
}
TEST_METHOD(TestMethod6) //解决了words中重复单词问题,约定words中不能存在重复单词
{
int len = 6;
char* words[] = { "ab", "bc", "cd", "ccm", "ce", "ex"};
int num = 4;
char* ans[] = {
"ab\nbc\nce\nex\n"
};
char** result = new char* [MAXANS];
int _num = gen_chain_word_unique(words, len, result);
Assert::AreEqual(num, _num);
vector<string> _ans, _result;
for (int i = 0; i < 1; i++) {
_ans.push_back(ans[i]);
_result.push_back(result[i]);
}
sort(_ans.begin(), _ans.end());
sort(_result.begin(), _result.end());
for (int i = 0; i < 1; i++) {
Assert::AreEqual(_ans[i], _result[i]);
}
free_result(result, 1);
delete[]result;
}
无奈使用的vs2022是社区版本,无法分析代码覆盖率。
9. 计算模块部分异常处理说明
我们并没有实现标准的异常处理,只是在出现错误的时候打印出错误信息并正常结束程序,这些错误包括:
1、调用wordlist时参数错误
if (strlen(argv[arg_index - 1]) == 1 || !isalpha(argv[arg_index][0]))
{
cout << "不合法的参数传递" << endl;
return 1;
}
five_t = argv[arg_index][0] - 'a' + 1;
if (five_t < 0)
{
five_t += 32;
}
break;
default:
cout << "不支持的参数" << endl;
return -1;
break;
if (!input)
{
cout << "不存在此文件" << endl;
return 3;
}
if (get_argv(argc, argv) != 0)
{
cout << "参数解析失败" << endl;
}
2、输入文本存在单词环
if (bfs(first_latter, flag, chain, ans, one_n, two_w, three_m, four_c, five_h, five_t, six_r) != 0)
{
cout << "存在单词环" << endl;
ans.clear();
return 0;
}
3、单词链数量过多
if (size_ans > 20000)
{
cout << "单词链数量过多" << endl;
ans.clear();
return 0;
}
4、不存在符合条件的字符串
else
{
cout << "不存在符合条件的字符串" << endl;
}
5、core接口参数有误
if (!((isalpha(head) || head == 0) && (isalpha(tail) || tail == 0)))
{
cout << "head 和 tail 参数必须为字母" << endl;
}
6、加载dll失败
HMODULE hDll = 0;
hDll = LoadLibrary("D:\\2022chun\\software\\repo\\core\\x64\\Release\\core.dll");
if (!hDll) {
cout << "动态链接库 core.dll 加载失败" << std::endl;
return -1;
}
typedef int (*chain_word)(char* words[], int len, char* result[], char head, char tail, bool enable_loop);
chain_word gen_chain_word;
gen_chain_word = (chain_word)GetProcAddress(hDll, "gen_chain_word");
if (gen_chain_word == 0) {
cout << "动态链接库中方法加载失败" << endl;
FreeLibrary(hDll);
return -1;
}
10. 界面模块详细设计过程
CLI界面
cli界面通过读取命令行参数对程序进行控制,在完成参数解析后,对计算模块进行相应的调用,具体实现如下:
ifstream input;
int one_n = 0; //统计该单词文本中共有多少条单词链,包含嵌套单词链
int two_w = 0; //计算最多单词数量的英语单词链
int three_m = 0; //计算首字母不同的单词数量最多单词链
int four_c = 0; // 计算字母最多的英语单词链
int five_h = 0; //指定单词链的首字母
//char four_h_argv = '\0';
int five_t = 0; //指定单词链的尾字母
//char five_t_argv = '\0';
int six_r = 0; //表示允许单词文本隐含单词环
int arg_index = 0;
//本项目对于输入的参数和文件名的顺序没有假设
int file_flag = 0;
int get_argv(int argc, char* argv[])
{
if (argc > 1)
{
for (arg_index = 1; arg_index < argc; arg_index++)
{
if (strlen(argv[arg_index]) > 4)
{
if (file_flag == 1)
{
cout << "不合法的参数传递" << endl;
return 1;
}
string input_file(argv[arg_index] + strlen(argv[arg_index]) - 4);
if (input_file != ".txt")
{
cout << "错误:需要输入txt文件" << endl;
return 2;
}
input.open(argv[arg_index]);
file_flag = 1;
}
else if (argv[arg_index][0] != '-' && strlen(argv[argc - 1]) == 2)
{
cout << "不合法的参数传递" << endl;
return 1;
}
else
{
switch (argv[arg_index][1])
{
case 'n':
one_n = 1;
break;
case 'w':
two_w = 1;
break;
case 'm':
three_m = 1;
break;
case 'c':
four_c = 1;
break;
case 'h':
arg_index++;
if (strlen(argv[arg_index - 1]) == 1 || !isalpha(argv[arg_index][0]))
{
cout << "不合法的参数传递" << endl;
return 1;
}
five_h = argv[arg_index][0] - 'a' + 1;
if (five_h < 0) //处理大写
{
five_h += 32;
}
break;
case 't':
arg_index++;
if (strlen(argv[arg_index - 1]) == 1 || !isalpha(argv[arg_index][0]))
{
cout << "不合法的参数传递" << endl;
return 1;
}
five_t = argv[arg_index][0] - 'a' + 1;
if (five_t < 0)
{
five_t += 32;
}
break;
case 'r':
six_r = 1;
break;
default:
break;
}
}
}
}
else
{
input.open("input.txt");
}
if (!input)
{
cout << "不存在此文件" << endl;
return 3;
}
return 0;
}
GUI简介
用户界面由python实现,并打包成相应的exe文件,通过下载git中的gui文件夹,即可通过其中的main.exe启动用户界面,如下图所示:
使用方法
用户在左边输入单词文本,或者通过上传文本文件按钮上传单词文本。需要注意的是,上传单词文本以后,系统默认读取上传的文本文件,而不是左边文本输入框的文本。若需要测试输入框的文本,请单击取消上传。
左下角为程序运行日志,如果运行失败会显示错误信息,否则会在右边输出结果,可点击保存结果,将输出结果保存的指定文件夹下。
11、界面模块与计算模块的对接
界面模块通过以下方法调用计算模块(以-n为例),使用popen调用命令行,并将得到的结果打印到日志中。
# 功能函数
def n(self):
if self.file == 0:
self.write_text()
a = os.popen("Wordlist.exe -n ./resourse/input_{}.txt".format(self.file))
a = a.readlines()
a = a[len(a) - 1]
self.write_log_to_Text(a)
# self.write_log_to_Text(self.error(a))
if a == "运行成功\n":
self.cat()
12、结对过程
在线下对需求和代码规范进行确定以后,结对编程主要以线上方式进行。
13、结对编程优缺点
优点
1、两个人可以相互为对方的代码找bug,写出正确的程序
2、两个人能够相互监督,提高编码效率
3、两个人可以减轻编码的压力
缺点:
1、时间消耗较大
2、选择结对对象比较困难
队员优缺点分析
颜月 | 吴时雨 | |
---|---|---|
优点 | 熟悉c++、学习能力较强、写代码能力较强 | 熟悉git、熟悉使用vs2022、沟通能力较强 |
缺点 | 对git不熟悉 | 不熟悉c++ |
14. PSP 表格记录实际花费的时间
见第二节的表,由于前期选择的算法错误,并且不太熟悉dll和单元测试,导致编码时间和学习新技术的时间超过预期。