项目 | 内容 |
---|---|
本次做业所属课程 | 2019BUAA软件工程 |
本次做业要求 | 结对项目-最长单词链 |
我在本课程的目标 | 学会团队合做开发项目,为之后的工做打下基础 |
本次做业的帮助 | 了解结对编程而且体验完整项目开发流程 |
项目地址html
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 90 |
-Estimate | -估计这个任务须要多少时间 | 60 | 90 |
Development | 开发 | 3200 | 3300 |
-Analysis | -需求分析 (包括学习新技术) | 200 | 80 |
-Design Spec | -生成设计文档 | 200 | 180 |
-Design Review | -设计复审 (和同事审核设计文档) | 150 | 90 |
-Coding Standard | -代码规范 (为目前的开发制定合适的规范) | 150 | 60 |
-Design | -具体设计 | 200 | 240 |
-Coding | -具体编码 | 1500 | 1800 |
-Code Review | -代码复审 | 200 | 210 |
-Test | -测试(自我测试,修改代码,提交修改) | 600 | 640 |
Reporting | 报告 | 400 | 900 |
-Test Report | -测试报告 | 210 | 720 |
-Size Measurement | -计算工做量 | 90 | 60 |
-Postmortem & Process Improvement Plan | -过后总结, 并提出过程改进计划 | 100 | 120 |
合计 | 3660 | 4290 |
In computer science, information hiding is the principle of segregation of the design decisions in a computer program that are most likely to change, thus protecting other parts of the program from extensive modification if the design decision is changed. The protection involves providing a stable interface which protects the remainder of the program from the implementation (the details that are most likely to change).node
Written another way, information hiding is the ability to prevent certain aspects of a class or software component from being accessible to its clients, using either programming language features (like private variables) or an explicit exporting policy.git
信息隐藏是为了避免让程序内部的信息直接暴露给用户,为了在设计被改动的时候保护其余部分。程序员
接口在这里就扮演着很重要的角色。类只向外界提供它们实现的接口中规定的方法,全部属性皆为私有。github
In computing and systems design a loosely coupled system is one in which each of its components has, or makes use of,little or no knowledge of the definitions of other separate components.算法
松耦合就是在设计模块的时候只用到了不多或几乎没有的其余模块中的东西,像数据,接口,服务等等。编程
在咱们的程序中,就实现了计算模块函数的封装,只提供直接可用的接口给用户,根据需求对core模块进行封装,生成的.dll文件在命令行程序和GUI中均可以进行使用,而不会暴露给用户。在在互换core模块的时候也较为顺利。api
接口:项目中计算模块接口的设计采用了要求中统一的API,即:数组
# 最多单词数 int gen_chain_word(char* words[], int len, char* result[], char head, char tail, bool enable_loop); # 最多字母数 int gen_chain_char(char* words[], int len, char* result[], char head, char tail, bool enable_loop)
因为在第一次阅读做业要求时已经了解了固定API的要求,所以咱们在第一次实现时已经采用了这个结构。具体算法采用树实现,而不是其余组广泛使用的图结构。架构
类:共有两个类和四个自定义异常结构体。其中Core类为封装好的计算模块,用于计算最长链,node类为树节点信息,自定义异常则用于抛出异常时输出信息。具体结构以下:
class node { public: string word; node* parent; node* first_child; node* next; int word_num; int character_num; __declspec(dllexport) node(string cur_word, int cur_word_num, int cur_character_num); }; class Core { public: __declspec(dllexport) int gen_chain_word(char* words[], int len, char* result[], char head, char tail, bool enable_loop); __declspec(dllexport) int gen_chain_char(char* words[], int len, char* result[], char head, char tail, bool enable_loop); __declspec(dllexport) bool gen_tree(node* cur_node, char* words[], int len, bool enable_loop, char tail, node* word_max_node, node* char_max_node, int words_index[][2]); __declspec(dllexport) bool find_in_chain(node* cur_node, string word); };
自定义异常参见第九节。
函数:计算模块共有五个函数(不包括node类的构造函数),除去API的两个函数gen_chain_word()和gen_chain_char()外,还有递归生成树函数gen_tree(),在树中查找函数find_in_chain()和排序时的比较函数compare()。关键函数的流程图以下:
基本算法为:首先对输入的全部单词words进行排序,并创建索引。此后将每一个单词做为一棵树的根节点建树。建树时根据索引,遍历全部知足和父节点链接条件的子节点,查找该子节点是否在父节点所在链上出现过。若出现过,根据enable_loop标志判断是否抛出成环的异常;反之则将子节点加入树。为了简化查找过程,咱们在建树的过程当中就计算最长单词和最长字母的节点。当全部树建好,可直接经过获得的最长单词链的叶节点向上遍历,找到整个链并输出。所以,咱们项目的独到之处包括:使用树结构、创建索引、建树完成后无需再遍历寻找最长链、自定义异常类型和创新异常单元测试方法。具体内容将在第六节和第九节详细说明。
咱们运行了一个有50个单词的容许成环的文本。优化后的性能分析结果以下:
在初次运行时,咱们发现-r参数下70个单词已经没法在300s内运行完成。在调试的过程当中,咱们发现,时间最长的部分是建树的过程gen_tree(),上图也印证了这个猜测。所以咱们决定对words排序后创建索引,在建树时只需遍历索引指向的首位范围便可。这样作会减慢小数据的速度(5个单词时的运行速度由4ms变为了12ms),但会提高数据较多时的性能。但当实现以后发现,对大数据量时改进仍然不够使人满意。咱们也尝试过对排序后的words去重,可是在单元测试时发现,代码:
int cnt = 1; for (i = 1; i < len; i++) { if (strcmp(words[i], words[i - 1]) != 0) { strcpy_s(words[cnt++], strlen(words[i]) + 1, words[i]); } } len = cnt
中的strcpy部分彷佛会在单元测试中抛出异常,咱们并未查到bug的缘由。在网上的惟一解释是因为被赋值的指针指向了字符串常量,不能被修改,但这个解释并不符合。尽管在release版本中这部分代码能够经过,最终为了保险起见,咱们删除了去重功能。
花费时间最长的函数是find_in_chain(),其做用是在找到一个首字母能够和当前节点尾字母相连的节点时,从当前节点向上查找,若是出现过这个单词,则判断是否有-r参数。若没出现过,则加到当前节点后面。咱们曾想过是否要将遍历改成维护一个“单词是否出现过的标志数组”的形式,但发现这样修改很是复杂,时间有限,没能实现。
此外,生成树之后从新便利寻找最长链也是没有必要的时间消耗,咱们在建树过程当中,直接记录下当前节点的链的单词数和字母数,使得在树建完后,直接获得最长链的叶节点,反向遍历便可获得整条链。
Design by Contract(契约式设计)
It prescribes that software designers should define formal, precise and verifiable interface specifications for software components
共同定义一个精确的接口,知道先验,功能和影响。
优势:
保证模块的正确性
复用容易
文档和设计都是通过精心的撰写,质量比较高
可靠性较强
这次做业中,就是已经设定好了一个模块——core,在这个模块中的函数也是给定的两个接口。给定这些接口就让咱们对这个项目的核心算法有了统一的认识和设计思路,即主要有两个功能,一个经过-w参数,一个经过-c参数来实现生成最长单词链的过程。
可是,这时候,缺点就体现出来了。在使用命令行运行程序的过程当中,咱们输入的命令都应该是一个不会变的,固定的常量,所以,将接口中的char*改成const char*才更符合编程的规范。但因为契约式的编程,没法修改既定的接口规则致使了这些方面的矛盾。
部分测试代码展现
TEST_METHOD(UnitTest_gen_tree5) { node1 = new node(cur_word, cur_word_num, cur_character_num); word_max_node = new node("", 0, 0); char_max_node = new node("", 0, 0); char head = 0, tail = 0; int len = 6; char* words[6] = { "aj", "jhgjh","hjhjbdkjhaksjdfhkjhkjhjd" ,"hdfdrp","pd","ddfghj" }; Assert::AreEqual(core_test.gen_chain_word(words, len, result, head, tail, true), 5); Assert::AreEqual(core_test.gen_chain_char(words, len, result, head, tail, true), 5); } TEST_METHOD(UnitTest_gen_tree6) { char* words[5] = { "aj", "cdfdrp", "ddfghj", "hhgjh", "sjhjb" }; word_max_node = new node("", 0, 0); char_max_node = new node("", 0, 0); char head = 0, tail = 0; int len = 5; Assert::IsFalse(core_test.gen_tree(node1, words, len, false, 0, word_max_node, char_max_node, words_index)); } TEST_METHOD(UnitTest_find_in_chain3) { char* words[5] = { "aj", "cdfdrp", "ddfghj", "hhgjh", "sjhjb" }; Assert::IsFalse(core_test.gen_tree(node1, words, len, false, 0, word_max_node, char_max_node, words_index)); Assert::IsFalse(core_test.gen_tree(node1, words, len, false, 0, word_max_node, char_max_node, words_index)); Assert::IsTrue(core_test.find_in_chain(node1->first_child->first_child, word)); } TEST_METHOD(UnitTest_command_line2) { argc = 4; char* argv[4] = { "Wordlist.exe","-w","-r","../Wordlist/file.txt" }; Assert::IsTrue(command_handler(argc, argv, words, len, head, tail, enable_loop, w_para)); }
被测试函数
测试的函数有五个:
int gen_chain_word(char* words[], int len, char* result[], char head, char tail, bool enable_loop); int gen_chain_char(char* words[], int len, char* result[], char head, char tail, bool enable_loop); bool gen_tree(node* cur_node, char* words[], int len, bool enable_loop, char tail, node* word_max_node, node* char_max_node, int words_index[][2]); bool find_in_chain(node* cur_node, string word); bool command_handler(int argc, char* argv[], char* words[], int &len, char &head, char &tail, bool &enable_loop, bool &w_para);
测试思路
首先明确程序的架构,command_handler是处理命令行参数函数;gen_chain_word和gen_chain_char是两个主函数,在这两个主函数中均调用了get_tree和find_in_chain这两个函数。
从小函数开始测试,根据gen_tree的输入和输出,使用assert函数,返回一个bool类型的值。主要测试重点就在于生成树的过程当中是否遇到禁止环出现还出现环的状况以及树的生成是否正确。
而后是find_in_chain函数,主要功能是找在该路径上是否出现了和这个word同样的word,不知足咱们的条件(一个单词只容许出现一次),返回bool类型的值。在测试时,就用gen_tree先生成一颗树,而后设定多种word让该函数去寻找。
对与两个主函数,由于主要的核心已经测试过,因而采用一些白盒测试的方法,测试其他没有覆盖的状况,调整-h,-t参数等等。
最后是command_handler函数的测试,这部分就是测试输入命令的正确与否。不过由于是命令行输入,已经有了一些状况的限制,须要测试的状况也能够较好地覆盖。测试内容分为正确指令和错误指令,错误指令有包含各类错误:有不正确指令,内容缺失,文件找不到等等。是在考虑的多种用户输入时可能犯下的错误进行相应的匹配和处理。
测试报告及覆盖率报告生成的辛酸史
测试报告
覆盖率报告
这个过程是对我来讲,整个项目,最艰难的一个过程,完成它的耗时远远超过了个人估计值。总的来讲,就是我使用的vs2017社区版没有这个功能而且其中较为流行的插件也均不支持vs2017的社区版(如下简称vs2017,由于专业版有现成的工具:))。
因而我展开了搏斗:
Round1:opencover和ReportGenerator
不适用!vs中缺乏工具。另外vs2015版还能够支持Opencover的一个UI extension,简直不要太简单。
Round2:使用命令行运行测试文件的DLL,生成.trx文件,再转为html文件查看报告
咱们在命令行能够运行测试命令,但必需要转为超级用户后才能生成.trx文件,不然会提示“拒绝访问”。接着,vs2017生成的.trx文件没有能够解析它的工具。咱们尝试了多种,trx2html,还有GitHub上开源的工具,都没法解析它。
Round3:最后,咱们得知了OpenCppCoverage这个软件能够经过命令行拿到覆盖率的html文件
起初,咱们发现,这个命令只能经过调用.exe文件获得一次结果的覆盖率。可是通过一阵思考和对这个软件GitHub主页的参数分析,咱们发现能够经过参数export_type=binary先生成覆盖率的二进制文件,再使用--input_coverage arg命令将多个测试样例覆盖获得的*.cov(二进制文件)进行merge。最后就获得了至关于整个测试文件的覆盖结果,再生成.html文件,方即可视化。先贴结果:
能够看出,覆盖率均达到95%以上,可视化效果也不错。html文件自取,提取码:lpw9 。
惟一的缺点就是须要本身生成不一样的文件,手动运行屡次命令,以下图
之后若要运行更多的测试,咱们的思路就是写.bat脚本,也算事实行了半自动化的测试,时间有限,留给你们提出更好的方法。
命令实例:
OpenCppCoverage.exe --sources=Wordlist --export_type=binary -- Wordlist.exe -w file.txt
OpenCppCoverage.exe --sources=Wordlist ----input_coverage Wordlist.cov --export_type=binary -- Wordlist.exe -w file.txt
附几个参考过的连接
由于计算模块的接口固定,使得诸如“没有出现-w和-c”、“-h后字母数超过一个”、“文件不存在”这样的异常没法在Core中处理,故在其余函数中以输出形式报出异常。本项目Core类实现的异常处理有:成环异常、-h后字母不是英文字母、-t后字母不是英文字母、链长度小于2四种异常。
自定义异常的通用结构以下:
struct ChainLessThen2Exception : public exception { const char * what() const throw () { return "length of chain is less than 2!"; } };
在catch时,只需使用:
try { // some function } catch(ChainLessThen2Exception& e) { cout << e.what(); }
便可输出异常信息。
因为网上没有找到异常检测的方法,咱们创新了如下格式来在单元测试中检测异常。能够看到,这个方法可以检测出是否捕获异常。
成环异常LoopException:对于enable_loop参数为false时,若是检测到环存在,抛出该异常。
测试样例
TEST_METHOD(UnitTest_Loop) { char* words[5] = { "aj", "hdfdrs", "jhgjh", "kjhjh", "sdfghj" }; int len = 5; char head = 0, tail = 0; try { core_test.gen_chain_char(words, len, result, head, tail, false); Assert::IsTrue(false); } catch(struct LoopException &e) { Assert::IsTrue(true); } catch(...) { Assert::IsTrue(false); } }
错误场景:"aj", "hdfdrs", "jhgjh", "kjhjh", "sdfghj"中出现环s..j-j..h-h..s,抛出LoopException异常。
首字母异常HeadInvalidException:若是head不为0,且head后面的字母不在'a'-'z'或'A'-'Z'范围内,抛出异常。
测试样例
TEST_METHOD(UnitTest_Head) { char* words[7] = {"ajsdjfhkhlkjhlakjshduhcuhuhuhuhvuhjhdkajsdfnajksdjkfhksdjahkjdhjdhje" }; try { core_test.gen_chain_char(words, len, result, 1, tail, true); Assert::IsTrue(false); } catch (struct HeadInvalidException &e) { Assert::IsTrue(true); } catch (...) { Assert::IsTrue(false); } }
错误场景:-h参数后跟1,非英文字母,抛出HeadException异常。
尾字母异常TailInvalidException:若是tail不为0,且tail后面的字母不在'a'-'z'或'A'-'Z'范围内,抛出异常.
测试样例
TEST_METHOD(UnitTest_Tail) { char* words[7] = {"ajsdjfhkhlkjhlakjshduhcuhuhuhuhvuhjhdkajsdfnajksdjkfhksdjahkjdhjdhje" }; try { core_test.gen_chain_char(words, len, result, head, 1, true); Assert::IsTrue(false); } catch (struct TailInvalidException &e) { Assert::IsTrue(true); } catch (...) { Assert::IsTrue(false); } }
错误场景:-h参数后跟1,非英文字母,抛出TailException异常。
链长度异常ChainLessThan2Exception:若是head不为0,且head后面的字母不在'a'-'z'或'A'-'Z'范围内,抛出异常。
测试样例
TEST_METHOD(UnitTest_Chain) { char* words[7] = {"ajsdjfhkhlkjhlakjshduhcuhuhuhuhvuhjhdkajsdfnajksdjkfhksdjahkjdhjdhje" }; try { core_test.gen_chain_char(words, len, result, head, tail, true); Assert::IsTrue(false); } catch (struct ChainLessThan2Exception &e) { Assert::IsTrue(true); } catch (...) { Assert::IsTrue(false); } }
错误场景:-h参数后跟1,非英文字母,抛出ChainLessThan2Exception异常。
此外,在Core类之外,咱们已经对输入不合法、文件不存在等异常进行了处理,保证程序不会崩溃。
UI采用qt实现,因为我之前有过一次pyqt开发的经验,上手起来并不难。咱们在底层放置了一些layout,同时也给每一个分区设置了layout,这样作的目的是保证界面缩放时比例不变,但作出来才发现底层layout没能随窗口一块儿变化,致使缩放时比例仍然存在问题,最终决定固定窗口大小。具体结构以下:
实现过程即为拖动控件到layout上,设置layout内控件比例便可,并未用到qt代码,所以此处无需示范代码部分。须要注意的是,因为-c和-w以及两种输入方式都知足同一时刻有且只有一个button被选中,故采用了radio button的方式,这种button默认为互相冲突的,即不可同时选中。但当ui完成后才发现,本想四个按钮分为两组相冲突,实际倒是四个按钮同一时刻只能选择一个。所以使用了QButtonGroup的形式进行分组。
界面主要包括上方的输入区,支持文件路径和直接输入单词;中间的参数和输出区,容许用户选择参数,获得正确结果和报错信息;下方的导出区,容许用户将结果导出到指定路径。
因为GUI上实际只有两个按钮,所以GUI和计算模块只有两个函数进行对接。代码以下:
QtGui_Wordlist::QtGui_Wordlist(QWidget *parent): QMainWindow(parent) { ui.setupUi(this); connect(ui.pushButton_generate_chain, SIGNAL(clicked()), this, SLOT(gen_chain())); connect(ui.pushButton_save_in_file, SIGNAL(clicked()), this, SLOT(save_file())); }
其中gen_chain()和save_file()分别是点击pushButton_generate_chain和pushButton_save_in_file的槽函数。槽函数的实现和正常实现基本相同,在槽函数中调用Core.dll中的api接口,惟一区别在于读取和输出的位置不一样,GUI的数据由text()从box读入,由setPlainText()输出到box。
GUI实现的功能有:
从文本框读入文件路径:
从文本框中输入单词进行查找:
-w和-c两种方式的选择:
-h和-t的使用:
-r的使用:
文件导出:
结对过程:由于第一堂软工课程坐在相邻的位置上,天然而然组成告终对做业的搭档。
为了达到做业目标的要求,只要双方的时间容许就会在一块儿进行结对编程。从最初的计划,设计,到编码,测试,再到最后的博客撰写,都存在着结对完成和明确分工两个状态的存在。
编码过程较为符合结对编程的要求,咱们面向同一个电脑,对项目进行构建,两人轮流编码,轮流复审。在后期,作出了些许的分工。例如我负责测试的工做,他负责gui的工做。这时,项目有什么问题均可以进行随时的交流,效率很高。
在以后的博客撰写中,在共同内容的部分,也进行了分工,写各自较为熟悉的内容,能够更好地展现咱们的项目和思想。
最后,很是感谢个人搭档,这整个任务对我也是一个不小的挑战,他帮助了我许多,让我坚持下来,也收获了很多的知识。
优势:
缺点:
张圆宁:
优势:积极主动;对新事物的尝试和熟悉很快;对问题有必定的探究能力,会寻找各类方法。
缺点:编程能力有待提升,尤为在速度方面还须要进步;对解决某些客观问题上(例如软件自己的限制等方面)没有足够的耐心;对问题的分析不够完善。
牛宇航:
优势:勤于思考,善于钻研;有很强的编程能力;对问题有很强的应变能力。
缺点:无
见第二节。
16061200 陈治齐 16061076 顾展鹏
问题1:咱们的main函数中有部分对于异常的处理,这些异常都是封装好了在core.h头文件中。在使用咱们的主函数和GUI及对方的Core.dll以后,主函数由于找不到Core.h中的自定义异常,编译不经过,对方的exe和GUI没法运行。
解决办法:只要删除咱们main函数中的异常处理模块便可正常运行,更为正规的流程应该是在互换公用模块的时候分享并扩充异常处理模块。
解决结果:修改main中异常处理后成功运行,经过正确样例。
问题2:对方的主函数和GUI没法使用咱们的.dll文件,缘由在于对方是动态调用,咱们是静态调用。对方忘记修改.def文件以适应咱们新的.dll。
咱们组内使用的是静态调用:静态调用比较简单,编译DLL项目前,给.h文件中的函数前加上__declspec(dllexport) ,以生成.lib文件。将.lib文件拷贝到其余项目中后,只需引用.h头文件便可使用.dll(.cpp)文件的函数和类。
合做组使用的是动态调用:加载dll文件,在.def文件中写明.dll中的函数。若可以正确从.dll中取到函数所在地址,直接调用便可完成DLL的动态调用。
解决办法:对方应当更新.def文件为我方的.dll中的函数,便可运行。
修改结果:对方更新.def后成功运行我方.dll,经过正确样例。
这次的做业对我是一个比较大的挑战,尤为是投注了太多精力解决一些版本不兼容,版本不支持的问题,真的让我一度怀疑这门课的训练目标以及课程组是否仔细评估过这许多因素带给咱们的困难,做业量和完成时间的比例以及实际操做的可行性。总之,这第一个做业给了我太多意料以外的负担,为了弥补这几天实习一天后还要写软工的睡眠不足,我要好好休息几天。