传送门c++
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | 60 | 80 |
· Estimate | · 估计这个任务须要多少时间 | 14天 | 12天 |
Development | 开发 | 9天 | 7天 |
· Analysis | · 需求分析 (包括学习新技术) | 0.5天 | 1天 |
· Design Spec | · 生成设计文档 | 120 | 150 |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 | 60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 50 |
· Design | · 具体设计 | 1天 | 1天 |
· Coding | · 具体编码 | 5天 | 5天 |
· Code Review | · 代码复审 | 1.5天 | 1天 |
· Test | · 测试(自我测试,修改代码,提交修改) | 1.5天 | 1天 |
Reporting | 报告 | 1天 | 1天 |
· Test Report | · 测试报告 | 2小时 | 4小时 |
· Size Measurement | · 计算工做量 | 1小时 | 0.5小时 |
· Postmortem & Process Improvement Plan | · 过后总结, 并提出过程改进计划 | 0.5天 | 0.5天 |
合计 | 10天 | 12天 |
信息隐藏
咱们的设计在多个层面作了很好的封装,很好的保证了各个模块间的信息隐藏性,将搜索算法逻辑封装在具体实现内部,向外给出统一的公告接口,将计算逻辑封装在calculate这一个接口函数中。git
接口设计
接口设计上首先是一个全局对外接口calculate,咱们详细分析了实际需求的本质,设计了一个通用接口来知足各类各样的需求,在数据结构封装上咱们提供了数据访问和修改专用的接口,规范数据结构的使用,防止出现隐藏bug。没有使用任何全局变量,只使用了一些全局宏来输出log进行debug。对于可能出现的各种优化算法,咱们设计了公共的接口类,规范了统一的算法接口。github
松耦合
在计算逻辑和IO之间松耦合,计算逻辑和具体的IO方式无关,设置了规范的数据类型对象和错误信息提示对象,很好的完成了松耦合。算法
咱们在设计计算模块时使用了自定义的对外接口,接口形式以下编程
se_errcode Calculate(const string& input_text, string& output_text, LongestWordChainType& longest_type, const char& head, const char& tail, bool enable_circle, WordChainError& handle_error);
对于该接口的说明以下:数组
咱们将整个运算逻辑抽象在一个接口内,不一样的“最长”计算方式使用枚举类型LongestWordChainType进行描述,head和tail表示单词链的首尾字母,enable_circle表示是否容许输入中含有环,这样因此的需求选项均可以被集中在一个接口中,代码的复用性很是高,且很简洁。数据结构
咱们将输入输出抽象为string,具体的IO逻辑可能涉及读取文件,与gui协同,输入输出格式等问题,这一部分相对于计算逻辑十分独立,且较容易发生需求上的变化,故独立在计算模块以外,给计算模块的一概整理成string形式,并用非字母符号来分割单词,完成了计算和IO的解耦,同时string对象不须要手动维护char*数组,增长了程序的鲁棒性。架构
对于异常处理,通常c++编程中是不使用异常的,由于其会对运行效率带来巨大的影响,一旦抛出异常整个程序的运行时间将大大增长,因此在程序逻辑上,咱们使用自定义的错误码返回值来完成逻辑上的处理,同时创建专门的资源和错误信息管理对象handle_error,负责管理资源和错误信息记录app
关于计算逻辑的实现,咱们认为这里主要的两部分是数据结构和算法,接下来咱们会对这两方面分开进行说明。框架
数据结构方面:
咱们将该问题抽象为一个有向图,该图中的结点是26个字母,一个单词即可以表示为从首字母到尾字母的一条边,由问题的特性咱们知道,这张图中是有自圈(例如awa),多重边(例如awwb, awb),简单的使用邻接矩阵,邻接表是很难应对这种数据的。咱们抽象了新的“边”元素,使用WordMapElement类去描述它,对于首字母和尾字母相同的边,存储在这一个元素之中,并按照单词含有的字母数量降序进行排列存储。而首字母,尾字母根据问题去建立结点,使用特殊的“边”元素,构建出咱们所用的数据结构。
在具体实现上,咱们使用unordered_map<char, unordered_map<char, WordMapElement> >这样的c++容器结构去存储,避免了定长数组带来的维护性差,可扩展性差的问题,同时又具备直接根据key-value来访问元素的方便性。
在搜索时,咱们须要存储当前搜到的最长单词链的相关信息,咱们仿照上文中相似的模式创建相似的数据结构。
算法方面:
因为该问题自己是一个NP-Complete问题,因此会有不少的剪枝优化和启发式搜索算法,那么从设计框架角度,咱们须要一个可扩展性很高的搜索框架,而不是把算法耦合在总体逻辑中,因此,咱们设计一个通用的搜索接口SearchInterface,定义了公共的接口方法Search和LookUp,任何搜索算法只要继承接口类重写这两个方法便可,其内部的算法优化逻辑将封装在方法内部,与外部的逻辑无关,这样能够很方便的添加优化算法,同时保证架构设计的完整性。
另外一独特之处:
在做业文档给出的接口中,对于单词数最长和字母数最长,分别设计了两个接口,可是咱们认为本质上来说,这只是两种不一样的“长度”度量而已,从实现来讲只有计算长度时,每条边对应的长度不一样这一点差别,因此咱们的设计将其统一在一块儿,若是将来有新的需求,好比说“其中含字母a的个数最多”,只须要添加新的长度计算方式便可,而不用改动总体逻辑。
这种编程方式的特色是严格规定前置条件,后置条件,不变项,好比大二oo课程中就有相似的训练(JSF),这样作的好处是严格限制了输入输出条件,函数可能产生的反作用,从而能够很好的规范程序接口,同时,基于这种规定,能够更好地进行单元测试,覆盖到每个函数和方法的具体细节,使得程序的正确性获得了更大的保证, 可是与之相对的,就须要开发者付出大量的时间和精力,作很是详尽的测试,对于工期很紧的项目可能没法实际操做。我认为在时间中,能够选择部分绝对不能出现问题的核心模块,使用这种方式进行开发,对于比较边缘的模块,并不须要作这么多,从而让开发兼顾开发效率和正确性。
TEST_METHOD(Test_ExtractWord) { //TEST ExtractWord WordChainError error1; string input_text1 = "_this is a!@#$test of0extract!word...... "; vector<string> input_buffer1; vector<string> result1 = { "this","is","a","test","of","extract","word" }; ExtractWord(input_text1, input_buffer1,error1); Assert::AreEqual(result1.size(), input_buffer1.size()); for (int i = 0; i < result1.size(); i++) { Assert::AreEqual(result1[i], input_buffer1[i]); } WordChainError error2; string input_text2 = "_this___is___another======test of extract[][][]word. "; vector<string> input_buffer2; vector<string> result2 = { "this","is","another","test","of","extract","word" }; ExtractWord(input_text2, input_buffer2,error2); Assert::AreEqual(result2.size(), input_buffer2.size()); for (int i = 0; i < result2.size(); i++) { Assert::AreEqual(result2[i], input_buffer2[i]); } WordChainError error3; string input_text3 = "_[][][]...."; vector<string> input_buffer3; vector<string> result3 = { }; ExtractWord(input_text3, input_buffer3,error3); Assert::AreEqual(result3.size(), input_buffer3.size()); for (int i = 0; i < result3.size(); i++) { Assert::AreEqual(result3[i], input_buffer3[i]); } }
TEST_METHOD(Test_Class_Word) { //TEST Class_Word Word test1 = Word("a"); Assert::AreEqual(test1.GetHead(), 'a'); Assert::AreEqual(test1.GetTail(), 'a'); Assert::AreEqual(test1.GetWord(), string("a")); Assert::AreEqual(test1.GetKey(), string("aa")); Word test2 = Word("phycho"); Assert::AreEqual(test2.GetHead(), 'p'); Assert::AreEqual(test2.GetTail(), 'o'); Assert::AreEqual(test2.GetWord(), string("phycho")); Assert::AreEqual(test2.GetKey(), string("po")); }
TEST_METHOD(Test_Class_DistanceElement_Method) { //TEST Class_DistanceElement_Method: SetDistance/GetDistance/SetWordChain/CopyWordBuffer/ToString LongestWordChainType type1 = letter_longest; DistanceElement testElement1 = DistanceElement(type1); Assert::AreEqual(testElement1.GetDistance(), 0); vector<string> input1 = { "a","test","of","it" }; vector<string> output1; testElement1.SetWordChain(input1); testElement1.CopyWordBuffer(output1); for (int i = 0; i < input1.size(); i++) { Assert::AreEqual(output1[i], input1[i]); } testElement1.SetDistance(6); Assert::AreEqual(testElement1.GetDistance(), 6); Assert::AreEqual(testElement1.ToString(), string("a-test-of-it")); LongestWordChainType type2 = word_longest; DistanceElement testElement2 = DistanceElement(type2); Assert::AreEqual(testElement2.GetDistance(), 0); vector<string> input2 = { "another","test","of","it" }; vector<string> output2; testElement2.SetWordChain(input2); testElement2.CopyWordBuffer(output2); for (int i = 0; i < input2.size(); i++) { Assert::AreEqual(output2[i], input2[i]); } testElement2.SetDistance(2); Assert::AreEqual(testElement2.GetDistance(), 2); Assert::AreEqual(testElement2.ToString(), string("another-test-of-it")); }
TEST_METHOD(Test_Calculate) { //Test Calculate: include CalculateLongestChain/ChainSearch WordChainError error; string input_text ="Algebra))Apple 123Zoo Elephant Under Fox_Dog-Moon Leaf`;;Trick Pseudopseudohypoparathyroidism"; string output_text1 = ""; LongestWordChainType type1 = word_longest; Calculate(input_text, output_text1, type1, NO_ASSIGN_HEAD, NO_ASSIGN_TAIL, false,error); string result1 = "algebra\napple\nelephant\ntrick\n"; Assert::AreEqual(result1, output_text1); string output_text2 = ""; LongestWordChainType type2 = letter_longest; Calculate(input_text, output_text2, type2, NO_ASSIGN_HEAD, NO_ASSIGN_TAIL, false,error); string result2 = "pseudopseudohypoparathyroidism\nmoon\n"; Assert::AreEqual(result2, output_text2); string input_text_ring = "Algebra))Apple aaaaa 123Zoo Elephant Under Fox_Dog-Moon Leaf`;;Trick Pseudopseudohypoparathyroidism"; string output_text3 = ""; string result3 = "algebra\naaaaa\napple\nelephant\ntrick\n"; LongestWordChainType type3 = word_longest; Calculate(input_text_ring, output_text3, type3, NO_ASSIGN_HEAD, NO_ASSIGN_TAIL, true, error); Assert::AreEqual(result3, output_text3); }
其中重复单词异常会在命令行输出,可是不会影响程序的进行,计算模式异常和命令行参数异常均为在对命令行进行解析时发生的异常,咱们并无单独为其写一个方法,因此难以在单元测试中验证,仅会在命令行输出错误信息。
其他四种异常均在单元测试中进行了验证。
输入文件异常,对应找不到输入文件的场景等:
std::ifstream in("notexist.txt"); std::stringstream buffer1; WordChainError error3; if (!in.is_open()) { char buffer1[MAX_BUFFER_SIZE]; sprintf(buffer1, "Error Type: can't open input file\n"); string error_content(buffer1); int error_code = SE_ERROR_OPENING_INPUT_FILE; error3.AppendInfo(error_code, error_content); } string errortext3 = error3.ToString(); Assert::AreEqual(errortext3, string("Error Type: can't open input file\nError Content: Error Type: can't open input file\n"));
std::stringstream buffer2; std::ofstream out("close.txt"); WordChainError error4; out.close(); if (!out.is_open()) { char buffer2[MAX_BUFFER_SIZE]; sprintf(buffer2, "Error Type: can't open output file\n"); string error_content(buffer2); int error_code = SE_ERROR_OPENING_OUTPUT_FILE; error4.AppendInfo(error_code, error_content); } string errortext4 = error4.ToString(); Assert::AreEqual(errortext4, string("Error Type: can't open output file\nError Content: Error Type: can't open output file\n"));
WordChainError error1; string input_text1 = "Algebra))Apple aaaaa 123Zoo Elephant Under Fox_Dog-Moon Leaf`;;Trick Pseudopseudohypoparathyroidism"; string output_text1 = ""; string errortext1; LongestWordChainType type1 = word_longest; Calculate(input_text1, output_text1, type1, NO_ASSIGN_HEAD, NO_ASSIGN_TAIL,false, error1); errortext1 = error1.ToString(); Assert::AreEqual(errortext1,string("Error Type: input has circle but not enable circle\nError Content: Error Type: input has circle but not enable circle\n"));
WordChainError error2; string input_text2 = "Algebra Zoo"; string output_text2 = ""; string errortext2; LongestWordChainType type2 = word_longest; Calculate(input_text2, output_text2, type2, 'i', NO_ASSIGN_TAIL, false, error2); errortext2 = error2.ToString(); Assert::AreEqual(errortext2, string("Error Type: no available word chain\nError Content: no available word chain for head(i) and tail(0)\n"));
界面模块咱们使用了Qt的库进行了设计。编码上仍然是c++语言,ui设计上使用了Qt Creator进行设计。
使用说明
以上的需求能够大概代表咱们的用户界面须要至少五个参数选择的交互按钮,两个界面,其中一个负责写入文本,一个负责显示正确结果和错误信息。另外须要四个按钮,分别对应导入文本,运行程序,导出结果和显示使用说明。
明确了以上需求以后,咱们在Qt Creator中设计了大概的用户界面(macOS下):
其中使用radiobutton选择两种计算方式,checkbox选择是否容许单词环,下拉框选择是否有开头和结尾字母的要求,这些设计都是为了方便用户的使用。
如下为部分用户界面的代码:
//按钮触发事件(引入文件以及显示帮助信息) void MainWindow::on_pushButton_import_clicked() { QString fileName=QFileDialog::getOpenFileName(this,tr("Choose File"),"",tr("text(*.txt)")); QFile file(fileName); if(!file.open(QFile::ReadOnly|QFile::Text)){ QString errMsg="error when import file"; ui->outputArea->setText(errMsg); return; } QTextStream in(&file); ui->inputArea->clear(); ui->inputArea->setText(in.readAll()); } void MainWindow::on_pushButton_help_clicked() { dialog = new Dialog(this); dialog->setModal(false); QString helpMsg="test help"; dialog->ui->textBrowser->setPlainText(helpMsg); dialog->show(); }
void MainWindow::on_pushButton_run_clicked() { int para=ui->radioButton_w->isChecked()?1:2; bool ring=ui->checkBox_loop->isChecked(); string content = ui->inputArea->toPlainText().toStdString(); char head, tail; if (ui->comboBox_h->currentIndex() == 0) { head = '\0'; } else { head = 'a' + ui->comboBox_h->currentIndex() - 1; } if (ui->comboBox_t->currentIndex() == 0) { tail = '\0'; } else { tail = 'a' + ui->comboBox_t->currentIndex() - 1; } if(content.size()==0){ QString errMsg="empty input!"; ui->outputArea->setPlainText(errMsg); } else{ //call corresponding function string output; LongestWordChainType type; se_errcode code; QString s = "fin"; WordChainError error; if (para == 1) { type = word_longest; code=Calculate(content, output,type,head,tail,ring,error); } else { type = letter_longest; code=Calculate(content, output, type, head, tail, ring,error); } if (code == SE_OK) { QString result = QString::fromStdString(output); ui->outputArea->setPlainText(result); } else { string result = error.ToString(); QString error= QString::fromStdString(result); ui->outputArea->setPlainText(error); } } //cout<<"onclick_run"<<endl; }
功能运行结果以下:
优势: