項目 | 內容 |
---|---|
這個做業屬於哪一個課程 | 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拋出異常的方式。
在閱讀對方的源代碼的過程當中,咱們發現對方定義了大量的自定義結構,枚舉,使得代碼的可讀性較高,哪怕在沒有註釋的狀況下閱讀也不會很困難,這一點值得咱們學習。