DeadLine:2017.10.15 3:00pm
我們在第一個作業中,用各種語言實現了一個命令行的生成數獨終局和求解數獨的小程序。我們看看如果要把我們的小程序升級為能穩定運行,給用戶提供服務的軟件,應該怎么做。
1.結對項目
第一階段目標:把生成數獨終局與求解數獨的功能封裝為獨立模塊,並設計單元測試。
大家的代碼都各有特色,大家寫的“軟件”也有一定的用處。如果現在我們要把這個功能放到不同的環境中去(例如,命令行,Windows圖形界面程序,網頁程序,手機App),就會碰到困難:許多同學的代碼都散落在各個函數中,很難把剝離出來作為一個獨立的模塊運行以滿足不同的需求。
我們看到,不同的代碼解決不同層面的問題:
- 有些是計算數據的(例如生成數獨)
- 有些是控制輸入的(例如scanf,cin,圖形界面的輸入字段)
- 有些是數據可視化的(例如printf,cout,println,DrawText)
有些則更為特殊,是架構相關的(例如main函數,並不是所有的程序都需要某個特定格式的main)
這些代碼的種類不同,混雜在一起對於后期的維護擴展很不友好,所以它們的組織結構就需要精心的整理和優化。
我們希望把生成數獨終局/求解數獨的功能能獨立出來,成為一個獨立的模塊(class library, DLL, 或其它),這樣的話,命令行和GUI的程序都能使用同一份代碼。為了方便起見,我們稱之為計算核心"Core模塊",這個模塊至少可以在幾個地方使用:
- 命令行測試程序使用
- 在單元測試框架下使用
- 與數據可視化部分結合使用
把計算核心在單元測試框架中做過完備的測試后,我們就可以在算法層級保證了這個模塊的正確性。
但我們知道軟件並非只有計算核心,實際的軟件是交付給最終用戶的軟件,除了計算核心外,還需要有一定的界面和必要的輔助功能。那么這個Core模塊和使用它的其他模塊之間是什么關系呢?它們要通過一定的API(Application Programming Interface)來和其他模塊交流。這個API接口應該怎么設計呢?為了簡單,我們可以從下面的最簡單的接口開始:
void generate(int number, int[][] result)
這個函數接受一個整數number
和一個大小為number x 81
的二維數組作為輸入,其中number
表示要求生成的數獨的個數,result用來存儲生成的number
個數獨終局。
注意:本次作業不再限定左上角的數字。
假設我們用類Core
封裝了這個接口,我們的測試程序可以是非常簡單的:
//調用Core中封裝好的函數
int result[100][81];
Core.generate(100, result);
bool valid = true;
for (int i = 0; i < 100 ; i += 1)
//對於第i個棋盤,左上角要求固定為1
valid &= (result[i][0]==1);
//我們斷言valid為true,即所有生成的數獨左上角都符合固定某個數字的要求
Assert(valid==true);
當然,我們這里的判斷並不充分,沒有驗證數獨終局本身的特性:每行每列每宮都只能由1-9不重復的數字組成,但同學們在測試時不能這樣“偷懶”。
在本次作業中,我們希望大家在個人項目的基礎上完成一個數獨游戲軟件,這個軟件會為用戶提供如下特色功能:
- 難度區分,每盤游戲用戶都可以選擇 容易/中等/難 三個等級。
- 提示功能,用戶不會的時候可以點擊‘提示’,程序就會提示當前空格應該填什么。
- 計時功能,能夠保持用戶的最佳記錄。
為了實現上述軟件,我們首先要在個人項目基礎上增量改進,實現一個Core模塊,並基於Core模塊實現在命令行測試程序中支持下述命令行參數(原有命令行參數不變)
-m 參數設定難度
命令行中使用-n和-m參數分別控制生成數獨游戲初始盤的數量與難度等級,
sudoku.exe -n [number] -m [mode]
-n和-m參數的限制范圍不同,具體約定如下:
- [number]的范圍限定為1 - 10000。
- [mode]的范圍限定為1 - 3,不同的數字代表了數獨游戲的難度級別,如下表所示:
編號 | 級別 |
---|---|
1 | 簡單 |
2 | 入門 |
3 | 困難 |
請在博客中說明你對於不同難度級別的嚴格定義,並說明這樣定義的理由。
例如下述命令將生成20個簡單級別的數獨游戲初始棋盤至文件sudoku.txt
中,挖空的地方用0表示:
sudoku.exe -n 20 -m 1
9 0 8 0 6 0 1 2 0
2 0 7 4 0 1 9 0 8
1 4 6 0 2 0 3 5 0
0 1 2 0 7 0 5 0 3
0 7 3 0 1 0 4 8 2
4 8 0 0 0 5 6 0 1
7 0 4 5 9 0 8 1 6
0 0 0 7 4 6 2 0 0
3 0 5 0 8 0 7 0 9
9 0 0 8 0 0 4 0 0
……
-r 參數設定挖空數量
命令行中使用-n參數控制生成數獨游戲初始盤的數量,-r參數控制生成數獨游戲初始盤中挖空的數量范圍,使用-u參數控制生成數獨游戲初始盤的解必須唯一,
sudoku.exe -n [number] -r [lower]~[upper] -u
-r參數的范圍限制如下:
- [lower] 的值最小為20,
- [upper] 的值最大為55,
- [upper] >= [lower]。
如果命令行中有-u參數,則生成的數獨游戲初始盤的解必須唯一;否則,則對解的數量不做限制。
注意: -m參數不與-r和-u參數同時出現,如果同時出現則提示參數的正確用法。
例如下述命令將生成20個挖空數在20到55之間的數獨游戲初始盤至文件sudoku.txt
中,
sudoku.exe -n 20 -r 20~55
下述命令將生成20個挖空數在20到55之間並且解唯一的數獨游戲初始盤至文件sudoku.txt
中,
sudoku.exe -n 20 -r 20~55 -u
現在,請同學們在個人項目的基礎上進行增量修改,根據以上修改自己的數獨項目。
完成上述接口后,我們要把之前程序中實現的其他功能也封裝成獨立的模塊並一一進行測試,比如讀取數獨題目文件、輸出打印等。建議大家在每一步只增量修改一個模塊並做測試。這里的測試包括新模塊的單元測試與原功能的回歸測試。每實現一個新的功能,要保證以前運行正確的例子繼續是正確的。通過這樣的回歸測試,可以保證自己實現的系統始終是滿足預定狀態約束的。(請看書中關於單元測試,回歸測試的內容)在確認修改的功能正確之后再簽入代碼。
項目要求:
- 在個人項目上增量修改,實現
void generate(int number, int mode, int[][] result)
接口,對於輸入的數獨游戲的需求數量和難度等級,通過result
返回生成的數獨游戲的集合。 - 對這一
generate
接口進行測試,把單元測試代碼Push到Github上(注意避免把單元測試的結果Push到Github上)。 - 實現
void generate(int number, int lower, int upper, bool unique, int[][] result)
接口,生成number
個空白數下界為lower
,上界為upper
的數獨游戲,並且如果unique
為真,則生成的數獨游戲的解必須唯一,計算的結果存儲在result
中。 - 對這一
generate
接口進行測試,把單元測試代碼Push到Github上(注意避免把單元測試的結果Push到Github上)。 - 實現
bool solve(int[] puzzle, int[] solution)
接口,對於輸入的數獨題目puzzle
(數獨矩陣按行展開為一維數組),返回一個bool
值表示是否有解。如果有解,則將一個可行解存儲在solution
中(同樣表示為數獨矩陣的一維數組展開)。 - 對
solve
接口進行測試,把單元測試代碼Push到Github上(注意避免把單元測試的結果Push到Github上)。 - 設計其他部分的接口,按照設計好的接口在個人項目基礎上增量修改,同樣把單元測試代碼Push到Github上。
- 在完成這一階段的任務之后,使用
git tag step1
標記第一階段已經完成,並在Push到Github上時使用--tags
參數把tag也推送到Github,例如git push origin --tags
。
博客要求:
- 詳細介紹你對於上述
Core
接口的實現,以及你為Core
模塊設計的其他接口,並說明UI模塊該如何使用這些接口。 - 選擇部分單元測試代碼發布在博客中,並說明測試的函數,構造測試數據的思路。
- 將單元測試得到的測試覆蓋率截圖,發表在博客中。要求總體覆蓋率到90%以上,否則單元測試部分視作無效。
第二階段目標:通過測試程序和API接口測試對於異常處理的支持
在上面我們只討論了正確的輸入下,我們對於程序輸出的期待。但如果程序的輸入出現了錯誤,比如命令行參數是“-10000”,或者是“1000000000000c”,你又該怎么辦呢?要怎樣才能告訴函數的調用者“你錯了”?又該如何方便地告訴函數的調用者“哪里錯了”?在這種時候,我們一般會定義各種異常(Exception),讓Core
在碰到各種異常情況的時候,能給調用者充分的錯誤信息。當然,我們同樣要進行增量修改:
項目要求:
- 設計好異常的種類與錯誤提示,例如讓程序支持“生成數獨數量”異常。
- 在
Core
模塊中實現拋出異常的功能,並撰寫測試用例:傳進去一個錯誤的數獨游戲,期望能捕獲這個異常。如果沒有,測試就報錯。 - 回歸測試所有以前的功能,保證以前的功能還能繼續工作。
- 在完成這一階段的任務之后,使用
git tag step2
標記第二階段已經完成,並在Push到Github上時使用--tags
參數把tag也推送到Github。
博客要求:
- 在博客中詳細介紹對哪些異常進行了處理以及每種異常的設計目標。
- 每種異常都要選擇一個單元測試樣例發布在博客中,並指明錯誤對應的場景。
第三階段目標:從計算模塊到可用軟件
在個人項目階段已經有同學做了一些不錯的GUI,但大部分同學只是增加了一個界面而已,並不能稱得上是開發了一個軟件。一個軟件不僅需要有負責數據計算的Core
模塊,用戶體驗友好的UI模塊,還要有足夠的健壯性、詳細的運行說明等。
首先要開發一個用戶體驗友好的UI模塊,並把計算核心與用戶界面完美地對接起來。
項目要求:
- 新建一個工程,把
Core
核心模塊作為一個DLL(動態鏈接庫)引用在新工程中。 - 開發一個UI模塊,實現如下需求:
- 隨機生成數獨游戲的功能,將會從用戶體驗角度對隨機性進行測試。
- 每盤游戲用戶都可以選擇 容易/中等/難 三個等級,將會從用戶體驗角度對不同難度的游戲進行測試。
- 用戶不會的時候可以在某個空格上點擊‘提示’,程序會提示該空格處需要填什么數字。
- 計時功能,能夠記錄用戶解數獨棋盤的耗時,並保持用戶的最佳記錄。
- 將UI模塊與
Core
模塊對接。 - 在完成這一階段的任務之后,使用
git tag step3
標記第三階段已經完成,並在Push到Github上時使用--tags
參數把tag也推送到Github。
博客要求:詳細地描述UI模塊的設計與兩個模塊的對接,並在博客中截圖實現的功能。
第四階段目標:界面模塊,測試模塊和核心模塊的松耦合【附加題】
既然各組同學都寫了高質量的各個模塊,而且模塊之間的關系是明確定義的,一致的,那么,小組A的測試模塊就可以測試小組B的核心模塊;小組C的用戶界面模塊就可以和小組B的核心模塊結合起來,正常運行。對吧?
那么現在,請你(假設為A)尋找另外一個小組(假設為B),與他們交換核心模塊與界面模塊,並測試一下下面的情況:
- A的核心模塊,加上B的測試模塊和用戶界面模塊
- B的核心模塊,加上A的測試模塊和用戶界面模塊
項目要求:
根據與合作小組對接過程中出現的問題,尋找並改進模塊中的bug。這部分修改需要另開一個新的分支dev-combine
,並Push到Github上。
博客要求:
在博客中指明合作小組兩位同學的學號,分析兩組不同的模塊合並之后出現的問題,為何會出現這樣的問題,以及是如何根據反饋改進自己模塊的。
第五階段目標:通過增量修改的方式,改進程序,發布一個真正的軟件【附加題】
在完成第四階段的目標后,可以通過你們小組的界面模塊和合作小組的計算模塊組合成一個帶界面的數獨游戲。但它還不是一個完整的軟件,你要為數獨游戲增加必要的說明與引導步驟,比如怎么玩,如果卡住了怎么辦。
項目要求:
把相關代碼簽入Github一個新的分支dev-product
。
博客要求:
把這個軟件發布出來,在博客中發布下載地址。收集至少10位用戶的反饋,並說明你在收到反饋后是怎樣改進自己的產品的。
2.評分規則
博文部分得分點
博客共五十分
1)在文章開頭給出Github項目地址。(1')
2)在開始實現程序之前,在下述PSP表格記錄下你估計將在程序的各個模塊的開發上耗費的時間。(0.5')
3)看教科書和其它資料中關於Information Hiding, Interface Design, Loose Coupling的章節,說明你們在結對編程中是如何利用這些方法對接口進行設計的。(5')
4)計算模塊接口的設計與實現過程。設計包括代碼如何組織,比如會有幾個類,幾個函數,他們之間關系如何,關鍵函數是否需要畫出流程圖?說明你的算法的關鍵(不必列出源代碼),以及獨到之處。(7')
5)閱讀有關UML的內容:https://en.wikipedia.org/wiki/Unified_Modeling_Language。畫出UML圖顯示計算模塊部分各個實體之間的關系(畫一個圖即可)。(2’)
6)計算模塊接口部分的性能改進。記錄在改進計算模塊性能上所花費的時間,描述你改進的思路,並展示一張性能分析圖(由VS 2015/2017的性能分析工具自動生成),並展示你程序中消耗最大的函數。(3')
7)看Design by Contract, Code Contract的內容:
http://en.wikipedia.org/wiki/Design_by_contract
http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx
描述這些做法的優缺點, 說明你是如何把它們融入結對作業中的。(5')
8)計算模塊部分單元測試展示。展示出項目部分單元測試代碼,並說明測試的函數,構造測試數據的思路。並將單元測試得到的測試覆蓋率截圖,發表在博客中。要求總體覆蓋率到90%以上,否則單元測試部分視作無效。(6')
9)計算模塊部分異常處理說明。在博客中詳細介紹每種異常的設計目標。每種異常都要選擇一個單元測試樣例發布在博客中,並指明錯誤對應的場景。(5')
10)界面模塊的詳細設計過程。在博客中詳細介紹界面模塊是如何設計的,並寫一些必要的代碼說明解釋實現過程。(5')
11)界面模塊與計算模塊的對接。詳細地描述UI模塊的設計與兩個模塊的對接,並在博客中截圖實現的功能。(4')
12)描述結對的過程,提供非擺拍的兩人在討論的結對照片。(1')
13)看教科書和其它參考書,網站中關於結對編程的章節,例如:
http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html
說明結對編程的優點和缺點。
結對的每一個人的優點和缺點在哪里 (要列出至少三個優點和一個缺點)。(5')
14)在你實現完程序之后,在附錄提供的PSP表格記錄下你在程序的各個模塊上實際花費的時間。(0.5')
注:結對小組中兩個人發布獨立博客,其中2)、3)、5)、7)、13)、14)部分請獨立完成,不允許雷同。項目的測試分數兩人共享,博客的分數各自獨立。附加題的相關要求請按附加題的要求補充在博客中。
程序部分得分點
程序共七十分
源代碼管理評分(5'):
該評分主要通過源代碼管理中的commit注釋信息,增量修改的內容,是否有運行說明,每個階段是否打上了標簽等內容給分。(5')
第一階段(20'):
該評分將進行這-c -s -n -m -u -r
六個參數的正確性測試
第二階段(20'):
將針對上述六個參數進行魯棒性測試,可能測試的內容包括且不限於:
錯誤的命令、錯誤的參數、大小寫、錯誤的參數組合、錯誤的文件格式等。
要求必須正常結束,崩潰不得分。
錯誤無任何提示,不得分。
錯誤種類較多,提示合理,得正分。
第三階段(25'):
1)隨機生成數獨游戲的功能,將會從用戶體驗角度對隨機性進行測試。(6')
2)每盤游戲用戶都可以選擇 容易/中等/難 三個等級,將會從用戶體驗角度對不同難度的游戲進行測試。 (7')
3)用戶不會的時候可以在某個空格上點擊‘提示’,程序會提示該空格處需要填什么數字。(6')
4)計時功能,能夠記錄用戶解數獨棋盤的耗時,並保持用戶的最佳記錄。(6')
附加題得分點
附加題1(10'):
在結對項目博客中按照階段四的博客要求添加相應內容(5')
最終的對接效果(5')
附加題2(10'):
在結對項目博客中按照階段五的博客要求添加相應內容(5')
可玩性(5')
3.測試須知
組織目錄
與個人項目類似,在結對項目中我們也會對大家的計算核心進行正確性的自行測試,需要大家遵循一定的規范。所有提交到Github上的項目均需要建立一個名字為BIN的文件夾,里面必須含有計算核心生成的可執行文件與相關的依賴庫,請注意以下三點:
- 確保VS產生的臨時文件和編譯生成臨時文件不被加入到Git代碼倉庫中(使用.gitignore文件管理哪些文件可以被忽略)。
- 確保命令行測試程序的名字為sudoku.exe,確保核心模塊的DLL名字為Core.dll。
- 確保生成的棋盤文件sudoku.txt與可執行文件在同一目錄下,生成文件時請使用相對路徑!
一個示例組織目錄如下所示:
/ SudokuProject(工程名字自行指定即可)
/ main.cpp
/ generator.cpp
/ BIN
/ Core.dll(Core模塊)
/ Lib.dll(運行需要的[其他]動態鏈接庫文件)
/ sudoku.exe
/ sudoku.txt (運行exe后生成)
參數約定
助教在測試時,將以命令行運行可執行文件的方式進行批量測試,參數及其約定如下:
參數名字 | 參數意義 | 范圍限制 | 用法示例 |
---|---|---|---|
-c | 需要的數獨終盤數量 | 1-1000000 | 示例:sudoku.exe -c 20 [表示生成20個數獨終盤] |
-s | 需要解的數獨棋盤文件路徑 | 絕對或相對路徑 | 示例: sudoku.exe -s game.txt [表示從game.txt讀取若干個數獨游戲,並給出其解答,生成到sudoku.txt中] |
-n | 需要的游戲數量 | 1-10000 | 示例:sudoku.exe -n 1000 [表示生成1000個數獨游戲] |
-m | 生成游戲的難度 | 1-3 | 示例:sudoku.exe -n 1000 -m 1 [表示生成1000個簡單數獨游戲,只有m和n一起使用才認為參數無誤,否則請報錯] |
-r | 生成游戲中挖空的數量范圍 | 20-55 | 示例:sudoku.exe -n 20 -r 20~55 [表示生成20個挖空數在20到55之間的數獨游戲,只有r和n一起使用才認為參數無誤,否則請報錯] |
-u | 生成游戲的解唯一 | 示例:sudoku.exe -n 20 -u [表示生成20個解唯一的數獨游戲,只有u和n一起使用才認為參數無誤,否則請報錯] |
[新]等價數獨
需要注意的是,在個人項目的測試中,我們把等價但不相同的數獨也算作了不重復的數獨。但因為本次結對項目我們更加注重數獨游戲的可玩性,所以在本次項目中,我們會屏蔽通過數字交換得到的等價數獨,這部分重復的數獨不計入最終結果。何為“通過數字交換得到的數獨”呢,請看下面的例子:
9 5 8 3 6 7 1 2 4
2 3 7 4 5 1 9 6 8
1 4 6 9 2 8 3 5 7
6 1 2 8 7 4 5 9 3
5 7 3 6 1 9 4 8 2
4 8 9 2 3 5 6 7 1
7 2 4 5 9 3 8 1 6
8 9 1 7 4 6 2 3 5
3 6 5 1 8 2 7 4 9
將1
和9
的位置調換,可以得到一個新的“有效數獨”。
1 5 8 3 6 7 9 2 4
2 3 7 4 5 9 1 6 8
9 4 6 1 2 8 3 5 7
6 9 2 8 7 4 5 1 3
5 7 3 6 9 1 4 8 2
4 8 1 2 3 5 6 7 9
7 2 4 5 1 3 8 9 6
8 1 9 7 4 6 2 3 5
3 6 5 9 8 2 7 4 1
那么每個測試點的最終得分計算公式為:
測試點正確性測試得分 × 不等價數獨組數 ÷ 實際需求個數個數
由於只有數獨終盤才存在“等價數獨”的情況,所以我們將利用開發者提供的“求解數獨”命令行接口求解開發者生成的數獨游戲,將求解后的終盤作為判斷不重復數據比例的依據。比如某個測試點sudoku.exe -n 100 -r 30~40
基准分為10分,通過開發者提供的“求解數獨”命令行求解該命令生成的數獨游戲后,發現求解出的數獨終盤一共有40組(等價的數獨都會歸類到同一組中),那么最終開發者在該測試點的得分為 40/100 * 10 = 4 分。
請注意,由於開發者的求解數獨接口直接關系到數獨游戲的評分,請確保sudoku.exe -s puzzle_file_path
的接口依舊有效!
[新]錯誤處理
我們都知道健壯性對於軟件來說是非常必要的,所以本次自動測試我們也會加入各種各樣出錯情況的測試。助教測試時將會選擇不同種類的出錯場景,要求開發者程序不會崩潰的情況下,能夠盡可能精確報錯(就像編譯器一樣)。你可以有“容錯性”的出錯設計,但必須輸出必要的提示或說明。
4. FAQ
有任何疑問請直接在本博客下留言,我們會盡快回復。
5. 附錄
附:PSP 2.1表格
PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
---|---|---|---|
Planning | 計划 | ||
· Estimate | · 估計這個任務需要多少時間 | ||
Development | 開發 | ||
· Analysis | · 需求分析 (包括學習新技術) | ||
· Design Spec | · 生成設計文檔 | ||
· Design Review | · 設計復審 (和同事審核設計文檔) | ||
· Coding Standard | · 代碼規范 (為目前的開發制定合適的規范) | ||
· Design | · 具體設計 | ||
· Coding | · 具體編碼 | ||
· Code Review | · 代碼復審 | ||
· Test | · 測試(自我測試,修改代碼,提交修改) | ||
Reporting | 報告 | ||
· Test Report | · 測試報告 | ||
· Size Measurement | · 計算工作量 | ||
· Postmortem & Process Improvement Plan | · 事后總結, 並提出過程改進計划 | ||
合計 |