項目 | 內容 |
---|---|
本次做業所屬課程 | 2019BUAA軟件工程 |
本次做業要求 | 結對項目-最長單詞鏈 |
我在本課程的目標 | 學會團隊合做開發項目,爲之後的工做打下基礎 |
本次做業的幫助 | 瞭解結對編程而且體驗完整項目開發流程 |
項目地址html
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | 60 | 90 |
-Estimate | -估計這個任務須要多少時間 | 60 | 90 |
Development | 開發 | 3200 | 3300 |
-Analysis | -需求分析 (包括學習新技術) | 200 | 80 |
-Design Spec | -生成設計文檔 | 200 | 180 |
-Design Review | -設計複審 (和同事審覈設計文檔) | 150 | 90 |
-Coding Standard | -代碼規範 (爲目前的開發制定合適的規範) | 150 | 60 |
-Design | -具體設計 | 200 | 240 |
-Coding | -具體編碼 | 1500 | 1800 |
-Code Review | -代碼複審 | 200 | 210 |
-Test | -測試(自我測試,修改代碼,提交修改) | 600 | 640 |
Reporting | 報告 | 400 | 900 |
-Test Report | -測試報告 | 210 | 720 |
-Size Measurement | -計算工做量 | 90 | 60 |
-Postmortem & Process Improvement Plan | -過後總結, 並提出過程改進計劃 | 100 | 120 |
合計 | 3660 | 4290 |
In computer science, information hiding is the principle of segregation of the design decisions in a computer program that are most likely to change, thus protecting other parts of the program from extensive modification if the design decision is changed. The protection involves providing a stable interface which protects the remainder of the program from the implementation (the details that are most likely to change).node
Written another way, information hiding is the ability to prevent certain aspects of a class or software component from being accessible to its clients, using either programming language features (like private variables) or an explicit exporting policy.git
信息隱藏是爲了避免讓程序內部的信息直接暴露給用戶,爲了在設計被改動的時候保護其餘部分。程序員
接口在這裏就扮演着很重要的角色。類只向外界提供它們實現的接口中規定的方法,全部屬性皆爲私有。github
In computing and systems design a loosely coupled system is one in which each of its components has, or makes use of,little or no knowledge of the definitions of other separate components.算法
鬆耦合就是在設計模塊的時候只用到了不多或幾乎沒有的其餘模塊中的東西,像數據,接口,服務等等。編程
在咱們的程序中,就實現了計算模塊函數的封裝,只提供直接可用的接口給用戶,根據需求對core模塊進行封裝,生成的.dll文件在命令行程序和GUI中均可以進行使用,而不會暴露給用戶。在在互換core模塊的時候也較爲順利。api
接口:項目中計算模塊接口的設計採用了要求中統一的API,即:數組
# 最多單詞數 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)
因爲在第一次閱讀做業要求時已經瞭解了固定API的要求,所以咱們在第一次實現時已經採用了這個結構。具體算法採用樹實現,而不是其餘組廣泛使用的圖結構。架構
類:共有兩個類和四個自定義異常結構體。其中Core類爲封裝好的計算模塊,用於計算最長鏈,node類爲樹節點信息,自定義異常則用於拋出異常時輸出信息。具體結構以下:
class node { public: string word; node* parent; node* first_child; node* next; int word_num; int character_num; __declspec(dllexport) node(string cur_word, int cur_word_num, int cur_character_num); }; class Core { public: __declspec(dllexport) int gen_chain_word(char* words[], int len, char* result[], char head, char tail, bool enable_loop); __declspec(dllexport) int gen_chain_char(char* words[], int len, char* result[], char head, char tail, bool enable_loop); __declspec(dllexport) bool gen_tree(node* cur_node, char* words[], int len, bool enable_loop, char tail, node* word_max_node, node* char_max_node, int words_index[][2]); __declspec(dllexport) bool find_in_chain(node* cur_node, string word); };
自定義異常參見第九節。
函數:計算模塊共有五個函數(不包括node類的構造函數),除去API的兩個函數gen_chain_word()和gen_chain_char()外,還有遞歸生成樹函數gen_tree(),在樹中查找函數find_in_chain()和排序時的比較函數compare()。關鍵函數的流程圖以下:
基本算法爲:首先對輸入的全部單詞words進行排序,並創建索引。此後將每一個單詞做爲一棵樹的根節點建樹。建樹時根據索引,遍歷全部知足和父節點鏈接條件的子節點,查找該子節點是否在父節點所在鏈上出現過。若出現過,根據enable_loop標誌判斷是否拋出成環的異常;反之則將子節點加入樹。爲了簡化查找過程,咱們在建樹的過程當中就計算最長單詞和最長字母的節點。當全部樹建好,可直接經過獲得的最長單詞鏈的葉節點向上遍歷,找到整個鏈並輸出。所以,咱們項目的獨到之處包括:使用樹結構、創建索引、建樹完成後無需再遍歷尋找最長鏈、自定義異常類型和創新異常單元測試方法。具體內容將在第六節和第九節詳細說明。
咱們運行了一個有50個單詞的容許成環的文本。優化後的性能分析結果以下:
在初次運行時,咱們發現-r參數下70個單詞已經沒法在300s內運行完成。在調試的過程當中,咱們發現,時間最長的部分是建樹的過程gen_tree(),上圖也印證了這個猜測。所以咱們決定對words排序後創建索引,在建樹時只需遍歷索引指向的首位範圍便可。這樣作會減慢小數據的速度(5個單詞時的運行速度由4ms變爲了12ms),但會提高數據較多時的性能。但當實現以後發現,對大數據量時改進仍然不夠使人滿意。咱們也嘗試過對排序後的words去重,可是在單元測試時發現,代碼:
int cnt = 1; for (i = 1; i < len; i++) { if (strcmp(words[i], words[i - 1]) != 0) { strcpy_s(words[cnt++], strlen(words[i]) + 1, words[i]); } } len = cnt
中的strcpy部分彷佛會在單元測試中拋出異常,咱們並未查到bug的緣由。在網上的惟一解釋是因爲被賦值的指針指向了字符串常量,不能被修改,但這個解釋並不符合。儘管在release版本中這部分代碼能夠經過,最終爲了保險起見,咱們刪除了去重功能。
花費時間最長的函數是find_in_chain(),其做用是在找到一個首字母能夠和當前節點尾字母相連的節點時,從當前節點向上查找,若是出現過這個單詞,則判斷是否有-r參數。若沒出現過,則加到當前節點後面。咱們曾想過是否要將遍歷改成維護一個「單詞是否出現過的標誌數組」的形式,但發現這樣修改很是複雜,時間有限,沒能實現。
此外,生成樹之後從新便利尋找最長鏈也是沒有必要的時間消耗,咱們在建樹過程當中,直接記錄下當前節點的鏈的單詞數和字母數,使得在樹建完後,直接獲得最長鏈的葉節點,反向遍歷便可獲得整條鏈。
Design by Contract(契約式設計)
It prescribes that software designers should define formal, precise and verifiable interface specifications for software components
共同定義一個精確的接口,知道先驗,功能和影響。
優勢:
保證模塊的正確性
複用容易
文檔和設計都是通過精心的撰寫,質量比較高
可靠性較強
這次做業中,就是已經設定好了一個模塊——core,在這個模塊中的函數也是給定的兩個接口。給定這些接口就讓咱們對這個項目的核心算法有了統一的認識和設計思路,即主要有兩個功能,一個經過-w參數,一個經過-c參數來實現生成最長單詞鏈的過程。
可是,這時候,缺點就體現出來了。在使用命令行運行程序的過程當中,咱們輸入的命令都應該是一個不會變的,固定的常量,所以,將接口中的char*改成const char*才更符合編程的規範。但因爲契約式的編程,沒法修改既定的接口規則致使了這些方面的矛盾。
部分測試代碼展現
TEST_METHOD(UnitTest_gen_tree5) { node1 = new node(cur_word, cur_word_num, cur_character_num); word_max_node = new node("", 0, 0); char_max_node = new node("", 0, 0); char head = 0, tail = 0; int len = 6; char* words[6] = { "aj", "jhgjh","hjhjbdkjhaksjdfhkjhkjhjd" ,"hdfdrp","pd","ddfghj" }; Assert::AreEqual(core_test.gen_chain_word(words, len, result, head, tail, true), 5); Assert::AreEqual(core_test.gen_chain_char(words, len, result, head, tail, true), 5); } TEST_METHOD(UnitTest_gen_tree6) { char* words[5] = { "aj", "cdfdrp", "ddfghj", "hhgjh", "sjhjb" }; word_max_node = new node("", 0, 0); char_max_node = new node("", 0, 0); char head = 0, tail = 0; int len = 5; Assert::IsFalse(core_test.gen_tree(node1, words, len, false, 0, word_max_node, char_max_node, words_index)); } TEST_METHOD(UnitTest_find_in_chain3) { char* words[5] = { "aj", "cdfdrp", "ddfghj", "hhgjh", "sjhjb" }; Assert::IsFalse(core_test.gen_tree(node1, words, len, false, 0, word_max_node, char_max_node, words_index)); Assert::IsFalse(core_test.gen_tree(node1, words, len, false, 0, word_max_node, char_max_node, words_index)); Assert::IsTrue(core_test.find_in_chain(node1->first_child->first_child, word)); } TEST_METHOD(UnitTest_command_line2) { argc = 4; char* argv[4] = { "Wordlist.exe","-w","-r","../Wordlist/file.txt" }; Assert::IsTrue(command_handler(argc, argv, words, len, head, tail, enable_loop, w_para)); }
被測試函數
測試的函數有五個:
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); bool gen_tree(node* cur_node, char* words[], int len, bool enable_loop, char tail, node* word_max_node, node* char_max_node, int words_index[][2]); bool find_in_chain(node* cur_node, string word); bool command_handler(int argc, char* argv[], char* words[], int &len, char &head, char &tail, bool &enable_loop, bool &w_para);
測試思路
首先明確程序的架構,command_handler是處理命令行參數函數;gen_chain_word和gen_chain_char是兩個主函數,在這兩個主函數中均調用了get_tree和find_in_chain這兩個函數。
從小函數開始測試,根據gen_tree的輸入和輸出,使用assert函數,返回一個bool類型的值。主要測試重點就在於生成樹的過程當中是否遇到禁止環出現還出現環的狀況以及樹的生成是否正確。
而後是find_in_chain函數,主要功能是找在該路徑上是否出現了和這個word同樣的word,不知足咱們的條件(一個單詞只容許出現一次),返回bool類型的值。在測試時,就用gen_tree先生成一顆樹,而後設定多種word讓該函數去尋找。
對與兩個主函數,由於主要的核心已經測試過,因而採用一些白盒測試的方法,測試其他沒有覆蓋的狀況,調整-h,-t參數等等。
最後是command_handler函數的測試,這部分就是測試輸入命令的正確與否。不過由於是命令行輸入,已經有了一些狀況的限制,須要測試的狀況也能夠較好地覆蓋。測試內容分爲正確指令和錯誤指令,錯誤指令有包含各類錯誤:有不正確指令,內容缺失,文件找不到等等。是在考慮的多種用戶輸入時可能犯下的錯誤進行相應的匹配和處理。
測試報告及覆蓋率報告生成的辛酸史
測試報告
覆蓋率報告
這個過程是對我來講,整個項目,最艱難的一個過程,完成它的耗時遠遠超過了個人估計值。總的來講,就是我使用的vs2017社區版沒有這個功能而且其中較爲流行的插件也均不支持vs2017的社區版(如下簡稱vs2017,由於專業版有現成的工具:))。
因而我展開了搏鬥:
Round1:opencover和ReportGenerator
不適用!vs中缺乏工具。另外vs2015版還能夠支持Opencover的一個UI extension,簡直不要太簡單。
Round2:使用命令行運行測試文件的DLL,生成.trx文件,再轉爲html文件查看報告
咱們在命令行能夠運行測試命令,但必需要轉爲超級用戶後才能生成.trx文件,不然會提示「拒絕訪問」。接着,vs2017生成的.trx文件沒有能夠解析它的工具。咱們嘗試了多種,trx2html,還有GitHub上開源的工具,都沒法解析它。
Round3:最後,咱們得知了OpenCppCoverage這個軟件能夠經過命令行拿到覆蓋率的html文件
起初,咱們發現,這個命令只能經過調用.exe文件獲得一次結果的覆蓋率。可是通過一陣思考和對這個軟件GitHub主頁的參數分析,咱們發現能夠經過參數export_type=binary先生成覆蓋率的二進制文件,再使用--input_coverage arg命令將多個測試樣例覆蓋獲得的*.cov(二進制文件)進行merge。最後就獲得了至關於整個測試文件的覆蓋結果,再生成.html文件,方即可視化。先貼結果:
能夠看出,覆蓋率均達到95%以上,可視化效果也不錯。html文件自取,提取碼:lpw9 。
惟一的缺點就是須要本身生成不一樣的文件,手動運行屢次命令,以下圖
之後若要運行更多的測試,咱們的思路就是寫.bat腳本,也算事實行了半自動化的測試,時間有限,留給你們提出更好的方法。
命令實例:
OpenCppCoverage.exe --sources=Wordlist --export_type=binary -- Wordlist.exe -w file.txt
OpenCppCoverage.exe --sources=Wordlist ----input_coverage Wordlist.cov --export_type=binary -- Wordlist.exe -w file.txt
附幾個參考過的連接
由於計算模塊的接口固定,使得諸如「沒有出現-w和-c」、「-h後字母數超過一個」、「文件不存在」這樣的異常沒法在Core中處理,故在其餘函數中以輸出形式報出異常。本項目Core類實現的異常處理有:成環異常、-h後字母不是英文字母、-t後字母不是英文字母、鏈長度小於2四種異常。
自定義異常的通用結構以下:
struct ChainLessThen2Exception : public exception { const char * what() const throw () { return "length of chain is less than 2!"; } };
在catch時,只需使用:
try { // some function } catch(ChainLessThen2Exception& e) { cout << e.what(); }
便可輸出異常信息。
因爲網上沒有找到異常檢測的方法,咱們創新瞭如下格式來在單元測試中檢測異常。能夠看到,這個方法可以檢測出是否捕獲異常。
成環異常LoopException:對於enable_loop參數爲false時,若是檢測到環存在,拋出該異常。
測試樣例
TEST_METHOD(UnitTest_Loop) { char* words[5] = { "aj", "hdfdrs", "jhgjh", "kjhjh", "sdfghj" }; int len = 5; char head = 0, tail = 0; try { core_test.gen_chain_char(words, len, result, head, tail, false); Assert::IsTrue(false); } catch(struct LoopException &e) { Assert::IsTrue(true); } catch(...) { Assert::IsTrue(false); } }
錯誤場景:"aj", "hdfdrs", "jhgjh", "kjhjh", "sdfghj"中出現環s..j-j..h-h..s,拋出LoopException異常。
首字母異常HeadInvalidException:若是head不爲0,且head後面的字母不在'a'-'z'或'A'-'Z'範圍內,拋出異常。
測試樣例
TEST_METHOD(UnitTest_Head) { char* words[7] = {"ajsdjfhkhlkjhlakjshduhcuhuhuhuhvuhjhdkajsdfnajksdjkfhksdjahkjdhjdhje" }; try { core_test.gen_chain_char(words, len, result, 1, tail, true); Assert::IsTrue(false); } catch (struct HeadInvalidException &e) { Assert::IsTrue(true); } catch (...) { Assert::IsTrue(false); } }
錯誤場景:-h參數後跟1,非英文字母,拋出HeadException異常。
尾字母異常TailInvalidException:若是tail不爲0,且tail後面的字母不在'a'-'z'或'A'-'Z'範圍內,拋出異常.
測試樣例
TEST_METHOD(UnitTest_Tail) { char* words[7] = {"ajsdjfhkhlkjhlakjshduhcuhuhuhuhvuhjhdkajsdfnajksdjkfhksdjahkjdhjdhje" }; try { core_test.gen_chain_char(words, len, result, head, 1, true); Assert::IsTrue(false); } catch (struct TailInvalidException &e) { Assert::IsTrue(true); } catch (...) { Assert::IsTrue(false); } }
錯誤場景:-h參數後跟1,非英文字母,拋出TailException異常。
鏈長度異常ChainLessThan2Exception:若是head不爲0,且head後面的字母不在'a'-'z'或'A'-'Z'範圍內,拋出異常。
測試樣例
TEST_METHOD(UnitTest_Chain) { char* words[7] = {"ajsdjfhkhlkjhlakjshduhcuhuhuhuhvuhjhdkajsdfnajksdjkfhksdjahkjdhjdhje" }; try { core_test.gen_chain_char(words, len, result, head, tail, true); Assert::IsTrue(false); } catch (struct ChainLessThan2Exception &e) { Assert::IsTrue(true); } catch (...) { Assert::IsTrue(false); } }
錯誤場景:-h參數後跟1,非英文字母,拋出ChainLessThan2Exception異常。
此外,在Core類之外,咱們已經對輸入不合法、文件不存在等異常進行了處理,保證程序不會崩潰。
UI採用qt實現,因爲我之前有過一次pyqt開發的經驗,上手起來並不難。咱們在底層放置了一些layout,同時也給每一個分區設置了layout,這樣作的目的是保證界面縮放時比例不變,但作出來才發現底層layout沒能隨窗口一塊兒變化,致使縮放時比例仍然存在問題,最終決定固定窗口大小。具體結構以下:
實現過程即爲拖動控件到layout上,設置layout內控件比例便可,並未用到qt代碼,所以此處無需示範代碼部分。須要注意的是,因爲-c和-w以及兩種輸入方式都知足同一時刻有且只有一個button被選中,故採用了radio button的方式,這種button默認爲互相沖突的,即不可同時選中。但當ui完成後才發現,本想四個按鈕分爲兩組相沖突,實際倒是四個按鈕同一時刻只能選擇一個。所以使用了QButtonGroup的形式進行分組。
界面主要包括上方的輸入區,支持文件路徑和直接輸入單詞;中間的參數和輸出區,容許用戶選擇參數,獲得正確結果和報錯信息;下方的導出區,容許用戶將結果導出到指定路徑。
因爲GUI上實際只有兩個按鈕,所以GUI和計算模塊只有兩個函數進行對接。代碼以下:
QtGui_Wordlist::QtGui_Wordlist(QWidget *parent): QMainWindow(parent) { ui.setupUi(this); connect(ui.pushButton_generate_chain, SIGNAL(clicked()), this, SLOT(gen_chain())); connect(ui.pushButton_save_in_file, SIGNAL(clicked()), this, SLOT(save_file())); }
其中gen_chain()和save_file()分別是點擊pushButton_generate_chain和pushButton_save_in_file的槽函數。槽函數的實現和正常實現基本相同,在槽函數中調用Core.dll中的api接口,惟一區別在於讀取和輸出的位置不一樣,GUI的數據由text()從box讀入,由setPlainText()輸出到box。
GUI實現的功能有:
從文本框讀入文件路徑:
從文本框中輸入單詞進行查找:
-w和-c兩種方式的選擇:
-h和-t的使用:
-r的使用:
文件導出:
結對過程:由於第一堂軟工課程坐在相鄰的位置上,天然而然組成告終對做業的搭檔。
爲了達到做業目標的要求,只要雙方的時間容許就會在一塊兒進行結對編程。從最初的計劃,設計,到編碼,測試,再到最後的博客撰寫,都存在着結對完成和明確分工兩個狀態的存在。
編碼過程較爲符合結對編程的要求,咱們面向同一個電腦,對項目進行構建,兩人輪流編碼,輪流複審。在後期,作出了些許的分工。例如我負責測試的工做,他負責gui的工做。這時,項目有什麼問題均可以進行隨時的交流,效率很高。
在以後的博客撰寫中,在共同內容的部分,也進行了分工,寫各自較爲熟悉的內容,能夠更好地展現咱們的項目和思想。
最後,很是感謝個人搭檔,這整個任務對我也是一個不小的挑戰,他幫助了我許多,讓我堅持下來,也收穫了很多的知識。
優勢:
缺點:
張圓寧:
優勢:積極主動;對新事物的嘗試和熟悉很快;對問題有必定的探究能力,會尋找各類方法。
缺點:編程能力有待提升,尤爲在速度方面還須要進步;對解決某些客觀問題上(例如軟件自己的限制等方面)沒有足夠的耐心;對問題的分析不夠完善。
牛宇航:
優勢:勤于思考,善於鑽研;有很強的編程能力;對問題有很強的應變能力。
缺點:無
見第二節。
16061200 陳治齊 16061076 顧展鵬
問題1:咱們的main函數中有部分對於異常的處理,這些異常都是封裝好了在core.h頭文件中。在使用咱們的主函數和GUI及對方的Core.dll以後,主函數由於找不到Core.h中的自定義異常,編譯不經過,對方的exe和GUI沒法運行。
解決辦法:只要刪除咱們main函數中的異常處理模塊便可正常運行,更爲正規的流程應該是在互換公用模塊的時候分享並擴充異常處理模塊。
解決結果:修改main中異常處理後成功運行,經過正確樣例。
問題2:對方的主函數和GUI沒法使用咱們的.dll文件,緣由在於對方是動態調用,咱們是靜態調用。對方忘記修改.def文件以適應咱們新的.dll。
咱們組內使用的是靜態調用:靜態調用比較簡單,編譯DLL項目前,給.h文件中的函數前加上__declspec(dllexport) ,以生成.lib文件。將.lib文件拷貝到其餘項目中後,只需引用.h頭文件便可使用.dll(.cpp)文件的函數和類。
合做組使用的是動態調用:加載dll文件,在.def文件中寫明.dll中的函數。若可以正確從.dll中取到函數所在地址,直接調用便可完成DLL的動態調用。
解決辦法:對方應當更新.def文件爲我方的.dll中的函數,便可運行。
修改結果:對方更新.def後成功運行我方.dll,經過正確樣例。
這次的做業對我是一個比較大的挑戰,尤爲是投注了太多精力解決一些版本不兼容,版本不支持的問題,真的讓我一度懷疑這門課的訓練目標以及課程組是否仔細評估過這許多因素帶給咱們的困難,做業量和完成時間的比例以及實際操做的可行性。總之,這第一個做業給了我太多意料以外的負擔,爲了彌補這幾天實習一天後還要寫軟工的睡眠不足,我要好好休息幾天。