软件工程课程结对项目总结
项目 | 内容 |
---|---|
本次作业所属课程 | 2019BUAA软件工程 |
本次作业要求 | 结对编程作业 |
我在本课程的目标 | 熟悉结对编程流程 |
本次作业的帮助 | 实践了结对编程的流程,对结对编程的优缺点有了更深的体会 |
本次作业项目github地址 | 项目地址 |
1.本次作业项目github地址
项目地址
2.开发前PSP表
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 30 | |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 180 | |
· Design Spec | · 生成设计文档 | 60 | |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | |
· Design | · 具体设计 | 250 | |
· Coding | · 具体编码 | 450 | |
· Code Review | · 代码复审 | 250 | |
· Test | · 测试(自我测试,修改代码,提交修改) | 300 | |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 100 | |
· Size Measurement | · 计算工作量 | 20 | |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | |
合计 | 1710 |
3.接口设计方法
看教科书和其它资料中关于Information Hiding, Interface Design, Loose Coupling的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的
Information Hiding-信息隐藏是一种编程原则,通常与封装一起使用。Information Hiding建议对于程序内部的实现进行隐藏,从而防止对于对程序进行粗放的修改。对外提供比较稳定的接口,在使用时可忽略内部的实现。
Interface Design-在阅读资料过程中,发现良好的接口设计要遵循许多原则例如单一职责,里氏替换,依赖倒置等,由于本次实现的接口较为简单,基本保证了这些原则。
Loose Coupling-即松散耦合。松散耦合系统中,组件对于其他组件定义的系统利用较少,松散耦合的优点是,可以将一些模块自由的替换为其他功能相同的模块。
在我们的设计中,为了保证依赖倒置原则以及采用松散耦合的结构,对于内部的计算类Sovler类用Core类进行额外的封装,Core类仅仅提供两个静态的接口可供调用。这样一来,用户便不用对于Sovler类的内部方法又一定的了解,也不用调用接口前先产生一个Solver对象。
4. 计算模块接口的设计与实现过程
计算模块接口的设计与实现过程,设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。
计算模块是整个项目的核心,发现这个模块计算的内容有很强的聚合性,因此采用面向对象的方式进行封装,最后抽象出一个实体计算类Solver以及接口类Core,共两个类。Core类的接口每次会实例化一个Solver对象,然后调用max_chain_word函数进行计算。画出一个调用的流程图:
算法大致思路:首先拿到问题需要对原问题进行建模,一条单词链相邻两个单词的特征是,前一个单词的末尾等于后一个单词的头,这个可以通过有向图的有向边实现。对于不出现-r参数的情况相当于求图中一些指定点作为起点,一些点作为终点的最大路径问题(采用了一个经典的SPFA算法)。对于出现-r参数的情况归约为求有向有环图中的最大路径问题,属于一个非常经典的NP问题,对于这种情况并没有多项式时间的准确算法,因此采用dfs搜索算法。
算法的独到之处:采用一图多权值的方法,因为原问题有求最长单词数和字符数两种需求,所以在两个节点边的权值记录了两个,求最大单词数目时使用权值为1,求最长单词数使用权值为目的节点的字符长度。这样对于两种问题的求解方法是基本一样的,只是计算不同的边权,很好的做到了代码复用。对于输入的单词列表转化为图之后首先采用拓扑排序的方法判断图是否有环,对于-r参数的情况下不一定调用dfs搜索(当判断出图中无环时仍然采用时间复杂度低的SPFA算法)。
5.UML图
阅读有关UML的内容:https://en.wikipedia.org/wiki/Unified_Modeling_Language。画出UML图显示计算模块部分各个实体之间的关系(画一个图即可)。
VS2017可以下载安装包,支持自动生成类图,参考博客 。最后生成的类图如下:
6.计算模块接口部分的性能改进
计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。
在完成基本程序的运行功能之后,我对程序的性能进行了一定的改进,在这个过程中是很痛苦的,大概花了两天的时间,其中也走了很多弯路,最后在无环图上优化取得了一定效果,但是有环图改进的幅度很小。
改进的思路:对于无环的情况,尽可能采用时间复杂度比较低的算法,权衡再三最后将最终算法改为SPFA算法。对于有环的情况,因为跟许多同学讨论证实这是一个NP问题,所以能做的改进也是很有限,主要的思路就是在dfs搜索的时候能剪枝,以及在某些特殊的情况下需要进行特殊处理(比如如果判断出无环,即使带-r参数要调用SPFA函数而不是暴力搜索,以及如果有环的情况下找到一个长度为n的链(n为节点总数)也可以停止搜索得到答案)。对于有环的情况,实际上还可以考虑一些启发式算法比如模拟退火算法等,但是本题目需要输出准确解,启发式算法有一定风险,因此没有采用该方法。
这部分主要有两种情况,一个是没有-r参数的时候调用SPFA方法时的性能,
下面展示一个较小数据集下不带-r的性能分析图:
发现其中耗时最多是的生成图的函数,说明计算算法性能基本可以。
一个是带有-r参数且图中有环会频繁递归调用DFS函数的情况,这一部分毫无疑问是dfs函数被多次递归调用。
7.Design by Contract, Code Contract
看Design by Contract, Code Contract的内容:
http://en.wikipedia.org/wiki/Design_by_contract
http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx
描述这些做法的优缺点, 说明你是如何把它们融入结对作业中的
Design by Contract,即契约编程,意为我们在声明一个函数/方法的时候,对函数的输入和输出所具备的性质是有所期望和规定的。对于所有组件,规定前置条件,后置条件以及不变量。契约编程要求对于前置条件,后置条件,不变量。
契约编程的好处是可以保证程序的健壮性,并严格的分析责任。编程人员仅需要对自己的部分按照契约负责。缺点是,契约式设计比较繁琐,如果每一处都采用契约式设计会费时费力。
具体在我们的作业中,并未严格按照契约式编程规定前置条件,后置条件以及不变量,更多的是在组件内检测是否符合条件。
8.计算模块部分单元测试展示
计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到90%以上,否则单元测试部分视作无效。
计算模块单元测试覆盖率达到了98%,截图如下
单元测试分为两大部分,一个是对正常情况下的处理,一个是对异常处理,异常部分的单元测试请参见博客第九部分。正常情况下的测试分为三种,一个是无环无-r参数,一个无环有-r参数,一个是有环有-r参数。对前两种,测试了最大单词数和最大字符数结果相同、结果不同的情况,同时也测试了不同的头尾对结果的限制。对于第三种有环的情况,也测试所有无环的情况,同时测试了图中有多个环的情况。除上面的常规测试之外还增加了边界测试,比如最后的结果只有一个单词(不能构成单词链),以及输入长度为0的情形。展示部分单元测试代码如下:
char *test_list1[] = { "abc","cbd","dbbw","csw","zde","opl","wxx" };
char *test_list2[] = {"room", "mazhenya", "apple", "elephant","mahaoxiang","gxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzorange","peanut"};
char *test_list3[] = {"abc","cbd","bba","ddb","yz","uv","wx","vw","xy"};
char *test_list4[] = { "xppppy", "fb","ef","bc","de","cd","zpppb","yppppz" };
char *test_list5[] = { "xppppy", "fb","ef","bc","ft","tb","zpppb","yppppz" };
char *test_list6[] = { "xppppy", "ce","ef","bc","de","cd","zpppb","yppppz" };
char *test_list7[] = { "tppppppppz","zppppppppx","ab","ef","bc","de","cd" };
char *test_list8[] = { "ac","bz","cb","cc" };
char *test_list9[] = { "ac","bc","cb","cc" };//有环
char *test_list10[] = { "cx","xy","bc","zd","yz","ab","de","cpppppppppppppppppppd" };
char *test_list11[] = {"abc", "abc", "xyz"};
TEST_METHOD(TestMethod1)
{
char *answer1[] = { "abc","cbd","dbbw","wxx" };
int word_num = 7;
int answer_num = 4;
char **results1 = new char*[word_num + 1];
int res = Core::gen_chain_word(test_list1, word_num, results1, 0, 0, false);
Assert::IsTrue(judge(array2string(results1,res), array2string(answer1,answer_num)));
for (int i = 0; i < res; i++)
delete[] results1[i];
delete[] results1;
}
TEST_METHOD(TestMethod2)
{
int word_num = 7;
int answer_num = 2;
char **results2 = new char*[word_num + 1];
char *answer2[] = { "dbbw", "wxx" };
int res = Core::gen_chain_word(test_list1, 7, results2, 'd', 0, false);
Assert::IsTrue(judge(array2string(results2, res), array2string(answer2, 2)));
}
TEST_METHOD(TestMethod3)
{
int word_num = 7;
int answer_num = 3;
char **results3 = new char*[word_num + 1];
char *answer3[] = { "abc","cbd","dbbw"};
int res = Core::gen_chain_word(test_list1, word_num, results3, 0, 'w', false);
Assert::IsTrue(judge(array2string(results3, res), array2string(answer3, answer_num)));
}
TEST_METHOD(TestMethod4) {
int word_num = 8;
int answer_num = 4;
char **results4 = new char*[word_num + 1];
char *answer4[] = { "room", "mazhenya", "apple", "elephant" };
int res = Core::gen_chain_word(test_list2, word_num, results4, 0, 0, false);
Assert::IsTrue(judge(array2string(results4, res), array2string(answer4, answer_num)));
}
TEST_METHOD(TestMethod5) {
int word_num = 8;
int answer_num = 3;
char **results5 = new char*[word_num + 1];
char *answer5[] = { "mazhenya", "apple", "elephant" };
int res = Core::gen_chain_char(test_list2, word_num, results5, 'm', 't', false);
Assert::IsTrue(judge(array2string(results5, res), array2string(answer5, answer_num)));
}
TEST_METHOD(TestMethod6) {// -c 以t结尾
int word_num = 8;
int answer_num = 2;
char **results = new char*[word_num + 1];
char *answer[] = { "zzzzzzzzzzzzzzzzzzzzzzzzzzzzorange", "elephant" };
int res = Core::gen_chain_char(test_list2, word_num, results, 0, 't', false);
Assert::IsTrue(judge(array2string(results, res), array2string(answer, answer_num)));
}
TEST_METHOD(TestMethod7) {
int word_num = 8;
int answer_num = 2;
char **results = new char*[word_num + 1];
char *answer[] = { "mahaoxiang","gxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"};
int res = Core::gen_chain_char(test_list2, word_num, results, 'm', 0, false);
Assert::IsTrue(judge(array2string(results, res), array2string(answer, answer_num)));
}
TEST_METHOD(TestMethod8) {
int word_num = 9;
int answer_num = 5;
char **results = new char*[word_num + 1];
char *answer[] = { "uv","vw","wx","xy","yz" };
int res = Core::gen_chain_word(test_list3, word_num, results, 0, 0, true);
//output(results, res);
Assert::IsTrue(judge(array2string(results, res), array2string(answer, answer_num)));
}
TEST_METHOD(TestMethod9) {
int word_num = 9;
int answer_num = 4;
char **results = new char*[word_num + 1];
char *answer[] = { "abc","cbd","ddb","bba" };
int res = Core::gen_chain_word(test_list3, word_num, results, 0, 'a', true);
//output(results, res);
Assert::IsTrue(judge(array2string(results, res), array2string(answer, answer_num)));
}
9.计算模块部分异常处理说明
计算模块部分异常处理说明。 在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。
异常1 有环无-r参数 异常发生在输入文件中存在单词环,但是命令设置不支持-r参数,通常发生在忘记设置-r参数的情景下。设计目标是提醒用户检查输入文本或者参数设置是否有误。以下是一个测试循环异常的单元测试用例。
char *test_list3[] = {"abc","cbd","bba","ddb","yz","uv","wx","vw","xy"};
TEST_METHOD(TestMethod10) {
try {
int word_num = 9;
int answer_num = 4;
char **results = new char*[word_num + 1];
int res = Core::gen_chain_word(test_list3, word_num, results, 0, 0, false);
Assert::IsTrue(res == -1);
}
catch (const char* s) {
Assert::IsTrue(strcmp(s, LOOP_ERROR) == 0);
cout << s << endl;
}
}
异常2 输入的头尾字符不符合题目要求异常发生在用户指定的头尾字符不是0也不是小写字母,目的是提醒用户检查设置的头尾字符限制参数。以下是一个头尾字符异常的单元测试。
char *test_list3[] = {"abc","cbd","bba","ddb","yz","uv","wx","vw","xy"};
try {
int word_num = 9;
int answer_num = 4;
char **results = new char*[word_num + 1];
int res = Core::gen_chain_char(test_list3, word_num, results, '+', '-', true);
Assert::IsTrue(res == -1);
}
catch (const char* s) {
Assert::IsTrue(strcmp(s, TAIL_CHAR_ERROR) == 0);
//Assert::IsTrue(s == LOOP_ERROR);
cout << s << endl;
}
异常3 输入的单词列表中含非法单词 异常发生在用户直接调用接口的时候,没有保证每一个单词都是由小写英文单词组成,因此在计算之前也需要进行单词合法性检查。
TEST_METHOD(TestMethod12) {
char *test_list[] = {"happ1we2", "yuer", "opui8op"};
try {
int word_num = 3;
int answer_num = 0;
char **results = new char*[word_num + 1];
int res = Core::gen_chain_word(test_list, word_num, results, 0, 0, false);
}
catch (const char* s) {
Assert::IsTrue(strcmp(s, WORD_ILLEGAL)==0);
cout << s << endl;
}
}
其余还有一些参数错误等异常均在命令行输入处理模块进行处理。
10.界面模块的详细设计过程
在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。
对于界面模块的设计,采用了MFC程序来实现。整个UI界面包括输入,输出,参数选择三个部分,具体如下图所示。
下面对部分MFC元素的实现作解释说明。
(1)选择文件输入还是界面输入的CombBox框
comb_choose_input.AddString(_T("选择文件输入(默认)"));
comb_choose_input.AddString(_T("直接输入"));
comb_choose_input.SetCurSel(0);
在初始化时插入选择枝,并设定默认选择为文件输入,在点击运行后的时间通过GetCurSel()方法来获取用户的选择。
(2)参数选择区mode单选框
使用
GetCheckedRadioButton(IDC_max_word, IDC_max_length)
来判断选择枝
(3)文本框(包括输入,输出,head,tail)
采用
GetDlgItem(ID)->GetWindowText(Cstring)
来获取文本框中的文本,需要注意的是,文本内容获取到后类型为Cstring,可用
CT2A(Cstring.GetBuffer())
来转换Cstring为string。
(4)选择(保存)文件的按钮
使用
CString strFile = _T("");
CFileDialog dlgFile(TRUE, NULL, NULL, OFN_HIDEREADONLY, _T("Describe Files All Files (*.*)|*.*||"), NULL);
if (dlgFile.DoModal())
{
strFile = dlgFile.GetPathName();
file_path = CT2A(strFile.GetBuffer());
}
来调用MFC内封装的选择文件UI,如果要保存文件直接打开文件并将内容写入即可。
11.界面模块与计算模块的对接
详细地描述UI模块的设计与两个模块的对接,并在博客中截图实现的功能。
实现的界面截图
UI模块的运行
在UI模块的运行按钮中绑定事件,点击后,UI模块获取界面上用户输入的信息,转换为相应的参数来调用。
需要注意的是,在运行UI模块时,并不支持通过命令行输入参数进行测试,请直接打开UI模块(MFC_max_word_chain.exe)。如果希望通过命令行打开加参数的方式进行测试,请使用max_word_chain.exe。
对接方式
UI模块在资源文件中引入Core.lib,在MFC_max_word_chainDlg.cpp这个事件响应文件中,需要调用计算模块时,头文件引入Core.cpp。
处理好用户界面输入的信息后,调用
static int gen_chain_word(char* words[], int len, char* result[], char head, char tail, bool enable_loop);
static int gen_chain_char(char* words[], int len, char* result[], char head, char tail, bool enable_loop);
这两个接口进行运算。运算后将result中获取的结果转换为Cstring返回到用户界面。
12.结对的过程
我们两人的结对编程,经历了顺利的开端,略微坎坷的经过和最后圆满的结束。
起初最开始接触结对编程,我们按照领航员与驾驶员的模式。首先两人一起商讨了关于最长链算法的问题,并在有环时如何处理上进行了一段时间的探讨。随后按照驾驶员写代码,领航员检查设计并监督的方式进行,开始效果良好,避免了不少人为bug的产生。
在几次身份对换之后,我们发现,有时在他人的代码基础之上继续完成自己的代码十分困难。思路不断交替,写代码的效率有了一定的下降。在同伴在自己的代码基础上进行完成时,若完成的结果和自己的思路有些冲突,还会造成一些麻烦。好在项目比较小,我们两方也都比较认真且有团队精神,在不断的磨合之中,适应了对方的步伐,磨合的也越来越好。在最后例如单元测试,错误处理等模块的实现中,我们的结对编程比较顺利。
总体来说这次结对编程是一次不错的体验,与他人一起交流,互相交换编程思想的经历难能可贵。虽然在效率上可能有些问题,但最终项目的完成质量不错就是好的。
13.结对编程反思
看教科书和其它参考书,网站中关于结对编程的章节,例如:
http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html
说明结对编程的优点和缺点。
结对的每一个人的优点和缺点在哪里 (要列出至少三个优点和一个缺点)。
结对编程最大的优点在于,两人不断的复审,从而提高代码的质量。在结对编程中,两人不断交换角色,不断审核对方的代码。“当局者迷,旁观者清”,作为旁观者更容易发现代码的问题。同时,写代码时两人不时交流,利于在某些算法或者架构上优化。
当然,结对编程中也会有两人配合不佳导致效率较低的情况。问题过难或过简单都不太适合结对编程,比较浪费时间。
成员 | 优点 | 缺点 |
---|---|---|
马振亚 | (1)检查设计时认真仔细,是个好的领航员 (2)建立测试样例比较完备 (3)为人比较耐心细致,对搭档友好 | 有时不太冷静,容易对代码有误操作 |
马浩翔 | (1)能够自己探索一些解决方案(2)对搭档友善(3)对git的掌握还可以 | 不爱看作业要求 |
14.PSP表格回填
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 30 | 75 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 180 | 120 |
· Design Spec | · 生成设计文档 | 60 | 10 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 10 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | 10 |
· Design | · 具体设计 | 250 | 600 |
· Coding | · 具体编码 | 450 | 480 |
· Code Review | · 代码复审 | 250 | 300 |
· Test | · 测试(自我测试,修改代码,提交修改) | 300 | 720 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 100 | 30 |
· Size Measurement | · 计算工作量 | 20 | 15 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 50 |
合计 | 1710 | 2345 |
15 模块交换与模块松耦合
因为有两个小组的邀请,所以最后和两个小组进行了模块交换,因为跟第一个小组中出现了问题并进行了修改,所以以下博客展示与第一个小组的交换出现的问题。
合作小组1成员: 16061192 汪慕澜 16061103 赵智源
合作小组2成员: 16021160 庄廓然 15061078 杨帅
问题: 问题主要起源于我对接口的理解有误,在第一版程序的时候将类的两个函数public化作为了一个接口,而且这个类是有状态的。但是发现对方的直接声明了一个没有状态的类,声明了类的static方法。之前自己的写法会产生很多warning(warning提示调用接口需要声明一个类的客户端对象),在对面小组的解释后对自己的接口设计进行了重构,对接口又进行了一次封装。
交换效果:
我们的程序在对方的gui上可以完成运行,效果如下:
对方的程序也可以在我们的gui上完成运行: