https://github.com/supplient/longest_word_chainc++
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | ||
· Estimate | · 估計這個任務須要多少時間 | 45 | 30 |
Development | 開發 | ||
· Analysi | · 需求分析 (包括學習新技術) | 120 | 180 |
· Design Spec | · 生成設計文檔 | 45 | 0 |
· Design Review | · 設計複審 (和同事審覈設計文檔) | 45 | 0 |
· Coding Standard | · 代碼規範 (爲目前的開發制定合適的規範) | 30 | 45 |
· Design | · 具體設計 | 120 | 60 |
· Coding | · 具體編碼 | 480 | 450 |
· Code Review | · 代碼複審 | 120 | 180 |
· Test | · 測試(自我測試,修改代碼,提交修改) | 360 | 450 |
Reporting | 報告 | ||
· Test Report | · 測試報告 | 60 | 60 |
· Size Measurement | · 計算工做量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 過後總結, 並提出過程改進計劃 | 30 | 0 |
合計 | 1485 | 1485 |
cmdUI中的CoreSetting類的接口設計遵循了數據封裝的思想,解析後的結果不對外直接開放,僅能經過const函數進行查詢。它的接口設計簡單樸素,符合cmdUI對它的IO需求,不須要額外添加適配器進行轉換。並且由於遵循了鬆散耦合的思想,因此不須要非得要cmdUI才能調用CoreSetting,這使得能夠爲之單獨構造單元測試。git
UIUtility整個模塊的存在遵循了鬆散耦合的思想。由於UI控制部分中存在和UI形式關聯不多的部分,因此單獨將這部分代碼抽離出來做爲一個獨立的模塊,方便不一樣種類的UI共通調用。在這之中,StreamReader是其主體類,它的接口設計很是樸素,若是將read和getReadLen看作一個調用過程的話,它能夠被看作是一個無反作用的算法類。同時,咱們爲了使得調用UIUtility的過程的複雜度看起來類似,咱們提供了一個封裝類FileReader做爲接口來簡化對文件的調用時的複雜度。程序員
在Core模塊中,咱們將無狀態的Core類做爲其惟一的導出類。這也就意味着對於其餘模塊而言,Core模塊的接口有且僅有一個Core類。並且由於Core類無狀態,因此無反作用,這也就意味着容許多個模塊並行調用Core模塊,符合計算模塊的設計思想,不過在本次做業中這屬於多此一舉就是了。Core模塊內部咱們使用ChainSolver做爲實現類,它的全部內部計算數據都不對外開放,返回的也是新構造且不做管理的新內存,這確保了Core類的方法中能夠簡單地構造ChainSolver類,而後委託運算,而不須要作額外的適配工做。github
Core模塊自己遵循了鬆散耦合的思想,正如上點所說,它容許多個模塊並行調用,這點的前提是它能夠被多個模塊調用。因此Core模塊的實現是徹底獨立,甚至於接口自己都是C化,使用char*[],而非string或者CString,這使得它與UI模塊實現解耦,使得不一樣種UI模塊調用它的複雜度都比較接近。算法
咱們認爲此次結對編程的題目能夠抽象爲有向有環圖求最長路徑,目前咱們使用的算法仍是深度遍歷窮舉所用可能的路徑,該圖有26個點對應26個英文字母,每一個邊表明輸入的單詞,例如輸入"hello"能夠化爲從點'h'到'o'的邊。編程
首先根據五項需求-w-r-h-t-c, 其中單獨的-w是能夠約化到-r的,而單獨的-r是不能約化到-w,對於-h-t只是對路徑首尾進行檢測約束,對於-c只是將圖的邊加權。因此算法應該針對-r設計。深度遍歷的過程將從一個點開始,例如'h',若是該點有指向點的邊,則遞歸訪問下一點,下一點的遞歸完成後纔會訪問'h'的其餘邊的指向。後端
邊和點的struct分別是:數組
struct Edge { std::string word; int code; int weight; int next; };
邊Edge的word存儲輸入的原始字符串;code是該邊的識別編碼;weight是該邊權重,weight默認爲1,在-c時爲字符串長度;next是字符串末尾字符ascii碼-'a'的值,方便將點的編碼都歸爲0-25。數據結構
struct WordMap { std::vector<Edge> toLast; };
點WordMap只包含一個邊的vector。多線程
核心計算包括兩個類Core和ChainSolver,Core負責標準輸入,全部外部類和函數只調用Core,Core再調用ChainSolver的get_max_chain函數。
ChainSolver類有三個函數公有函數get_max_chain、私有函數CreateMap、私有函數Recursion,get_max_chain調用CreateMap來建立圖,再調用Recursion遞歸DFS建立過的圖。
接收如下參數:
char *input[], int num, char *result[], char head, char tail, bool isGetMaxChar, bool enable_loop
以上參數分別是輸入的單詞數組,輸入的單詞量,須要輸出結果的result,head爲-h的頭部要求,tail爲-t的尾部要求,isGetMaxChar是-c的邊權重修改要求,enable_loop是-r的要求。
這個部分須要考慮重複輸入的單詞並將其拋棄,map<string,int>類型的inputWord存儲的是已錄入的單詞。
if (inputWord.find(s) != inputWord.end()) { return 0; } inputWord.insert(std::pair<std::string, int>(s, code));
單詞編碼部分使用最普通的線性編碼0-100,最開始想到過APHash編碼可是對於做業裏面的小規模的輸入太過拖沓。
使用的是遞歸法DFS,對傳入節點進行路徑窮舉遍歷,進入過的點和邊都會被記載在isUsedPoint和isUsedEdge中,遞歸返回後再將記載記錄釋放。優化部分會在第六節描述。
咱們對DFS窮舉法的主要優化是自環剪枝,很明顯在求-r最長路徑的過程當中自環是一定須要走的邊,不須要再遞歸斷定了,因此在Recursion進入節點的時候先將該節點全部未走過的自環邊加入路徑中。
//ensure the edge that wait to push is not used before. if (isUsedEdge[iter.code]) continue;//'continue' will jump this edge. //push every self-circle edge. path.push_back(iter.word); if (iter.next == point) { len+=iter.weight; continue; } ... ... ... //pop every self-circle edge. for (auto iter : map[point].toLast) { if (iter.next == point) { isUsedEdge[iter.code] = false; path.pop_back(); } else { break; } }
輸入爲55個徹底隨機的單詞的狀況下,VS性能分析結果以下:
從圖中能夠看到ChainSolver::get_max_chain函數佔用CPU資源總計約30%, 佔整個後端main調用的四分之三,遞歸遍歷毫無疑問是整個項目裏面的性能瓶頸。
優勢:
缺點:
個人見解是DbC應該適度、局部地被使用,過分追求徹底的DbC是不可取的。適度的DbC在此次的結對編程中咱們也使用了。
固然,這也是由於咱們此次的做業的複雜度並不高,再加上由於結對編程的關係,因此溝通時間不少不少,因此如此簡單形式的約定就可行了。更高複雜度的工程中,我以爲可能局部的,例如相似於Core模塊的算法類,中可能會須要嚴格的DbC來確保需求描述的準確性。
Core模塊的單元測試覆蓋率:
對Core模塊進行單元測試的過程是:構造測試樣例=>傳入進Core模塊=>檢查返回的單詞鏈的長度與內容是否正確。
其中,咱們將後兩步獨立出來,封裝進了testutil的兩個函數中。
兩個函數分別是:
void testRight(char* words[], int words_len, char* res[], int res_len, bool is_max_char, char head, char tail, bool enable_loop); void testRightMulti(char* words[], int words_len, vector<char**> res, vector<int> res_len, bool is_max_char, char head, char tail, bool enable_loop);
兩個函數的區別在於testRightMulti容許符合要求的最長單詞鏈有多條,Core模塊只要返回其中一條即爲正解。
值得關注的是enable_loop參數。
當enable_loop爲假時,會按索引序比較正解與Core返回的解;
當enable_loop爲真時,只會進行無序比較。
做爲展現的一個簡單的測試樣例:
TEST_METHOD(simple) { char* words[] = { "hello", "world", "ofk", "kw" }; char* res[] = { "hello", "ofk", "kw", "world" }; testRight(words, 4, res, 4); }
咱們構造測試樣例的方式有如下四種:
a. 分析做業要求,找出邊界條件,構造邊界樣例。
b. 根據算法的搜索方式的性質,構造針對性樣例。
c. 使用簡單算法自動生成大樣例,構造壓力樣例。
d. 針對可能被拋出的異常,構造異常樣例。
咱們首先細讀做業文檔,從中劃出能夠得出邊界條件的語句,而後針對該語句構造邊界樣例。這一過程在demand_analyze.md的前半段被體現。摘取其中一段:
在完成算法編寫後,針對算法中可能存在的分支、迭代,構造針對性樣例試圖去覆蓋,檢驗是否如預期那樣執行。這一過程在demand_analyze.md的後半段被體現。摘取其中一段:
咱們設計了一種簡單的算法來自動生成大的測試樣例。咱們稱這種算法爲PyramidGenerator。
算法的基本思路是構造一個最多隻有26個非根節點的樹,且該樹具備最長深度的節點有且僅有一個。
如圖構造樹,每個節點表明一個字母,每條邊表示以起點爲首字母、以終點爲尾字母的一族單詞。由此,每條邊上均可以生成無數單詞。
而若是咱們控制root-t-e-o這條路徑上的每條邊都只生成一個單詞,例如:tme-ego,那麼該路徑的單詞連起來就是惟一最長單詞鏈。
經過使用PyramidGenerator,咱們能夠生成任意大小的無環測試樣例。
咱們簡單地針對Core執行中會拋出的異常構造對應的樣例,當該樣例被輸入後,嘗試捕獲異常。
計算部分一處異常處理,當沒有啓用-r功能卻檢查到有環圖時將拋出異常w_c_h_t_ChainLoop,如下代碼的斷定條件是該邊的終點已被走過,同時沒有開啓-r,同時不是自環。
if (isUsedPoint[iter.next] && !isEnableLoop && point != iter.next) { throw w_c_h_t_ChainLoop; }
另外也須要考慮對Core的單元測試異常拋出,其實如下異常在程序做爲總體運行時不會拋出:
傳入的尾部要求是否合理:
if (tail_input != 0 && (tail_input < 'a' || tail_input > 'z')) { throw para_tail_error; }
傳入的頭部要求是否合理:
if (head_input != 0 && (head_input <'a' || head_input > 'z')) { throw para_head_error; }
傳入的enable_loop是否合法:
try { isEnableLoop = enable_loop; } catch(...){ throw para_loop_error; }
傳入的input數組裏面是否是的確有num個值:
for (i = 0; i < num; i++) { try { CreateMap(input[i], isGetMaxChar); } catch (...) { throw para_input_error; } }
傳入的res是否能接受結果:
try { for (auto iter : maxPath) { char *new_str = new char[iter.length() + 2]; // std::cout << iter << " "; for (unsigned int j = 0; j < iter.length(); j++) new_str[j] = iter[j]; new_str[iter.length()] = '\0'; result[i] = new_str; // TODO release such memory i++; } } catch (...) { throw para_res_error; }
傳入input輸入字符串是否包括非法字符(在建立圖時統一拋出異常):
int ChainSolver::CreateMap(char *c_s, bool isGetMaxChar) { try { ... ... ... } catch (...) { throw create_map_error; } return 0;
界面分爲三個組成部分:
a. UIUtility: UI共通的代碼的集合
b. cmdUI: 基於命令行實現的UI
c. MFCUI: 基於MFC實現的GUI
由於不管是命令行實現仍是MFC實現,只要是UI界面就會有部分共通的邏輯須要處理。
因此咱們將這部分邏輯抽離出來,編譯成UIUtility.dll。
實際被抽離出來的邏輯爲對文本輸入進行分詞,獲得英語單詞數組的部分。咱們將其稱爲StreamReader,它接受一個輸入流,輸出對應的單詞數組。
爲了方便調用,咱們也提供了一個做爲StreamReader的封裝的FileReader,它接受一個文件名,並將該文件的輸入流做爲StreamReader的輸入。
StreamReader的具體實現是從流中逐字符讀取,自身維護一個臨時單詞與單詞數組。
這部分實現了以命令行參數爲用戶輸入的用戶界面。
對命令行參數進行解析的工做被抽離出來在CoreSetting中被實現。CoreSetting會檢驗參數的合法性,並解析它的含義。
cmdUI的主程序就可使用CoreSetting解析出來的結果作進一步的操做。
以後主程序簡單地調用Core模塊,而後將結果輸出至文件即結束程序。
這部分實現了基於MFC實現的GUI。
按照做業要求,GUI實現須要
咱們的設計中,整個界面分爲4塊:
輸入
經過兩個單選按鈕來控制輸入方式。對於直接輸入就直接用一個文本框接收輸入,對於文件輸入就提供一個地址輸入欄和文件打開按鈕來讓用戶給出文件地址。選項
選項分爲三組。對於-w, -c這組,使用單選按鈕保證用戶只能夠選取其中一個按鈕。對於-h, -t,使用勾選框來控制是否添加,同時給出一個只接受一個英文字符的文本框來接受指定字母做爲參數。對於-r,簡單地使用一個勾選框來表示是否啓用該參數。運行
運行包括程序的執行與程序的關閉兩部分。也就是Run和Cancel兩個按鈕。當用戶點擊Run時,程序將會讀取輸入和選項中的數據,並檢查數據合法性,以後委託給Core執行,最後將返回的結果填充進輸出部分。當Cancel被點擊時,簡單地結束程序的運行。輸出
輸出分爲兩部分。第一部分是打印顯示,當Run被點擊,而且數據被順利處理之後,返回的結果會直接打印到一個文字框中。第二部分是導出,相似於輸入部分的地址選擇,咱們也提供了接口供用戶選擇輸出文件,並提供Export按鈕來執行導出功能。
大部分代碼都是簡單顯然的。值得一提的是選項中的-h, -t所使用的文本框的實現。咱們的作法是,每當文本框內容被改變,檢查文本長度,若大於1,則截斷前面的,只留下最後一個字符。而後檢查該字符是否爲英文字母(大小寫無所謂,自動轉化成小寫字母),若非英文字母則忽略。
UI模塊的設計已在10中詳細描述,此處不重複。
咱們將Core模塊做爲一個獨立的dll抽離出了代碼。咱們準備了一個導出類Core來做爲Core模塊總體對外的接口,UI模塊能夠經過簡單地調用這個Core類的靜態函數來調用Core模塊。
在UI模塊調用dll模塊時,咱們採起的方案是隱式調用。爲了支持這樣的作法,咱們在UI模塊的附加依賴項中加入了Core.lib,而且將Core.dll和UI模塊的可執行文件放在了同一個文件夾中。
關於具體函數接口,咱們只是簡單地按照做業的要求實現了Core的接口。接口函數內部會創建ChainSolver類來執行真正的計算過程。固然,這對UI模塊而言是透明的。
對於cmdUI,它能解析命令行參數並輸出結果至solution.txt中。
例如,命令行參數以下:
運行後的控制檯顯示:
同時,BIN目錄下產生輸出文件solution.txt:
而若是參數不正確,例以下圖的非法參數-k:
則會提示錯誤,並終止程序:
對於MFCUI,它能提供一組控件來讓用戶輸入,並提供一組控件來讓用戶獲得輸出。
例如,執行後用戶界面以下:
用戶能夠經過這部分來選擇輸入模式,上面的是直接輸入文本內容,下面的是選擇文本文件做爲輸入:
能夠經過這部分來控制選項:
能夠經過這部分來查看輸出並選擇文件來導出:
例如,以下做爲輸入:
點擊Run後就會獲得以下輸出:
點擊Open選擇導出文件:
再點擊Export執行導出,若是成功就會有以下提示:
若是選項不正確,例以下圖的明明勾選了-h,卻沒有給出對應的首字母:
就會給出報錯提示:
咱們兩人的結對一直牢牢圍繞鄒欣老師《構建之法》一書的要求:
在結對編程模式下,一對程序員肩並肩、平等
地、互補地進行開發工做。他們並排坐在一臺電
腦前,面對同一個顯示器,使用同一個鍵盤、同
一個鼠標一塊兒工做。他們一塊兒分析,一塊兒設計,
一塊兒寫測試用例,一塊兒編碼,一塊兒作單元測試,
一塊兒作集成測試,一塊兒寫文檔等。
咱們兩人在這次結對編程以前就已熟識,此次結對編程的任務也必然是無縫銜接般地完成。在結對的過程當中咱們大部分地代碼都是坐在一塊兒完成的,一塊兒分析一塊兒設計。固然第一次嘗試這樣的編程方式也會帶來不少疑惑,好比編程一方不免會出現一些沒必要要的並且較爲複雜的構想,好比在計算核心的部分是否須要將字符串進行哈希編碼,此時須要另外一方加入思考博弈,這樣的過程頗有趣不過也很花時間。
總的來講,咱們以爲結對編程是個高強度、注重思惟碰撞的編程方式,和傳統那種「各自碼各自的代碼再push」的方式,結對編程的過程當中咱們感覺到了實時性的交流和code review,這是一種很是敏捷的軟件開發方式。
結對編程的優勢:
結對編程的缺點:
搭檔的優勢:
搭檔的缺點:
咱們和馬振亞&馬浩翔組交換了模塊。
名字 | 學號 |
---|---|
馬振亞 | 16061109 |
馬浩翔 | 16061097 |
由於咱們兩組都將Core模塊封裝成了dll,因此交換過程很是簡單便捷。他們將他們Core的dll, lib, h文件給咱們,而後咱們調整了一下編譯選項、調用關係就能夠方便地使用他們的代碼。
銜接過程最大的不順利是由於咱們仿照了做業示例中的那樣將Core類的全部方法都聲明爲靜態方法,而他們組選擇的是將接口聲明爲Core類的成員函數。因此咱們不得不修改咱們的代碼去符合他們組的接口要求。
另外一個不順利是由於他們組是直接在Core類中實現了算法,因此他們的Core.h中包含了一些運行須要的庫函數的頭文件。而在咱們的機子上,他們所須要的stdc++.h頭文件並不存在,因此咱們不得不刪去這句,再包含vector和map來知足編譯需求。
銜接完成後在測試過程當中也發現了不一樣。主要是需求理解的不一樣,對於他們的Core模塊而言,空輸入、空輸出都是會拋出異常的、不合法的,但對於咱們的程序而言,這些都是合法的。