项目 | 内容 |
---|---|
这个做业属于哪一个课程 | 2019春BUAA SCSE软件工程 |
这个做业的要求在哪里 | 结对项目-最长单词链 |
我在这个课程的目标是 | 学习结对编程 |
这个做业在哪一个具体方面帮助我实现目标 | 两人合做完成项目 |
最长单词链c++
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | ||
· Estimate | · 估计这个任务须要多少时间 | 10 | 30 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 45 | 60 |
· Design Spec | · 生成设计文档 | 45 | 45 |
· Design Review | · 设计复审 (和同事审核设计文档) | 45 | 45 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 45 | 45 |
· Design | · 具体设计 | 60 | 90 |
· Coding | · 具体编码 | 1000 | 1500 |
· Code Review | · 代码复审 | 60 | 120 |
· Test | · 测试(自我测试,修改代码,提交修改) | 360 | 800 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 60 | 60 |
· Size Measurement | · 计算工做量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 过后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 1790 | 2855 |
咱们的程序设计对于信息隐藏的最具体体现可能就是对于Core 类的封装,在实现时咱们使用了大量的函数,最终封装出了两个,这样就可以很好的隐藏起其余的函数,只提供了两个接口。对于接口,我以为主要仍是要实现的合理,好比简单明了的名称,以及后续的可扩展性,这些是比较重要的。在此次做业里,咱们也是按照规定好的接口完成了任务。至于低耦合,我以为咱们仍是能够的,将各大部件都进行了分离,好比计算、GUI、输入输出等,所以咱们的撰写过程里遇到BUG时,处理也比较轻松。git
计算模块最终的函数是get_chain_word()和get_chain_char(),在个人编写代码的过程当中,发现这两种状况十分的类似,几乎就是能够当作一种状况来考虑。咱们将26个字母视为节点,这样可以必定程度上加快效率。每一个节点都要保存最长单词链的长度,一个包含着全部以它为头字母的字母链,以及这个字母的最长链。github
总体共使用了一个类以及14个函数。封装时,咱们将这些函数分装到了Core类中,并导出Dll。
算法
其主要调用关系以下图。首先判断是否容许环路存在,若是容许不环路存在,就调用get_Chain()函数来处理。若是容许环路存在,就调用get_Chain_With_R()函数来处理。get_Chain()和get_Chain_With_R()都是靠传入的参数来判断是要求单词数仍是字符数。进入这两个函数后,对整个图进行初始化,对每一个单词,若是判断的是单词数目最多,就将其长度设置为1,不然就将其长度设置为字符数,这样就能够将两种状况统一判断了。初始化后判断单词中是否存在环路,若是不容许环路却出现了环路报异常。而后就开始生成起始节点,而后对每一个起始节点开始判断其最长路,最终找出最长链,将其保存在result中。其计算过程就是对每一个节点找出其对应的最长链,并将最长链挂在下面,最终就能找到起始节点的最长链。
编程
最重要的函数就是find_Longest_Chain()函数和find_Longest_Chain_With_R()函数。对于find_Longest_Chain()函数,因为没有环路,所以每一个字母实际上只须要计算一次,若是发现这个字母已经计算过了,就直接返回它的最长链的长度。若是没计算过,就判断它是否是到达告终尾。若是是结尾,根据可能规定的结尾字母来判断这个字母的最长链长度。数组
没有被计算过也不是结尾时,就对这个字母下的全部单词的结尾字母都判断一遍,找到里面最长的,最终计算出字母的最长链长度和最长链。函数
对于find_Longest_Chain_With_R()就稍稍有点区别,由于一个字母能够被屡次用到,因此就不能经过判断这个字母是否被计算过来获得它的最长链。咱们的作法是,针对每一次递归调用,找到以这个字母为开头,没有在这一层递归调用中判断过,也不在临时的判断路径,也没有被肯定下来最终路径的最长单词,在进行递归判断这个单词的尾字母。经过这种方式,可以访问到整个图。同时也不会出现重复访问。
工具
在每次调用的结束前,都要判断,此次找到的最长路径是否比如今的最长路径长,若是更加长的话,就将新的路径替换掉旧的路径。性能
算法的关键之处在于将两种不一样的要求合二为一,同时只使用26个字母做为节点,将全部以某个字母为开头的单词所有保存在节点的链表之中,而不是针对大量的单词,以单词做为节点来考虑。单元测试
在封装的的Dll里Core类中只暴露了两个函数get_chain_word()和get_chain_char()。
在性能这一块,咱们最开始的想法就是单独保存每一个单词,将单词视为图中的节点,可是后来发现这样作会致使图太大,临接矩阵的生成就要花费大量的时间,更别说去判断里面是否存在环路了。所以咱们考虑26个字母,将字母做为图来看,将以这个字母开头的单词所有保存在链表之中,再来进行判断。因为改进前尚未写的太深,不少重要函数都尚未进行编写,改进的时间花费的很少,两个小时就完成了新的设计。
使用性能分析工具生成测试100个点的带环图的结果如图,能够发现,在执行的过程当中,find_Longest_Chain_With_R()这个函数递归调用了屡次,占用了整个executiveCommand()函数的较长时间,另外的时间都花在了输出结果到文件之中。
Design by Contract的优势
首先优势就是可以有一个规范,这样子对于大型团队而言,人员众多,可以减小代码中的风格差别,还有接口,这样可以让这种大型项目比较融洽的进行下去。不至于最后又要统一各类各样的接口、函数而浪费时间。
Design by Contract的缺点
其最主要的缺点在于,若是要制定详细的规范,会花费大量的时间,尤为是在时间较为紧迫的状况下,不得不去稍微放款点标准,毕竟不可能在一开始就想到某些接口的具体实现,这个须要必定时间的判断。最终拖累了项目。而在人员较少的时候,当面沟通比较方便的时候,能够在撰写的同时约定好,不用事先进行约定。
如何融入做业
其实咱们在一开始也是约定好了一系列的标准,而且在一开始咱们是比较严格的在遵照,可是在后期,在我完成的那部分里出现了较大的问题,最终致使几乎是从新更换了一个方法,于是最终可能某些地方没有遵照约定,好比某些函数里的变量的命名,在不一样的函数里存在着相同的变量名可是意义却不同,也存在着两个数据的功能几乎一致,可是却不能将其调整为一个数据的现象。这是个人疏忽。
单元测试覆盖率以下:
部分测试代码展现
TEST_METHOD(RightTest) { char * argv_temp[100]; int argc_temp = 7; ofstream outputFile("temp.txt"); outputFile << "aaa abb bbb bcc ccc cdd"; outputFile.close(); argv_temp[0] = (char*)malloc(sizeof(char) * 14); strcpy(argv_temp[0], "WordList.exe"); argv_temp[1] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[1], "-w"); argv_temp[2] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[2], "-h"); argv_temp[3] = (char*)malloc(sizeof(char) * 2); strcpy(argv_temp[3], "a"); argv_temp[4] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[4], "-t"); argv_temp[5] = (char*)malloc(sizeof(char) * 2); strcpy(argv_temp[5], "c"); argv_temp[6] = (char*)malloc(sizeof(char) * 10); strcpy(argv_temp[6], "temp.txt"); string error_message; Assert::AreEqual((double)kErrorNone, (double)executiveCommand(argc_temp, argv_temp, true, &error_message)); Assert::AreEqual(error_message.c_str(), ""); }
此样例测试测试最为简单的状况,只有一个方向,且没有分支的。
TEST_METHOD(RightTestWithR) { char * argv_temp[100]; int argc_temp = 8; ofstream outputFile("temp.txt"); outputFile << "aaa abb acc baa bbb bcc caa cbb ccc"; outputFile.close(); argv_temp[0] = (char*)malloc(sizeof(char) * 14); strcpy(argv_temp[0], "WordList.exe"); argv_temp[1] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[1], "-w"); argv_temp[2] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[2], "-h"); argv_temp[3] = (char*)malloc(sizeof(char) * 2); strcpy(argv_temp[3], "a"); argv_temp[4] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[4], "-t"); argv_temp[5] = (char*)malloc(sizeof(char) * 2); strcpy(argv_temp[5], "c"); argv_temp[6] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[6], "-r"); argv_temp[7] = (char*)malloc(sizeof(char) * 10); strcpy(argv_temp[7], "temp.txt"); string error_message; Assert::AreEqual((double)kErrorNone, (double)executiveCommand(argc_temp, argv_temp, true, &error_message)); Assert::AreEqual(error_message.c_str(), ""); }
此样例较上同样例略复杂一些,为全联通三角形,且为双向
TEST_METHOD(ErrorCirculation) { char * argv_temp[100]; int argc_temp = 7; ofstream outputFile("temp.txt"); outputFile << "aaa abb acc baa bbb bcc caa cbb ccc"; outputFile.close(); argv_temp[0] = (char*)malloc(sizeof(char) * 14); strcpy(argv_temp[0], "WordList.exe"); argv_temp[1] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[1], "-w"); argv_temp[2] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[2], "-h"); argv_temp[3] = (char*)malloc(sizeof(char) * 2); strcpy(argv_temp[3], "a"); argv_temp[4] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[4], "-t"); argv_temp[5] = (char*)malloc(sizeof(char) * 2); strcpy(argv_temp[5], "c"); argv_temp[6] = (char*)malloc(sizeof(char) * 10); strcpy(argv_temp[6], "temp.txt"); string error_message; try { executiveCommand(argc_temp, argv_temp, true, &error_message); } catch (exception &e) { Assert::AreEqual(e.what(), "Error: Found circulation in words"); } }
此样例为较上述样例少了-r参数,故会抛出异常。
TEST_METHOD(RightTestWithR) { char * argv_temp[100]; int argc_temp = 8; ofstream outputFile("temp.txt"); outputFile << "aaa abb acc bdd bee cff cgg dhh hii"; outputFile.close(); argv_temp[0] = (char*)malloc(sizeof(char) * 14); strcpy(argv_temp[0], "WordList.exe"); argv_temp[1] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[1], "-w"); argv_temp[2] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[2], "-h"); argv_temp[3] = (char*)malloc(sizeof(char) * 2); strcpy(argv_temp[3], "a"); argv_temp[4] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[4], "-t"); argv_temp[5] = (char*)malloc(sizeof(char) * 2); strcpy(argv_temp[5], "c"); argv_temp[6] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[6], "-r"); argv_temp[7] = (char*)malloc(sizeof(char) * 10); strcpy(argv_temp[7], "temp.txt"); string error_message; Assert::AreEqual((double)kErrorNone, (double)executiveCommand(argc_temp, argv_temp, true, &error_message)); Assert::AreEqual(error_message.c_str(), ""); }
此样例为一个简单的树,只有一个根节点。
TEST_METHOD(RightTestWithR) { char * argv_temp[100]; int argc_temp = 8; ofstream outputFile("temp.txt"); outputFile << "aaa abb acc bdd bee cff cgg dhh hii jkk khh"; outputFile.close(); argv_temp[0] = (char*)malloc(sizeof(char) * 14); strcpy(argv_temp[0], "WordList.exe"); argv_temp[1] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[1], "-w"); argv_temp[2] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[2], "-h"); argv_temp[3] = (char*)malloc(sizeof(char) * 2); strcpy(argv_temp[3], "a"); argv_temp[4] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[4], "-t"); argv_temp[5] = (char*)malloc(sizeof(char) * 2); strcpy(argv_temp[5], "c"); argv_temp[6] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[6], "-r"); argv_temp[7] = (char*)malloc(sizeof(char) * 10); strcpy(argv_temp[7], "temp.txt"); string error_message; Assert::AreEqual((double)kErrorNone, (double)executiveCommand(argc_temp, argv_temp, true, &error_message)); Assert::AreEqual(error_message.c_str(), ""); }
此样例为稍复杂的树,有多个根节点,一块儿组成了森林。
计算模块异常主要分为 部分,一是命令格式错误,例如-w和-c均不存在或均存在,命令参数重复,首尾字符长度不为1等,这些是在接收到命令时就能得出结论的。二是文件相关错误,包括读写错误,也包括读取string时的错误;三是文本错误,例如没有-r指令时却有环路。
对此咱们解决办法是当遇到错误时抛出异常,基本格式以下:
std::logic_error ex("Error: Repeated parameter -c"); throw exception(ex);
而在CLI或者GUI的模块中,对此异常进行处理,例如:
try{ executiveCommand(argc, argv, true, &result_string); } catch (exception &e){ cout << e.what() << endl; system("pause"); return 0; }
此时程序将会中止后续的运行,并显示错误提示信息。
例如-w -c参数同时出现:
// -w -c 参数均出现 TEST_METHOD(CommandParsing_CoexistenceOfWC) { char * argv_temp[100]; int argc_temp = 4; ofstream output_file("temp.txt"); output_file.close(); argv_temp[0] = (char*)malloc(sizeof(char) * 14); strcpy(argv_temp[0], "WordList.exe"); argv_temp[1] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[1], "-w"); argv_temp[2] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[2], "-c"); argv_temp[3] = (char*)malloc(sizeof(char) * 10); strcpy(argv_temp[3], "temp.txt"); string error_message; try { executiveCommand(argc_temp, argv_temp, true, &error_message); } catch (exception &e) { Assert::AreEqual(e.what(), "Error: Coexistence of -w and -c"); } }
例如须要读取的文件路径不存在
// 须要读取的文件路径不存在 TEST_METHOD(GetWords_getWordsFromFile_noFile) { remove("temp.txt"); GetWords get_words; Assert::IsFalse(get_words.getWordsFromFile("temp.txt")); }
例如没有-r指令时却有环
// 没有-r指令却有环路 TEST_METHOD(Core_circle_without_r) { char * argv_temp[100]; int argc_temp = 3; ofstream outputFile("temp.txt"); outputFile << "abc cba"; outputFile.close(); argv_temp[0] = (char*)malloc(sizeof(char) * 14); strcpy(argv_temp[0], "WordList.exe"); argv_temp[1] = (char*)malloc(sizeof(char) * 3); strcpy(argv_temp[1], "-w"); argv_temp[2] = (char*)malloc(sizeof(char) * 10); strcpy(argv_temp[2], "temp.txt"); string error_message; try { executiveCommand(argc_temp, argv_temp, true, &error_message); } catch (exception &e) { Assert::AreEqual(e.what(), "Error: Found circulation in words"); } }
程序的GUI使用Qt进行设计。程序UI以下:
其中-w和-c是单选按钮,-h,-t,-r是复选按钮,若使用-h,-t则须要同时在其对应的输入框中输入相应首尾字母。Reading file path和Reading string text是单选按钮,分别表示从指定文件路径读取文本,或直接输入文本,路径或文本均在输入框1中输入。点击Executive 后将会解析命令,若是执行正确,结果将会显示在输出框中,不然将会弹出错误提示信息。输入框2指定导出文件路径,默认为当前路径下的solution.txt文件,点击Export result to按钮便可将输出框中的内容导出到指定文件。
GUI部分仅涉及界面及相关逻辑,命令解析后的执行均交给API部分执行。GUI的主体部分是QWDialog类,其包含以下变量:
包含以下函数:
整个程序主要分为3个部分,分别为负责计算的Core模块,负责获取命令的界面模块(分别包括CLI和GUI),负责在二者之间传递数据,解析命令,文件读写的API模块,此部分主要介绍API模块。
API模块主要由两部分组成,第一部分是文件读写部分,第二部分是命令解析部分。
首先是文件读写部分,主要包含于GetWords.h和GetWords.cpp文件中,文件读写部分包含一个类GetWords,包含以下变量:
包含以下函数:
负责从文件中或string中读取并分割单词,返回获取到的单词列表,将指定单词列表输出到指定文件。
而后是命令解析和数据传递部分,主要包含于CommandParsing.h和CommandParsing.cpp文件中,首先其中定义了两个enum,分别用于表示命令和错误代码:
包含一个函数:
参数含义:命令参数列表,命令参数数量,文原本源(即从文件中读取仍是直接输入),result_string用于返回字符串形式的错误信息,是否储存至文件(若不储存至文件,result_string将会在正确运行的状况下,储存运行结果),储存结果的文件路径。
executiveCommand函数运行逻辑以下:
首先进行初始化
统计命令参数,此时会对命令的合法性进行检测,例如是否存在重复参数,-w和-c是否均存在或均不存在
若是命令没有语法错误,将会调用GetWords类,计算模块逐步进行 文件读取,分解得到单词列表,计算结果,同时也会对文件是否存在等进行检测
上述步骤均正确后,将结果写入指定文件或返回至调用者
而GUI和CLI则会对调用executiveCommand函数,实现与计算模块的对接,例如:
CLI:
GUI:
咱们两人结对时间为第二周周五(即3月1日),当天即见面并进行讨论。在后续过程当中保持着较高频率的结对编程(两次见面之间的间隔通常不超过2天)。
两人第一次见面时的讨论照片:
结对编程的优势:互相监督,可以当场发现编写时产生的小BUG,同时可以及时交流,而且避免了一我的写代码时出现的"摸鱼"的状况。
结对编程的缺点:相比于两我的同时写,代码量有所降低,尤为是一些比较简单的代码,结对编程时比较浪费时间。
同伴的优势:
同伴的缺点
交换同窗及学号
姓名 | 学号 |
---|---|
牛雅哲 | 16131059 |
王文珺 | 16061007 |
咱们两组均将模块封装为了dll,对方采用的是cmake进行编译,可使用vs直接打开,故测试时并无遇到不少问题。
在测试的过程当中,咱们发现对方没有严格按照做业的要求对接口进行定义,例如输入和输出字符数组,他们采用的是string类,故在调用前,咱们须要将界面模块和API模块获得的字符数组拼接成为一个string。而对于控制台输出能够直接采用cout进行输出,而对于文件的输出,因为咱们是封装了一个函数进行文件输出,因此须要将string转化为字符数组做为参数进行传递,但本质上没有太大区别。
除此以外,对方的函数比规定多了一个参数,用于具体的返回错误信息,而咱们对此采用的是使用throw抛出异常的方式。
在阅读对方的源代码的过程当中,咱们发现对方定义了大量的自定义结构,枚举,使得代码的可读性较高,哪怕在没有注释的状况下阅读也不会很困难,这一点值得咱们学习。