项目 | 内容 |
---|---|
本次做业所属课程 | 2019BUAA软件工程 |
本次做业要求 | 结对编程做业 |
我在本课程的目标 | 熟悉结对编程流程 |
本次做业的帮助 | 实践告终对编程的流程,对结对编程的优缺点有了更深的体会 |
本次做业项目github地址 | 项目地址 |
项目地址html
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 |
看教科书和其它资料中关于Information Hiding, Interface Design, Loose Coupling的章节,说明大家在结对编程中是如何利用这些方法对接口进行设计的git
Information Hiding-信息隐藏是一种编程原则,一般与封装一块儿使用。Information Hiding建议对于程序内部的实现进行隐藏,从而防止对于对程序进行粗放的修改。对外提供比较稳定的接口,在使用时可忽略内部的实现。github
Interface Design-在阅读资料过程当中,发现良好的接口设计要遵循许多原则例如单一职责,里氏替换,依赖倒置等,因为本次实现的接口较为简单,基本保证了这些原则。算法
Loose Coupling-即松散耦合。松散耦合系统中,组件对于其余组件定义的系统利用较少,松散耦合的优势是,能够将一些模块自由的替换为其余功能相同的模块。编程
在咱们的设计中,为了保证依赖倒置原则以及采用松散耦合的结构,对于内部的计算类Sovler类用Core类进行额外的封装,Core类仅仅提供两个静态的接口可供调用。这样一来,用户便不用对于Sovler类的内部方法又必定的了解,也不用调用接口前先产生一个Solver对象。架构
计算模块接口的设计与实现过程,设计包括代码如何组织,好比会有几个类,几个函数,他们之间关系如何,关键函数是否须要画出流程图?说明你的算法的关键(没必要列出源代码),以及独到之处。app
计算模块是整个项目的核心,发现这个模块计算的内容有很强的聚合性,所以采用面向对象的方式进行封装,最后抽象出一个实体计算类Solver以及接口类Core,共两个类。Core类的接口每次会实例化一个Solver对象,而后调用max_chain_word函数进行计算。画出一个调用的流程图:函数
算法大体思路:首先拿到问题须要对原问题进行建模,一条单词链相邻两个单词的特征是,前一个单词的末尾等于后一个单词的头,这个能够经过有向图的有向边实现。对于不出现-r参数的状况至关于求图中一些指定点做为起点,一些点做为终点的最大路径问题(采用了一个经典的SPFA算法)。对于出现-r参数的状况归约为求有向有环图中的最大路径问题,属于一个很是经典的NP问题,对于这种状况并无多项式时间的准确算法,所以采用dfs搜索算法。工具
算法的独到之处:采用一图多权值的方法,由于原问题有求最长单词数和字符数两种需求,因此在两个节点边的权值记录了两个,求最大单词数目时使用权值为1,求最长单词数使用权值为目的节点的字符长度。这样对于两种问题的求解方法是基本同样的,只是计算不一样的边权,很好的作到了代码复用。对于输入的单词列表转化为图以后首先采用拓扑排序的方法判断图是否有环,对于-r参数的状况下不必定调用dfs搜索(当判断出图中无环时仍然采用时间复杂度低的SPFA算法)。oop
阅读有关UML的内容:https://en.wikipedia.org/wiki/Unified_Modeling_Language。画出UML图显示计算模块部分各个实体之间的关系(画一个图便可)。
VS2017能够下载安装包,支持自动生成类图,参考博客 。最后生成的类图以下:
计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展现一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展现你程序中消耗最大的函数。
在完成基本程序的运行功能以后,我对程序的性能进行了必定的改进,在这个过程当中是很痛苦的,大概花了两天的时间,其中也走了不少弯路,最后在无环图上优化取得了必定效果,可是有环图改进的幅度很小。
改进的思路:对于无环的状况,尽量采用时间复杂度比较低的算法,权衡再三最后将最终算法改成SPFA算法。对于有环的状况,由于跟许多同窗讨论证明这是一个NP问题,因此能作的改进也是颇有限,主要的思路就是在dfs搜索的时候能剪枝,以及在某些特殊的状况下须要进行特殊处理(好比若是判断出无环,即便带-r参数要调用SPFA函数而不是暴力搜索,以及若是有环的状况下找到一个长度为n的链(n为节点总数)也能够中止搜索获得答案)。对于有环的状况,实际上还能够考虑一些启发式算法好比模拟退火算法等,可是本题目须要输出准确解,启发式算法有必定风险,所以没有采用该方法。
这部分主要有两种状况,一个是没有-r参数的时候调用SPFA方法时的性能,
下面展现一个较小数据集下不带-r的性能分析图:
发现其中耗时最可能是的生成图的函数,说明计算算法性能基本能够。
一个是带有-r参数且图中有环会频繁递归调用DFS函数的状况,这一部分毫无疑问是dfs函数被屡次递归调用。
看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,即契约编程,意为咱们在声明一个函数/方法的时候,对函数的输入和输出所具有的性质是有所指望和规定的。对于全部组件,规定前置条件,后置条件以及不变量。契约编程要求对于前置条件,后置条件,不变量。
契约编程的好处是能够保证程序的健壮性,并严格的分析责任。编程人员仅须要对本身的部分按照契约负责。缺点是,契约式设计比较繁琐,若是每一处都采用契约式设计会费时费力。
具体在咱们的做业中,并未严格按照契约式编程规定前置条件,后置条件以及不变量,更多的是在组件内检测是否符合条件。
计算模块部分单元测试展现。展现出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试获得的测试覆盖率截图,发表在博客中。要求整体覆盖率到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))); }
计算模块部分异常处理说明。 在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。
异常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 输入的单词列表中含非法单词 异常发生在用户直接调用接口的时候,没有保证每个单词都是由小写英文单词组成,所以在计算以前也须要进行单词合法性检查。
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; } }
其他还有一些参数错误等异常均在命令行输入处理模块进行处理。
在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。
对于界面模块的设计,采用了MFC程序来实现。整个UI界面包括输入,输出,参数选择三个部分,具体以下图所示。
下面对部分MFC元素的实现做解释说明。
comb_choose_input.AddString(_T("选择文件输入(默认)")); comb_choose_input.AddString(_T("直接输入")); comb_choose_input.SetCurSel(0);
在初始化时插入选择枝,并设定默认选择为文件输入,在点击运行后的时间经过GetCurSel()方法来获取用户的选择。
使用
GetCheckedRadioButton(IDC_max_word, IDC_max_length)
来判断选择枝
采用
GetDlgItem(ID)->GetWindowText(Cstring)
来获取文本框中的文本,须要注意的是,文本内容获取到后类型为Cstring,可用
CT2A(Cstring.GetBuffer())
来转换Cstring为string。
使用
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,若是要保存文件直接打开文件并将内容写入便可。
详细地描述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返回到用户界面。
咱们两人的结对编程,经历了顺利的开端,略微坎坷的通过和最后圆满的结束。
起初最开始接触结对编程,咱们按照领航员与驾驶员的模式。首先两人一块儿商讨了关于最长链算法的问题,并在有环时如何处理上进行了一段时间的探讨。随后按照驾驶员写代码,领航员检查设计并监督的方式进行,开始效果良好,避免了很多人为bug的产生。
在几回身份对换以后,咱们发现,有时在他人的代码基础之上继续完成本身的代码十分困难。思路不断交替,写代码的效率有了必定的降低。在同伴在本身的代码基础上进行完成时,若完成的结果和本身的思路有些冲突,还会形成一些麻烦。好在项目比较小,咱们两方也都比较认真且有团队精神,在不断的磨合之中,适应了对方的步伐,磨合的也愈来愈好。在最后例如单元测试,错误处理等模块的实现中,咱们的结对编程比较顺利。
整体来讲此次结对编程是一次不错的体验,与他人一块儿交流,互相交换编程思想的经历难能难得。虽然在效率上可能有些问题,但最终项目的完成质量不错就是好的。
看教科书和其它参考书,网站中关于结对编程的章节,例如:
http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html
说明结对编程的优势和缺点。
结对的每个人的优势和缺点在哪里 (要列出至少三个优势和一个缺点)。
结对编程最大的优势在于,两人不断的复审,从而提升代码的质量。在结对编程中,两人不断交换角色,不断审核对方的代码。“当局者迷,旁观者清”,做为旁观者更容易发现代码的问题。同时,写代码时两人不时交流,利于在某些算法或者架构上优化。
固然,结对编程中也会有两人配合不佳致使效率较低的状况。问题过难或过简单都不太适合结对编程,比较浪费时间。
成员 | 优势 | 缺点 |
---|---|---|
马振亚 | (1)检查设计时认真仔细,是个好的领航员 (2)创建测试样例比较完备 (3)为人比较耐心细致,对搭档友好 | 有时不太冷静,容易对代码有误操做 |
马浩翔 | (1)可以本身探索一些解决方案(2)对搭档友善(3)对git的掌握还能够 | 不爱看做业要求 |
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 |
由于有两个小组的邀请,因此最后和两个小组进行了模块交换,由于跟第一个小组中出现了问题并进行了修改,因此如下博客展现与第一个小组的交换出现的问题。
合做小组1成员: 16061192 汪慕澜 16061103 赵智源
合做小组2成员: 16021160 庄廓然 15061078 杨帅
问题: 问题主要起源于我对接口的理解有误,在初版程序的时候将类的两个函数public化做为了一个接口,并且这个类是有状态的。可是发现对方的直接声明了一个没有状态的类,声明了类的static方法。以前本身的写法会产生不少warning(warning提示调用接口须要声明一个类的客户端对象),在对面小组的解释后对本身的接口设计进行了重构,对接口又进行了一次封装。
交换效果:
咱们的程序在对方的gui上能够完成运行,效果以下:
对方的程序也能够在咱们的gui上完成运行: