项目 | 内容 |
---|---|
所属课程 | 2019春季计算机学院软件工程(任健)(北京航空航天大学) |
做业要求 | 这里 |
课程目标 | 提高本身的编程水平,拿一个合适的分数 |
这个做业在哪一个具体方面帮助我实现目标 | 学习结对编程 |
binggge/longestWordChain 共有两个分支,master分支为命令行程序和UI界面,是最终提交分支,UI分支为单独的用户界面程序。c++
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 15 | 30 |
· Estimate | · 估计这个任务须要多少时间 | 15 | 30 |
Development | 开发 | ~1400 | 1475 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 120 |
· Design Spec | · 生成设计文档 | 120 | 90 |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 15 |
· Design | · 具体设计 | 50 | 30 |
· Coding | · 具体编码 | ~360 | 600 |
· Code Review | · 代码复审 | 120 | 90 |
· Test | · 测试(自我测试,修改代码,提交修改) | 480 | 500 |
Reporting | 报告 | 255 | 195 |
· Test Report | · 测试报告 | 120 | 120 |
· Size Measurement | · 计算工做量 | 15 | 15 |
· Postmortem & Process Improvement Plan | · 过后总结, 并提出过程改进计划 | 120 | 60 |
合计 | ~1800 | 1700 |
在编程过程当中,对于每一个类隐藏一些其特有的属性,防止它们被其余类或方法修改。使用信息隐藏不只有助于保证一些信息的安全性,也有助于维护程序的健壮性。参考git
在咱们的程序中,咱们对外界不须要关心的数据都进行了隐藏,如图的边,节点等。其余类,如读入,GUI等不能对这些数据随意访问操做,只能经过规定的接口进行,这样咱们的数据修改都是可控的。程序员
外界应该只关心模块的接口而不是其具体实现。这样在修改模块内部逻辑提高运行效率时能够不须要改变接口。在咱们的计算程序中,对外界开放的接口只有题目中要求的两个,其余方法都是私有的。用户只须要关心传入传出数据的合法性,对于咱们计算模块的具体实现不须要关心。github
在计算机运算和系统设计中,一个松耦合的系统中的每个组件对其余独立组件的定义所知甚少或一无所知。子范围包括类、接口、数据和服务之间的耦合。松耦合是紧耦合的对立面。
参考 模块之间联系越密切,表示他们之间的耦合度越高。耦合度高的程序难以维护,又容易有更多的隐藏bug。所以松耦合时十分必要的。算法
咱们在完成代码编写后,与其余组同窗互换了GUI和DLL,另外一个组是周二的白世豪(16061167),宋卓洋(16061170)组。因为生成dll的教程相似,并无出现什么问题,对于无异常状况能够正常运行,以下图所示:编程
其中Core.dll是咱们生成的dll,DLL1.dll是另一个组的dll,通过测试,加载两个dll时GUI的行为一致。
安全
整个程序的逻辑大体以下图所示:微信
由于GUI程序是不会涉及到普通的参数正确性的(只有关于指定头尾的字母是否正确),因此 出于解耦合的须要,咱们并无将处理参数的程序集成在Core类之中,而是独立出来,在main函数中判断参数的正确性。Core类中确保接受到的是正确的单词组char *words[]
和其余合适的参数。咱们暴露在外的只有两个接口。函数
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);
其余的部分以private
成员的形式,供内部使用。工具
而后每一个接口中对单词的处理也须要分红两种。
\\没有-r选项,检测有环时直接报错,无环时以宽度优先搜索的形式(不会陷入死循环)找到最长单词链; \\有-r选项,不须要检测是否有环,直接以深度优先搜索的形式搜索最长单词链; \\伪代码以下 if(!enable_loop) { roundtest();//有环时抛出错误 createMap();//创建bfs的地图 BFS();//宽度优先搜索 getBFSResult();//处于节省时间和空间的考虑,上一步宽度优先搜索并无保存路径,而是得出了最长的长度,而后根据长度倒推出路径。 } else { createMap();//创建dfs的地图 DFS();//深度优先搜素 getDFSResult();//推导最终路径,缘由同上 }
基础的函数大体就是这些,两种类型chain_word
和chain_char
的各有一套。而后咱们还引入了get_tails()
就是在扫描边的时候,排除不可能的首尾选项。大体原理就是当不成环的时候,出度为0的点才多是尾字母,不然不多是最长链。这样咱们能够剪枝,减小搜索时的复杂度。
我使用了自动化工具生成类图与调用关系,参考工具Github,生成的调用关系以下: 考虑到自动生成的类图可能有些难懂,我又手画了一个简单的类图供参考。
总体上说,咱们采起了BFS解决没有-r的状况,使用DFS解决有-r的状况。
首先是没有-r的部分。因为算法在设计之初就已经考虑到了一些优化,咱们重复执行同一条指令50次,取得更好的采样效果。如图所示,整个算法的耗时最长是在建图与删重边。程序在输入8000个单词时运算时间在1s之内。所以没有作进一步改进,这里介绍一下完成的优化。
咱们主要完成了如下几个优化:
在含有-r的部分,考虑到这是一个NP问题,并且要求的是准确解,不是近似解,咱们采用回溯法进行深度搜索。程序输入90个单词,摘自Wikipedia的一个随机网页,运行时间3分04秒。
能够发现,程序在DFS上耗费了大量的时间。咱们对于这种模式依然作了一些优化,咱们同BFS同样减小了可能的起点,由于起点的出度>=入度,终点的入度>=出度,也有必定效果。
此外,咱们还在总体上对程序进行了优化。考虑处处理器支持AVX指令集,咱们在编译选项中选择了支持生成AVX2指令的编译选项,取得了不错的效果,对于一组较复杂的测试用例,运行时间从2分58秒减小到2分03秒,提高接近1/3。
契约式设计如同它的名字同样,讲究一种契约精神,提供一个接口,只接受合规的输入,保证输出合规。对于错误输入,能够不进行容错处理,而是经过抛出异常等形式。
根据维基百科的介绍,契约式设计一般包含:
- 可接受和不可接受的值或类型,以及它们的含义
- 返回的值或类型,以及它们的含义
- 可能出现的错误以及异常状况的值和类型,以及它们的含义
- 反作用
- 先验条件
- 后验条件
- 不变条件
- (不太常见)性能上的保证,如所用的时间和空间
它的优势十分明显,首先它下降了代码编写的复杂程度,由于程序员不须要对错误输入进行处理容错。第二它简化了测试,由于其明确了测试的范围与内容。第三它使整个程序更健壮,在保证每一个模块都遵照其本身的契约与整理逻辑正确时,能够论证总体的正确性。
它也有一些缺点。首先,全部程序员写的代码的行为必须遵照契约,不然契约就是无效的,这也是契约式设计的重要前提。第二,每段代码都必须通过论证,确保它遵照契约,工做量较大。
在咱们的结对做业中,咱们首先明确了每一个人主要负责的部分,对于有交集的函数等明确了行为,至关于进行了契约式设计。同时咱们明确了采起抛出异常到上层的行为处理异常而不是进行容错。我认为明确这种总体上的规范的行为就是一种契约式设计。
单元测试总体覆盖率91%。未覆盖到的部分主要是抛出异常的if语句。
计算模块暴露的接口共有两个,分别为
咱们对其进行了单元测试。测试的流程是测试程序加载一组测试数据,放入char * words[]中,与一些参数一块儿传入函数,获得返回的result后加载标答并逐个比较,以下面的代码片断所示:
TEST_METHOD(TestMethod1) { Core * core = new Core(); // do sth to update words core->gen_chain_word(words1, len, result1, 0, 0, false); // get true value into realAnswer Assert::AreEqual(51, length_of_result1); for (int i = 0; i < length_of_result1; i++) { Assert::AreEqual(strcmp(result1[i], realAnswer[i]), 0); } }
对于正常数据,咱们使用随机生成+对拍的方式检验。 对于异常数据,咱们构造了如下几个特殊状况:
其中对于有环的状况,咱们还分了如下几种状况:
如下为单元测试展现,因为太长默认收起。
注意,请依次运行每一个单元测试,不要一块儿运行,防止出错。
TEST_METHOD(TestMethod1) { Core * core = new Core(); int len = readFile1("../WordChainUnitTesr/words1.txt"); core->gen_chain_word(words1, len, result1, 0, 0, false); int len2 = readFile1("../WordChainUnitTesr/solution1.txt"); Assert::AreEqual(51, len2); for (int i = 0; i < len2; i++) { Assert::AreEqual(strcmp(result1[i], words1[i]), 0); } }其中 words1.txt, solution1.txt能够在Github上找到。
TEST_METHOD(TestMethod2) { Core * core = new Core(); int len = readFile1("../WordChainUnitTesr/words1_1.txt"); core->gen_chain_char(words1, len, result1, 0, 0, false); int len2 = readFile1("../WordChainUnitTesr/solution1_1.txt"); Assert::AreEqual(51, len2); for (int i = 0; i < len2; i++) { Assert::AreEqual(strcmp(result1[i], words1[i]), 0); } }其中 words1_1.txt, solution1_1.txt能够在Github上找到。
TEST_METHOD(TestMethod3) { Core * core = new Core(); int len = readFile1("../WordChainUnitTesr/words2.txt"); core->gen_chain_word(words1, len, result1, 0, 0, true); int len2 = readFile1("../WordChainUnitTesr/solution2.txt"); Assert::AreEqual(4, len2); for (int i = 0; i < len2; i++) { Assert::AreEqual(strcmp(result1[i], words1[i]), 0); } }其中 words2.txt, solution2.txt能够在Github上找到。
TEST_METHOD(TestMethod4) { Core * core = new Core(); int len = readFile1("../WordChainUnitTesr/words2_1.txt"); core->gen_chain_char(words1, len, result1, 0, 0, true); int len2 = readFile1("../WordChainUnitTesr/solution2_1.txt"); Assert::AreEqual(3, len2); for (int i = 0; i < len2; i++) { Assert::AreEqual(strcmp(result1[i], words1[i]), 0); } }其中 words2_1.txt, solution2_1.txt能够在Github上找到。
TEST_METHOD(TestMethod5) { Core * core = new Core(); int len = readFile1("../WordChainUnitTesr/words3.txt"); core->gen_chain_word(words1, len, result1, 'e', 0, false); int len2 = readFile1("../WordChainUnitTesr/solution3.txt"); Assert::AreEqual(4, len2); for (int i = 0; i < len2; i++) { Assert::AreEqual(strcmp(result1[i], words1[i]), 0); } }其中 words3.txt, solution3.txt能够在Github上找到。
TEST_METHOD(TestMethod6) { Core * core = new Core(); int len = readFile1("../WordChainUnitTesr/words4.txt"); core->gen_chain_word(words1, len, result1, 0, 'e', false); int len2 = readFile1("../WordChainUnitTesr/solution4.txt"); Assert::AreEqual(4, len2); for (int i = 0; i < len2; i++) { Assert::AreEqual(strcmp(result1[i], words1[i]), 0); } }其中 words4.txt, solution4.txt能够在Github上找到。
TEST_METHOD(TestMethod7) { Core * core = new Core(); int len = readFile1("../WordChainUnitTesr/words5.txt"); core->gen_chain_word(words1, len, result1, 'c', 'e', false); int len2 = readFile1("../WordChainUnitTesr/solution5.txt"); Assert::AreEqual(2, len2); for (int i = 0; i < len2; i++) { Assert::AreEqual(strcmp(result1[i], words1[i]), 0); } }其中 words5.txt, solution5.txt能够在Github上找到。
TEST_METHOD(TestMethod8) { Core * core = new Core(); int len = readFile1("../WordChainUnitTesr/words6.txt"); core->gen_chain_char(words1, len, result1, 'e', 'e', true); int len2 = readFile1("../WordChainUnitTesr/solution6.txt"); Assert::AreEqual(3, len2); for (int i = 0; i < len2; i++) { Assert::AreEqual(strcmp(result1[i], words1[i]), 0); } }其中 words6.txt, solution6.txt能够在Github上找到。
TEST_METHOD(TestMethod9) { Core * core = new Core(); int len = readFile1("../WordChainUnitTesr/words7.txt"); core->gen_chain_word(words1, len, result1, 0, 0, false); int len2 = readFile1("../WordChainUnitTesr/solution7.txt"); Assert::AreEqual(13, len2); for (int i = 0; i < len2; i++) { Assert::AreEqual(strcmp(result1[i], words1[i]), 0); } delete core; }其中 words7.txt, solution7.txt能够在Github上找到。
TEST_METHOD(TestMethod10) { try { Core * core = new Core(); int len = readFile1("../WordChainUnitTesr/words8.txt"); core->gen_chain_word(words1, len, result1, 0, 0, false); //RAISE EXCEPTION "Find a loop but no -r" } catch (const char* msg) { Assert::AreEqual(strcmp(msg, "Find a loop but no -r"), 0); } }其中 words8.txt能够在Github上找到。
TEST_METHOD(TestMethod11) { try { Core * core = new Core(); int len = readFile1("../WordChainUnitTesr/words9.txt"); core->gen_chain_word(words1, len, result1, 0, 0, false); //RAISE EXCEPTION "Find a loop but no -r" } catch (const char* msg) { Assert::AreEqual(strcmp(msg, "Find a loop but no -r"), 0); } }其中 words9.txt能够在Github上找到。
TEST_METHOD(TestMethod12) { try { Core * core = new Core(); int len = readFile1("../WordChainUnitTesr/words10.txt"); core->gen_chain_word(words1, len, result1, 0, 0, false); //RAISE EXCEPTION "Find a loop but no -r" } catch (const char* msg) { Assert::AreEqual(strcmp(msg, "No chains found"), 0); } }其中 words10.txt能够在Github上找到。
TEST_METHOD(TestMethod13) { try { Core * core = new Core(); int len = readFile1("../WordChainUnitTesr/words11.txt"); core->gen_chain_word(words1, len, result1, 'e', 0, false); //RAISE EXCEPTION "Find a loop but no -r" } catch (const char* msg) { Assert::AreEqual(strcmp(msg, "No chains found"), 0); } }其中 words11.txt能够在Github上找到。
TEST_METHOD(TestMethod14) { try { Core * core = new Core(); int len = readFile1("../WordChainUnitTesr/words12.txt"); core->gen_chain_word(words1, len, result1, 0,'e', false); //RAISE EXCEPTION "Find a loop but no -r" } catch (const char* msg) { Assert::AreEqual(strcmp(msg, "No chains found"), 0); } }其中 words12.txt能够在Github上找到。
TEST_METHOD(TestMethod15) { try { Core * core = new Core(); words1[0] = ""; core->gen_chain_word(words1, 0, result1, 0, 'e', false); //RAISE EXCEPTION "Find a loop but no -r" } catch (const char* msg) { Assert::AreEqual(strcmp(msg, "Input file is empty"), 0); } }其中 words0.txt是空白文件。
Wordlist.exe -w in.txt -c in.txt "duplicated read file" Wordlist.exe -w -r "missing arguments" Wordlist.exe -w aaa.xyz(does not exist) "file not exist" Wordlist.exe -w in.txt -h a -h b "duplicated -h" Wordlist.exe -w in.txt -h ab "wrong -h" Wordlist.exe -w in.txt -t a -t b "duplicated -t" Wordlist.exe -w in.txt -t ab "wrong -t" Wordlist.exe -w in.txt -r -r "duplicated -r" Wordlist.exe -r "no input file"; Wordlist.exe -x "undefined error" Wordlist.exe -w in.doc "wrong format, not *.txt"左边为指令,右边为报错信息
计算模块共计有三种异常,异常分别是:
测试数据见上一节
计算模块经过抛出异常的方式处理异常,向上一级抛出一个字符串,内容是具体的异常信息。
咱们使用QT绘制界面,经过加载DLL调用接口。在网上查询了教程后,咱们发现设计界面模块不是很难,主要流程以下:
画出界面上按钮,文本框等的位置
针对按钮编写不一样的运行逻辑
针对报错弹出报错窗口
在实现过程当中,咱们参考了这篇教程。
因为时间等缘由,咱们并无对界面进行过多的美化,以体现功能为主。界面主要代码以下:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include
#include
#include
#include
#include
#include
typedef int(*gen_chain_word)(char * words[], int len, char * result[], char head, char tail, bool enable_loop); typedef int(*gen_chain_char)(char * words[], int len, char * result[], char head, char tail, bool enable_loop);
MainWindowMainWindow(QWidget *parent) : QMainWindow(parent), ui(new UiMainWindow) { ui->setupUi(this); drawUI(this); connect(this->pushButton,SIGNAL(clicked()),this,SLOT(fileOpen())); connect(this->pushButton_2,SIGNAL(clicked()),this,SLOT(generate())); connect(this->pushButton_3,SIGNAL(clicked()),this,SLOT(fileSave()));
} void MainWindowfileOpen(){ QString filePath= QFileDialoggetOpenFileName(this,tr("file"),"",tr("TXT(.txt)")); if (filePath.isEmpty()){ return; }else{ QFile fin(filePath); if (fin.open(QIODeviceReadOnly | QIODeviceText)){ while (!fin.atEnd()){ QByteArray line = fin.readLine(); QString strin(line); this->textEdit->insertPlainText(strin); } } fin.close(); } } void MainWindow::generate(){ bool allFlag=true; QLibrary lib("dll1.dll"); gen_chain_word get_chain_word=(gen_chain_word)lib.resolve("gen_chain_word"); gen_chain_word get_chain_char=(gen_chain_char)lib.resolve("gen_chain_char"); bool A_w,A_c,A_r,A_h,A_t; A_w=this->checkBox->isChecked()?true:false; A_c=this->checkBox_2->isChecked()?true:false; A_r=this->checkBox_3->isChecked()?true:false; A_h=this->checkBox_4->isChecked()?true:false; A_t=this->checkBox_5->isChecked()?true:false; QString textin=this->textEdit->toPlainText(); qDebug() << textin; char ch; QByteArray ba = textin.toLatin1(); ch=ba.data(); char ** words=new char*[10000]; char ** result=new char*[10000]; int len=0; int chLen=strlen(ch); int currPos=0; while (currPos<chLen){ if (isalpha(ch[currPos]) != 0) { char * wordBuff = new char[1000]; int wordBuffPos = 0; //printf("%c",currentChar); while (isalpha(ch[currPos]) != 0) { wordBuff[wordBuffPos] = tolower(ch[currPos]); wordBuffPos++; currPos++; } wordBuff[wordBuffPos] = '\0'; words[len] = wordBuff; len++; } else { currPos++; } } qDebug()<<ch; qDebug()<<len; qDebug()<<words[len-1]; char head=0,tail=0; if (A_t){ QString tmp=this->plainTextEdit_2->toPlainText(); char* tt; QByteArray ba = tmp.toLatin1(); tt=ba.data(); if (strlen(tt)!=1){ allFlag=false; QMessageBoxcritical(0 , "ERROR" , "More than one char or no char got in -h or -t", QMessageBoxOk | QMessageBoxDefault ,0 , 0 ); }else } if (A_h){ QString tmp=this->plainTextEdit->toPlainText(); char* tt; QByteArray ba = tmp.toLatin1(); tt=ba.data(); if (strlen(tt)!=1){ allFlag=false; QMessageBoxcritical(0 , "ERROR" , "More than one char or no char got in -h or -t", QMessageBoxOk | QMessageBoxDefault ,0 , 0 );
} void MainWindowfileSave(){ QString filePath= QFileDialoggetSaveFileName(this,tr("file"),"",tr("TXT(*.txt)")); if (filePath.isEmpty()){ return; }else{ QFile fout(filePath); if (fout.open(QIODeviceWriteOnly | QIODeviceText)){ QString textin=this->textEdit_2->toPlainText(); QTextStream out(&fout); out << textin << endl; } fout.close(); } } MainWindow::~MainWindow() { delete ui; }
}else{
head=tt[0];
}
}
if (A_w && A_c){
allFlag=false;
QMessageBox::critical(0 , "ERROR" , "Cannot choose both -w and -c", QMessageBox::Ok | QMessageBox::Default ,0 , 0 );
}
if ((!A_w)&&(!A_c)){
allFlag=false;
QMessageBox::critical(0 , "ERROR" , "Must choose -w or -c", QMessageBox::Ok | QMessageBox::Default ,0 , 0 );
}
qDebug()<<"!!!!!!";
qDebug()<<(int)tail;
qDebug()<<(int)head;
if (allFlag){
try {
if (A_c) get_chain_char(words,len,result,head,tail,A_r);
if (A_w) get_chain_word(words,len,result,head,tail,A_r);
} catch (const char* msg) {
QMessageBox::critical(0 , "ERROR" , msg, QMessageBox::Ok | QMessageBox::Default ,0 , 0 );
}catch(...){
QMessageBox::critical(0 , "ERROR" , "ERROR", QMessageBox::Ok | QMessageBox::Default ,0 , 0 );
}
qDebug()<<"!!!!!!";
int pp=0;
while (result[pp]!=NULL){
qDebug(result[pp]);
this->textEdit_2->insertPlainText(result[pp]);
this->textEdit_2->insertPlainText("\n");
pp++;
}
}
界面模块能够在GitHub上的UI分支里看到
界面模块经过加载DLL的方式与计算模块对接,关键代码为:
typedef int(*gen_chain_word)(char * words[], int len, char * result[], char head, char tail, bool enable_loop); typedef int(*gen_chain_char)(char * words[], int len, char * result[], char head, char tail, bool enable_loop); QLibrary lib("Core.dll"); gen_chain_word get_chain_word=(gen_chain_word)lib.resolve("gen_chain_word"); gen_chain_word get_chain_char=(gen_chain_char)lib.resolve("gen_chain_char");
经过上面的代码即实现了功能对接。
第1,2行是声明要调用的函数的指针,类型
第三行是加载要使用的dll
第4,5行是定义dll的接口,在dll中找到接口,进行调用准备
GUI中还对部分异常输入作了处理,代码与命令行的相似。对于文件读入输出部分咱们使用QT自带的库进行读写。
结对过程总体来讲十分顺利,交流顺畅。我和队友在本次做业以前并不认识,是经过在群里发布结对信息结对的。在结对过程当中,咱们很快的就进入了工做的状态,效率要比预想中高,而在完成做业的过程当中,对于大多数问题都很快的达成了共识,思路上也比较接近。
在实际进行过结对编程后,我以为结对编程的优缺点都十分明显,主要优势有如下几点:
两人合做,思路更广。两我的能够随时地交流本身的想法,对于项目初期规划问题的解决方向十分有帮助。
更容易避免手误致使的BUG。在编程过程当中不可避免地出现打错字母,或者利用自动补全时补全了错误的内容。结对编程时另外一我的就能够及时发现这个问题,避免这类bug。
相互学习,共同提升。两我的能够从对方身上学到本身不足的地方,共同提升知识水平。
结对编程也有一些缺点,如:
对时间要求高。我和个人队友来自不一样的系,所以找到一个合适的都有空的时间十分困难。
效率较多人分开编程低。尽管结对编程能够有效地下降代码中的BUG,减小DEBUG时间,可是我以为分开编程效率仍是更高一些。
本身和队友的优缺点
优势 | 缺点 | |
---|---|---|
本身 | 比较肝,能一口气写好久 能在学习后掌握新知识 能积极沟通,解决问题 |
知识水平比较低,没有找到一个完美解决100单词的算法 对C++不熟悉,开始的效率较低 |
队友 | 实力很强,解决问题的逻辑清晰 善于沟通,能互相理解 负责任 |
稍微有点赶ddl,不过这是我的习惯不一样,其实在ddl以前作完都是合理的 |