BUAA軟體工程:結對程式設計——尋找單詞鏈
- 教學班級:週二班
- 專案地址:https://github.com/1aureate/2022-BUAA-SE-pair
專案 | 內容 |
---|---|
這個作業屬於哪個課程 | 2022春季軟體工程(羅傑 任健) |
這個作業屬於哪個課程 | 結對程式設計專案-最長英語單詞鏈-CSDN社群 |
我在這個課程的目標是 | 瞭解並提高自己對軟體工程的認識和實踐能力 |
這個作業在哪個具體方面幫助我實現目標 | 瞭解了何為結對程式設計,增加了專案合作開發經驗 |
1. 簡介
結對程式設計是指兩個人坐在一起,用一臺電腦一套鍵鼠進行編碼。一個人寫程式碼,一個人在旁邊“領航”。通過結對程式設計,可以使程式碼審查變為實時進行,兩個人的思考效率也要更高,還防止了自己寫程式碼容易划水摸魚。
本次與同學結對完成一個尋找單詞鏈的小程式,從簡單的命令列程式到單元測試、封裝動態庫、編寫圖形介面、處理異常資訊、提高單元測試的程式碼覆蓋率,我們體驗了一個需求的實現從簡單、不可靠,逐漸變得規範化、更有可靠性、更有可拓展性。通過這次實驗,我們也體會到了結對程式設計的好處與缺點,為今後的軟體編寫積累了寶貴的經驗。
2. PSP表
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | 60 | 40 |
· Estimate | · 估計這個任務需要多少時間 | 60 | 40 |
Development | 開發 | 1510 | 1710 |
· Analysis | · 需求分析 (包括學習新技術) | 80 | 40 |
· Design Spec | · 生成設計文件 | 120 | 240 |
· Design Review | · 設計複審 (和同事稽核設計文件) | 40 | 60 |
· Coding Standard | · 程式碼規範 (為目前的開發制定合適的規範) | 10 | 10 |
· Design | · 具體設計 | 100 | 120 |
· Coding | · 具體編碼 | 600 | 700 |
· Code Review | · 程式碼複審 | 200 | 180 |
· Test | · 測試(自我測試,修改程式碼,提交修改) | 360 | 360 |
Reporting | 報告 | 590 | 520 |
· Test Report | · 測試報告 | 210 | 180 |
· Size Measurement | · 計算工作量 | 60 | 40 |
· Postmortem & Process Improvement Plan | · 事後總結, 並提出過程改進計劃 | 320 | 300 |
合計 | 2160 | 2270 |
3. 介面設計方法
我們將程式的各個部分分成了不同的類,並儘量減少類之間的依賴性,使他們儘量”通過資料互動“,這樣只要保證某一階段的輸出格式不變,內部如何實現可以隨意改動。
這種方法保證了不同類之間是相互獨立的、不透明的,耦合度較低。
4. 介面設計與實現
不同階段對原始碼結構進行了較大的改動。
第一階段中,我們的主題任務是實現命令列程式,因此將所有的類都放在命令列專案中組織編寫。第一階段的末尾另建了一個dll專案與gui專案,dll通過呼叫命令列專案的原始碼生成動態庫。
第二階段時,我們發現命令列程式也應該呼叫動態庫,而不應該直接使用原始碼,因此我們將主體程式碼轉移到了dll專案中,並將命令列程式修改為呼叫動態庫的版本。
在這裡,我僅說明最終的專案中的主要類。不同專案中的類互相不影響。
在dll專案中,主要有:
類名 | 作用 |
---|---|
ParamHandler |
儲存從介面傳入的引數資訊,在後續計算中獲取引數資訊,如是否允許隱藏鏈 |
WordListHandler |
根據引數和已經得到的單詞列表,找出相應的單詞鏈並返回結果 |
Word |
由head(char), tail(char), contents(vector<string>) 組成,將有相同頭尾的單詞組合到一起,減小計算量。由於單詞僅有小寫字母,因此最多隻有\(26\times26=676\)個結點。 |
在命令列專案中,主要有:
類名 | 作用 |
---|---|
ParamHandler |
處理從命令列讀入的引數,包括異常處理 |
InputsHandler |
根據ParamHandler 得到的檔名,開啟檔案並從中提取單詞列表 |
OutputsHandler |
將動態庫的返回結果列印到標準輸出或者檔案中 |
ErrorCodeHandler |
將動態庫返回的錯誤碼對應到具體的錯誤資訊,其實就是個map<int, string>
|
類的數量較少,互動也非常簡單,再次就不詳細贅述了。
演算法的核心部分在WordListHandler
中,具體的演算法流程如下圖所示:
5. UML圖
6. 效能改進
只能想到用DFS計算,沒想出什麼好的效能改進方法。
效能分析圖如下:
可以看到,WordListHandler部分(也就是計算單詞鏈)佔據了大部分的時間,IO以及提取單詞佔據的時間不多。
7. Design by Contract, Code Contract
優點:
- 形式嚴謹,方便測試人員根據Constract設計測試用例
- 邏輯清晰,減少程式碼編寫過程中產生的小疏忽,如忘記處理空字串
缺點:
- 繁瑣。比如一個方法的作用是“找出最短路“,這一條簡單的自然語言需求需要轉換成極其複雜的形式語言才能說明。
由於本次開發工作量大,因此沒有采取這種對程式碼約束較強的開發形式。但是,我們有許多口頭上的contract
- 類之間儘量不互相引用,通過資料傳遞資訊
- 儘量使用int作為返回值,成功返回0,不成功返回負數。
8. 單元測試展示
結對程式設計的時間緊迫,我們並沒有時間正交開發或者尋找小組對拍,因此僅通過手動構造測試樣例的方法對程式碼進行單元測試。
以對gen_chain_word
函式的測試為例:
TEST_METHOD(testGenChainWord) {
// 將每個測試樣例抽象化為一個類,批量處理
class GenChainWordTestCase {
public:
char* words[100];
int len;
char head;
char tail;
bool enable_loop;
int stdChainNum;
GenChainWordTestCase(char* words[], int len, char head, char tail, bool enable_loop, int stdChainNum)
: len(len), head(head), tail(tail), enable_loop(enable_loop), stdChainNum(stdChainNum) {
for (int i = 0; i < len; i++) {
this->words[i] = words[i];
}
}
};
std::vector<GenChainWordTestCase> testCases;
char* words1[] = { "woo", "oom", "moon", "noox" };
testCases.push_back(GenChainWordTestCase(words1, 4, 0, 0, false, 4));
testCases.push_back(GenChainWordTestCase(words1, 4, 'w', 0, false, 4));
testCases.push_back(GenChainWordTestCase(words1, 4, 'o', 0, false, 3));
testCases.push_back(GenChainWordTestCase(words1, 4, 'n', 0, false, 0));
testCases.push_back(GenChainWordTestCase(words1, 4, 0, 'x', false, 4));
testCases.push_back(GenChainWordTestCase(words1, 4, 0, 'n', false, 3));
testCases.push_back(GenChainWordTestCase(words1, 4, 0, 'o', false, 0));
char* words2[] = { "Algebra", "Apple", "Zoo", "Elephant", "Elephant", "Under",
"Fox", "Dog", "Moon", "Leaf", "Trick", "Pseudopseudohypoparathyroidism" };
testCases.push_back(GenChainWordTestCase(words2, 12, 0, 0, false, 4));
char* words3[] = { "woo" };
testCases.push_back(GenChainWordTestCase(words3, 1, 0, 0, false, 0));
char* words4[1] = {};
testCases.push_back(GenChainWordTestCase(words4, 0, 0, 0, false, 0));
char* words5[] = { "woo", "oom", "moow" };
testCases.push_back(GenChainWordTestCase(words5, 3, 0, 0, false, -3));
testCases.push_back(GenChainWordTestCase(words5, 3, 0, 0, true, 3));
testCases.push_back(GenChainWordTestCase(words5, 3, 'w', 0, true, 3));
testCases.push_back(GenChainWordTestCase(words5, 3, 'o', 0, true, 3));
testCases.push_back(GenChainWordTestCase(words5, 3, 'm', 0, true, 3));
testCases.push_back(GenChainWordTestCase(words5, 3, 0, 'o', true, 3));
testCases.push_back(GenChainWordTestCase(words5, 3, 0, 'm', true, 3));
testCases.push_back(GenChainWordTestCase(words5, 3, 0, 'w', true, 3));
testCases.push_back(GenChainWordTestCase(words5, 3, 1, 'w', false, -4));
testCases.push_back(GenChainWordTestCase(words5, 3, 0, 2, false, -4));
char* words6[] = { "woo", "o", "oon" };
testCases.push_back(GenChainWordTestCase(words6, 3, 0, 0, false, -1));
char* words7[] = { "woo", "123", "oon" };
testCases.push_back(GenChainWordTestCase(words7, 3, 0, 0, false, -1));
auto result = new char* [10000];
// 僅比較結果的大小,比較返回結果比較困難。
for (auto& testCase : testCases) {
int chainNum = gen_chain_word(testCase.words, testCase.len, result,
testCase.head, testCase.tail, testCase.enable_loop);
Assert::AreEqual(chainNum, testCase.stdChainNum);
}
}
構造思路:
先構造正常資料:輸入資料是正常的,簡單的規定一下-h, -t
引數,看看程式是否能跑出正確結果
再構造不那麼正常的資料:只有一個單詞、沒有單詞。
再構造會丟擲異常的資料:非法字元、存在單詞鏈、引數異常。
單元覆蓋率截圖:
使用Visual Studio 2019企業版自帶的覆蓋率測試工具進行測試,覆蓋率如下:
有一些點比較零碎,因此沒有被覆蓋到。比如返回結果超過20000個、檔案不存在等等。整體的覆蓋率達到93%以上。
9. 異常處理說明
動態庫:
每個對外暴露的函式都會catch各種丟擲的異常,並根據錯誤程式碼表返回不同的錯誤程式碼。
0: Everything is alright;
-1: Illegal word exists.
-2: More than 20000 results.
-3: EnableLoop is false, but loop is found.
-4: Illegal head or tail letter.
others: Unexpected exception occured, please contact the developer.
具體的:
-
-1:words中存在非法單詞
-
測試:
-
char* words6[] = { "woo", "o", "oon" }; testCases.push_back(GenChainWordTestCase(words6, 3, 0, 0, false, -1)); char* words7[] = { "woo", "123", "oon" }; testCases.push_back(GenChainWordTestCase(words7, 3, 0, 0, false, -1));
-
-
-2:超過20000個結果
- 這一條並沒有構建測試用例
-
-3:沒有允許隱藏環的情況下出現環
-
// false代表不允許環,true是允許環 char* words5[] = { "woo", "oom", "moow" }; testCases.push_back(GenChainWordTestCase(words5, 3, 0, 0, false, -3)); testCases.push_back(GenChainWordTestCase(words5, 3, 0, 0, true, 3));
-
-
-4:
head或tail
不是小寫字母-
testCases.push_back(GenChainWordTestCase(words5, 3, '*', '@', false, -4)); testCases.push_back(GenChainWordTestCase(words5, 3, 0, '$', false, -4));
-
命令列應用:
命令列應用是直接呼叫動態庫的,並沒有給命令列單獨進行單元測試,無法提供測試樣例。
- 輸入引數異常:
- 沒有引數
- 出現不存在的引數
-
-h, -t
後沒有緊跟一個字母 - 沒有輸入檔名
- ......
- 輸入資料異常:
- 打不開檔案
- 檔名不以“.txt”結尾
- 在沒有
-r
引數的情況下出現單詞環
- 動態庫給出的異常
當命令列應用發現異常,會以錯誤資訊的形式輸出到視窗中,並終止程式。
10. 介面模組設計過程
使用pyqt簡單的設計了一個介面,實現了課程組的基本要求。
首先,用QTDesigner實現介面元素的設計
然後,用pyui將ui檔案轉換成py檔案
最後,手動給py檔案中的類新增邏輯關係。
11. 介面模組與計算模組對接
通過動態庫與計算模組進行對接。由於我們沒有找到使用python傳遞二維陣列的方法,因此為gui單獨設計了一套介面,叫xxx_python
extern"C" __declspec(dllexport) int gen_chains_all_python(char* words, char** result, char** error_msg);
extern"C" __declspec(dllexport) int gen_chain_word_python(char* words, char** result, char head, char tail, bool enable_loop, char** error_msg);
extern"C" __declspec(dllexport) int gen_chain_word_unique_python(char* words, char** result, char** error_msg);
extern"C" __declspec(dllexport) int gen_chain_char_python(char* words, char** result, char head, char tail, bool enable_loop, char** error_msg);
效果展示
正常執行 | |
---|---|
出現異常 |
12. 結對的過程
第一次嘗試兩個人坐在一起寫一份程式碼這種形式,剛開始兩人的熱情都很高漲,用一個下午初步確定瞭如何分析、儲存命令列引數並供給後續方法呼叫。
然而到了後期,兩個人的時間漸漸無法統一,結對的機會越來越少,專案變成了實際上的“二人合作開發”。
兩人結對程式設計時的照片:
13. 優缺點
結對程式設計的優缺點:
優點 | 缺點 |
---|---|
不宜走神,減少摸魚 | 對於簡單的模組,並不需要兩個大腦,降低了效率 |
兩個人的思路互相結合,形成1 + 1 > 2的效果,加快了專案的推進 | 兩個人的時間很難統一 |
程式碼審查實時進行,減少了程式碼出錯的可能性 | 對於一些“非理性”的問題,比如新增一種設計麻煩/不麻煩,兩個人很難形成共同意見 |
結對成員的優缺點:
李浩宇 | 徐家樂 |
---|---|
優:面向物件思想十分先進 | 優:十分喜歡面向過程程式設計 |
優:十分討厭c++語言 | 優:比較喜歡c++語言 |
優:喜歡良好的設計 | 缺:做事比較急功近利 |
缺:容易陷入個性的設計 | 優:python gui 開發較快 |
優:單元測試能力 very good | 優:會不斷督促專案進度 |
缺:懶惰成性,不愛幹活 | 缺:菜,演算法純純白給 |