傳送門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; }
功能運行結果以下:
優勢: