項目 | 內容 |
---|---|
本次做業所屬課程 | 2019BUAA軟件工程 |
本次做業要求 | 結對項目-最長單詞鏈 |
我在本課程的目標 | 熟悉和實踐軟件工程流程,適應團隊開發 |
本次做業的幫助 | 熟悉結對編程 |
項目地址html
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | 70 | 90 |
-Estimate | -估計這個任務須要多少時間 | 70 | 90 |
Development | 開發 | 3400 | 3300 |
-Analysis | -需求分析 (包括學習新技術) | 200 | 80 |
-Design Spec | -生成設計文檔 | 200 | 180 |
-Design Review | -設計複審 (和同事審覈設計文檔) | 150 | 90 |
-Coding Standard | -代碼規範 (爲目前的開發制定合適的規範) | 150 | 60 |
-Design | -具體設計 | 200 | 240 |
-Coding | -具體編碼 | 1700 | 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 |
合計 | 3770 | 4290 |
在查找資料時,我發現了這篇博文,其中有些部分講的仍是很好的。如「Loose Coupling幾乎已經與interface design等價了」,鬆耦合還有其餘的實例,但經過接口的合理設計,固定下來接口的形式,確實實現鬆耦合的方法之一。node
將API接口封裝爲Core類,變爲.dll文件,就能夠容許多個文件甚至項目直接調用,達到鬆耦合。因爲接口已經由做業要求給定,分析能夠得知,字符串數組words、result使用的類型爲char *words[],使得接口可供C甚至C#使用。而咱們的Core類中,存在一些只供Core內函數調用的函數(如compare()),此時就不將compare函數聲明爲__declspec(dllexport),即.lib中並無這個接口,保證了Information Hiding。此外,儘管存在一些地方使用全局變量將簡化算法(如保存最長鏈葉節點的node),但爲了更加安全的實現,咱們將節點的指針做爲參數傳入建樹函數,對其餘函數來講,達到Information Hiding的目的。git
接口:項目中計算模塊接口的設計採用了要求中統一的API,即:github
# 最多單詞數 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); };
自定義異常參見第九節。api
函數:計算模塊共有五個函數(不包括node類的構造函數),除去API的兩個函數gen_chain_word()和gen_chain_char()外,還有遞歸生成樹函數gen_tree(),在樹中查找函數find_in_chain()和排序時的比較函數compare()。關鍵函數的流程圖以下:數組
基本算法爲:首先對輸入的全部單詞words進行排序,並創建索引。此後將每一個單詞做爲一棵樹的根節點建樹。建樹時根據索引,遍歷全部知足和父節點鏈接條件的子節點,查找該子節點是否在父節點所在鏈上出現過。若出現過,根據enable_loop標誌判斷是否拋出成環的異常;反之則將子節點加入樹。爲了簡化查找過程,咱們在建樹的過程當中就計算最長單詞和最長字母的節點。當全部樹建好,可直接經過獲得的最長單詞鏈的葉節點向上遍歷,找到整個鏈並輸出。安全
所以,咱們項目的獨到之處包括:使用樹結構、創建索引、建樹完成後無需再遍歷尋找最長鏈、自定義異常類型和創新異常單元測試方法。具體內容將在第六節和第九節詳細說明。架構
wiki百科上uml的定義以下:
本項目的uml類圖:
因爲只使用了兩個類,且Core類單純封裝了方法,所以類之間耦合度較低。
咱們運行了一個有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參數。若沒出現過,則加到當前節點後面。咱們曾想過是否要將遍歷改成維護一個「單詞是否出現過的標誌數組」的形式,但發現這樣修改很是複雜,時間有限,沒能實現。
此外,生成樹之後從新便利尋找最長鏈也是沒有必要的時間消耗,咱們在建樹過程當中,直接記錄下當前節點的鏈的單詞數和字母數,使得在樹建完後,直接獲得最長鏈的葉節點,反向遍歷便可獲得整條鏈。
DbC爲契約式設計,詳細介紹能夠看Wiki中對DbC的介紹,契約式設計就是互相之間明確行爲先後的狀態,其目的是在設計時能明確模塊單元的狀態。在這個項目中,最明顯的就是API的固定。相同的API設計規範決定了調用其它.dll文件的方便性。又例如咱們用到的try{}catch(){}finally{}的結構自己就是DbC,不符合先驗條件程序就會返回。
可是契約式設計也有不少缺點,固定的接口讓不少人在設計時很是不方便,從羣裏不少人在問能不能修改API就能夠看出。此外,契約式設計獲得的API必須設計的足夠靈活又安全,所以必須在設計時花上一些時間。
部分測試代碼展現
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,經過正確樣例。
講道理,光是這個結對項目,就值兩學分了……幾乎天天都是下班之後熬夜寫代碼,嘗試測試軟件。不知道告訴了多少組咱們用了什麼測試工具成功作出了覆蓋率,這背後是咱們對一個又一個覆蓋率工具的嘗試和失敗,半夜一兩點回到宿舍,次日又是帶着睡眼去上班。儘管最後也沒能很快跑完大的測試集,可是總算是完成了項目。此次項目讓我終於認識到了什麼是Debug和Release模式,什麼又是解決方案和項目(工程)的區別,什麼是DLL,又如何進行測試。可是彷佛課程組在要求一些工具時,對工具的推薦和介紹過於少了。助教在羣裏說的工具在vs2017 community上並不支持。或許不少組在最後一天問出了其餘組如何完成覆蓋率測試,但咱們花費一兩個晚上尋找到了能用的覆蓋率軟件真的十分痛苦。此外,甚至出現了qt要求x64運行,而測試要求x86運行,咱們只能在一我的的電腦上運行x64的qt和源代碼,另外一人的電腦上運行x86的源代碼和測試……但總的來講,仍是學到了一些東西。感謝老師和助教對咱們項目的幫助。