項目 | 內容 |
---|---|
這個做業屬於哪一個課程 | 2019春季計算機學院軟件工程(羅傑)(北京航空航天大學) |
這個做業的要求在哪裏 | 結對項目-最長單詞鏈 |
我在這個課程的目標是 | 學習軟件工程方法與相關工具,提高本身的工程能力,鍛鍊本身與他人協同開發以及開發較大項目的能力 |
這個做業在哪一個具體方面幫助我實現目標 | 學習並嘗試實踐告終對編程,鍛鍊了代碼尤爲測試方面的基礎 |
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | ||
· Estimate · | 估計這個任務須要多少時間 | 30 | 60 |
Development | 開發 | ||
· Analysis | · 需求分析 (包括學習新技術) | 300 | 400 |
· Design Spec | · 生成設計文檔 | 60 | 40 |
· Design Review | · 設計複審 (和同事審覈設計文檔) | 120 | 90 |
· Coding Standard | · 代碼規範 (爲目前的開發制定合適的規範) | 15 | 10 |
· Design | · 具體設計 | 180 | 180 |
· Coding | · 具體編碼 | 600 | 720 |
· Code Review | · 代碼複審 | 250 | 240 |
· Test | · 測試(自我測試,修改代碼,提交修改) | 200 | 300 |
Reporting | 報告 | ||
· Test Report | · 測試報告 | 200 | 150 |
· Size Measurement | · 計算工做量 | 60 | 30 |
· Postmortem & Process Improvement Plan | · 過後總結, 並提出過程改進計劃 | 30 | 30 |
合計 | 2045 | 2250 |
信息隱藏我認爲便是封裝的概念,在咱們實現過程當中,咱們將界面模塊(Base)和計算模塊(Core)中的操做均封裝爲接口,而且Core中的core和graph兩個類也分別進行封裝,從而外部僅能調用Core模塊的接口,而Base暴露的也僅僅是開始運行的方法。c++
接口設計咱們嚴格參照了做業的要求,即Core模塊只暴露兩個函數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)
。其中須要注意的是二者的返回值均是單詞鏈的單詞個數(做業中的「單詞鏈長度」)。git
咱們從開始編程時便有意地爲鬆耦合作準備,具體操做即是開始時將Base和Core分開,而且Base也僅僅調用以上約定的兩個函數。在將Core封裝爲dll後Base的代碼幾乎能夠直接應用,而且最終僅須要更換Core.dll文件即可切換計算模塊。github
計算模塊主要由兩個類實現,分別是core和graph。其中core主要負責參數的讀入以及動態規劃算法的實現,而graph主要負責圖的創建以及圖論方面算法的實現,例如拓撲排序和DFS主要在graph中完成。算法
core類中,因爲暴露在外的兩個接口gen_chain_word
和gen_chain_char
僅僅是在初始化圖的時候有所不一樣(word函數將圖的邊權值所有初始化爲1,而char函數按單詞長度初始化權值),所以最終咱們選擇使用一個公共的函數做爲內部的實現,經過一個參數區分調用的是哪個接口。編程
算法流程圖:
具體實現中,首先在單詞進入Core前就要對words數組進行檢查,若其中包含非法字符則報錯,不然將全部字母轉爲小寫再進入具體算法。以後進入delete_repeat_words
將重複的單詞刪掉,由於單詞鏈中不容許出現重複單詞。windows
接下來進入main_func
,這裏須要按照單詞列表和輸入的模式創建圖,並初始化好各個數據結構。以後對圖進行拓撲排序,如有環且沒有指定-r則報錯,不然按照圖中所示分別選擇兩中不一樣的算法進行計算。數組
在計算結束後須要檢查答案是不是一個詞,由於動態規劃僅能找到最長的邊鏈條,但並不能排除存在單個詞很是長的狀況,所以若是答案符合這種形式仍須要刪掉這個詞並從新計算。網絡
算法的獨到之處:數據結構
這一部分中咱們花費了約3-4小時。咱們主要針對有環狀況的算法進行改進。因爲在一個有向有環圖中尋找最長的路徑是一個NP問題,從算法自己的角度來看不管如何都逃不開NP這個坑,所以咱們使用了普通的DFS來進行。咱們優化的地方在於將算法中訪存的消耗盡量下降。起初咱們使用了vector做爲路徑存儲的數據結構,在通過一次性能分析後咱們發現遞歸部分中退棧操做不少,所以咱們將vector改成一維數組,將原先的pop_back變成棧頂指針的--操做,從而下降了時間消耗。函數
下圖爲有環狀況下消耗CPU最多的函數,即DFS的主函數
咱們使用一個9000餘詞的文件對於無環狀況進行了測試,結果發現算法在無環狀況下表現良好,gen_chain_word/char中消耗最多的部分其實是接口中進行malloc的部分。
契約式設計的主要思路是設計接口時提供接口的先驗條件、後驗條件和不變式,這讓我想起了OO課上學習的JSF。
契約式設計的優勢有:
契約式設計的缺點是上手較難,一開始不容易嚴格按照要求實現,而且部分邏輯較爲複雜的函數使用契約時也比較麻煩。
在咱們實現的過程當中,咱們對函數運行先後的狀態事先進行了約定,從而能順利地將不一樣的函數串接起來造成完整的程序。但咱們並無在實際實現中添加斷言等來進行嚴格的約束。
單元測試範例
單元測試部分咱們分別對無環和有環以及各類異常狀況進行了測試,共構造了19個單元測試。
構造測試的思路大致上是儘量覆蓋各個分支。因爲char和word兩個接口在具體運行時僅僅是初始化權值不一樣,所以測試中咱們更注重於各類特殊狀況的測試,如開頭和結尾的自環等。
單元測試的分支覆蓋率爲97%,其中包含了各類異常測試的覆蓋。
計算模塊因爲僅僅將兩個函數接口暴露給用戶,從而用戶在調用函數時能夠存在各類不合法的輸入。主要的不合法輸入包括如下幾點
針對以上的錯誤咱們分別構造了以下測試
void invalid_char_test() //不合法字符 { char* words[] = { "a12345", "avdc", "fewt" }; char* results[100]; int len = gen_chain_word(words, 3, results, 0, 0, true); } void empty_string() //空串 { char* words[] = { "", "avdc", "fewt" }; char* results[100]; int len = gen_chain_word(words, 3, results, 0, 0, false); } void not_enough_words() //單詞不夠(一個詞不成鏈) { char* words[] = { "a" }; char* results[100]; int len = gen_chain_word(words, 1, results, 0, 0, true); } void has_loop() //(存在環) { char* words[] = { "aba", "aca", "fewt" }; char* results[100]; int len = gen_chain_word(words, 3, results, 0, 0, false); } void invalid_head() /(非法頭部) { char* words[] = { "aba", "avdc", "fewt" }; char* results[100]; int len = gen_chain_word(words, 3, results, 1, 0, false); } void invalid_tail() //(非法尾部) { char* words[] = { "aa", "avdc", "fewt" }; char* results[100]; int len = gen_chain_word(words, 3, results, 0, 1, false); } }
而且使用諸如如下格式的接口進行單元測試
TEST_METHOD(WordsException9) { Assert::ExpectException<std::invalid_argument>([&] {exception_test::invalid_head(); }); try { exception_test::invalid_head(); } catch (const std::exception& e) { Assert::AreEqual("Core: invalid head or tail", e.what()); } }
(在參閱了一些同窗的博客後我感受這種單元測試的寫法有點彆扭,實際上僅僅是爲了調用ExceptException接口而搞得如此複雜……)
界面主要分爲兩大部分:命令行參數讀取解析及文件讀取。
在命令行參數解析部分,因爲目前已有不少的開源庫可供使用,本着不重複造輪子的原則咱們使用了一個較爲輕量的頭文件庫cxxopts來處理命令行參數。
Github地址:https://github.com/jarro2783/cxxopts/ 簡單的使用介紹: 引入頭文件庫: #include <cxxopts.hpp> 建立一個cxxopts的Option實例,輸入參數是程序名和程序的描述 cxxopts::Options options("MyProgram", "One line description of MyProgram"); 使用add_options方法添加參數,其中括號內第一個參數爲短參數和長參數名,第二個參數是描述,可選的第三個參數是選項後輸入的實參。 options.add_options() ("d,debug", "Enable debugging") ("f,file", "File name", cxxopts::value<std::string>()) ; 使用parse方法對輸入的參數進行分析,其中這裏輸入的兩個參數爲main函數讀取的argc和argv。 auto result = options.parse(argc, argv); 使用 `result.count("option")` 獲取名爲「option」的參數出現的次數,並使用: result["option"].as<type>() 來獲取其值。注意到若是option不存在會拋出異常。
(以上內容摘自項目Github主頁)
這個庫能夠自動將各個參數的值讀出並對不合法的狀況拋出異常。但因爲其涉及的不合法狀況較爲樸素,我對一些相對複雜的不合法輸入進行了處理,例如同時輸入-w和-c、或輸入了兩個-w的狀況。
最終我將命令行參數讀取和分析封裝在base類的parse_arguments函數中,函數經過參數將讀取到的值返回。
在文件讀取部分,我設計的思路是按字符從文件頭開始向後掃描,並在出現特殊字符的位置斷開,從而將文件中的合法單詞分隔出來。
注意到在讀文件過程當中我對單詞的長度也作了約束,如讀到長度過長的單詞則會拋出異常。最終將以上單詞保存在base類成員中的數組內便可。
界面模塊我也寫了一些單元測試,主要測試以上的兩個函數。其中大部分測試均針對命令行參數的解析。例如:
dll模塊對接方面我使用了Base模塊顯式調用Dll的方法(即僅藉助dll文件,不借助lib文件)。經過windows.h中的LoadLibrary和GetProcAddress實現。最終將調用的過程與開頭讀文件、解析參數等過程結合,造成完整的運行程序。
最終只要將Core.dll文件放置在可執行文件同一目錄下,即可以運行程序計算結果。
咱們與1610106一、16061118組互換了Core.dll進行測試。
咱們的dll文件能夠在對方的界面模塊下運行
但對方的Core模塊並不能被咱們的base模塊調用。詢問得知對方的Core以C#實現,當我使用dumpbin查看其中導出的函數時並不能查看到任何信息。
在上網查閱了一些資料後我瞭解到C++調用C#須要將dll轉換爲C#的類組件,貌似並無相似於c++這樣直接調用的方法。
我和個人結對隊友因爲宿舍距離很遠,每次去咖啡廳又很貴,最終選擇了使用Teamviewer+視頻的方式進行結對合做。好在使用Teamviewer時網絡很流暢,而且對方也能夠在個人電腦上操做,甚至比兩我的在線下結對還要方便一些。這也讓我對結對編程有了更深的認識,結對不必定必須兩我的坐在一塊兒才能完成,只要溝通渠道方便、代碼均可以看到,就能方便地進行結對編程。固然因爲咱們開始的晚了一些,中間某些時候也不得不採用了並行的方式。
結對編程相比於我的開發和雙人並行開發而言,好處是實時的複審能夠避免不少小的問題。我在和隊友結對編程的過程當中,發生的一些筆誤或者算法方面的疏忽均可以被及時地發現並改正。而且結對編程在討論中進行,對於編程過程當中模塊間的約束也更加清晰。結對編程的缺點是假如兩我的時間常常錯開(假若有12小時時差),或兩人的時間安排有差,則很難進行。
成員 | 優勢 | 缺點 |
---|---|---|
周博聞 | 1.可以較快解決各類工程方面的問題 2.可以想到一些易被忽略的點 3. 快速上手新知識並加以應用 | 有些地方不夠細緻,致使浪費不少時間找小bug |
庹東成 | 1.算法能力很強 2.思路清晰,遇到新的問題能找出不少辦法 3.結對中效率高,對隊友很友善 | 命名及代碼格式有些不太規範 |