目錄
Github項目位址
接口設計
計算子產品接口的設計與實作過程
UML:計算子產品部分各個實體之間的關系
計算子產品接口部分的性能改進
Design by Contract, Code Contract
計算子產品部分單元測試展示
計算子產品部分異常處理說明
界面子產品的詳細設計過程
界面子產品與計算子產品的對接
結對的過程
結對程式設計
預計花費時間&實際花費的時間
附加題:界面子產品,測試子產品和核心子產品的松耦合
傳回目錄
https://github.com/514DNA/sudoku
- 難度級别-m說明:
- 1:生成35個空的單解數獨;
- 2:生成40-50個空的單解數獨
- 3:生成50-60個空的單解數獨 理由:在國際數獨比賽中要求數獨題目均為單解的标準數獨,在個人體驗中,也能感覺到多解數獨會造成很大的困擾,是以統一生成單解數獨。數獨的難度很大程度上依賴空的個數有關,是以把空的個數作為數獨難度的判别标準。
方法:Information Hiding, Interface Design, Loose Coupling
生成數獨終局
void generate(int number, int[][] result)
生成設定難度級别的數獨遊戲
void generate(int number, int mode, int[][] result)
生成設定挖空數量的數獨遊戲
void generate(int number, int lower, int upper, bool unique, int[][] result)
解數獨題
bool solve(int[] puzzle, int[] solution)
資料成員和方法分别有public類型和private類型,僅将外部需要調用的資料和方法公開,規範好傳入的參數的格式,傳回符合要求的結果。函數的實作過程,内部調用的函數不對使用者公開
代碼的組織: 類、函數及其關系
代碼一共有兩個類:數獨類和指令行參數處理類
數獨類主要有如下函數:
class Core
{
public
void set_play(bool a);//設定為遊戲模式
void create_sudoku_puzzle(int n, int mode);
void create_sudoku_puzzles(int block_num, int mode, int n);
void create_random_sudoku();
void solve_all_soduku(FILE *fp);
void generate(int number, int** result);
void generate(int number, int mode, int** result);
void generate(int number, int lower, int upper, bool unique, int** result);
bool solve(int* puzzle, int* solution);
private:
void init_sudoku();//初始化數獨
int init_check_puzzle();//挖空後是否單解
int can_delete(int addr);//能否挖空
};
指令行處理類有如下函數:
class arg_info
{
public:
int read_arg_info(int argc, char **argv); //讀取指令函參數
void run_cmd(int argc, char **argv);
private:
int str2num(char \*str);
int str2range_num(char *str);
void error_out(int error);//錯誤輸出
void set_arg_bit_on(int mode);//模式設定
};
關鍵函數流程圖
主要說一下生成數獨遊戲的函數之間的關系
生成數獨遊戲首先要調用create_sudoku_puzzles這個函數,然後根據生成題目的數量調用n次create_sudoku_puzzle函數。
這個函數首先調用建立數獨的函數,如果是指令行中調用,那麼就調用create_test_sudoku,這個函數不會生成重複等價數獨;如果是GUI調用(給玩家玩的),那麼就調用create_random_sudoku,這個函數保證了随機性。
生成完數獨就開始挖空,這些是挖空用的函數:
int can_delete(int addr);
int can_delete_senior(int addr);
調用can_delete用來判斷通過低級方法可以挖掉的格子;調用can_delete_senior用來判斷通過進階方法可以挖掉的格子;這三個函數是來回遞歸判斷數獨是否是唯一解的函數,由can_delete_senior調用:
int to_next(int i, int j, int n);
int init_check_puzzle();
int check_puzzle(int i, int j, int n);
這個函數是把原來嘗試挖掉的空加上的函數,也由can_delete_senior調用
void add_addr(int addr, int num);
找到可以挖的空之後調用一次clear_addr把空挖掉,都挖完調用一次print_sudoku輸出

算法的關鍵及獨到之處
分階段産生數獨遊戲采用兩種方法對生成的數獨終局挖空:低級方法——四種排除法直接把一個數推出來;進階方法——猜數并驗證。
如果某一個數挖完之後能被簡單方法再推出來,那麼就把這個格子放在緩存區内;找完目前所有的數,那麼就随機挖掉一個緩存區裡面的空。但是這樣不容易挖夠55個空,那麼隻能通過猜數來挖空。如果一個數挖完之後還是單解數獨,那麼就挖掉。通過暴力回溯法判斷數獨解是否唯一;從數獨格子中的最後一個空,往前尋找。如果挖完這個格子還是單解數獨,那麼就挖掉,然後繼續往前找,直到挖夠55個空就停止挖空。
這個挖空方法的獨到之處就是會先找到所有低級挖空方法可以挖的空,這樣就減少了暴力回溯的次數
花費的時間:2小時左右
改進的思路
程式中主要費時間的地方是判斷挖完空之後的數獨還是單解數獨。我們解數獨有以下幾種方法:1.四種排除法直接把一個數推出來(簡單方法);2.猜數(複雜方法)
挖空過程中,如果某一個數挖完之後能被簡單方法再推出來,那麼就把這個格子放在緩存區内。找完目前所有的數,那麼就随機挖掉一個緩存區裡面的空。但是這樣不容易挖夠55個空,那麼隻能通過猜數來挖空,猜數是這樣。如果一個數挖完之後還是單解數獨,那麼就挖掉。通過暴力回溯法判斷數獨解是否唯一,就造成了運作的緩慢
最開始是把所有挖過之後解唯一的數找到再随機一個,但是後來發現這樣做,判斷解唯一性的次數就多,要追求效率,就要減少這方面花的時間,也就是減少判斷的次數。是以從前往後找,找到第一個可以挖的格子就挖掉,然後再繼續找,直到挖的空夠55個。
但是這樣改了之後更慢了,這種回溯法對前面空多後面空少的數獨很棘手,會判斷的非常慢。是以就改成從後往前找,從後往前挖,速度提高。這就是執行世界最難指令:n 10000 -r 55~55 -u所用的時間,雖然挖空的函數還是用了很多時間,但是整體已經有很大改進了
性能分析圖
-c 1000000
-n 10000 -r 55~55 -u
消耗最大的函數
生成數獨時,消耗最大的函數是create_sudoku;生成數獨遊戲時,消耗最大的函數是create_sudoku_puzzle
- 基本思想:函數的調用者保證傳入參數的函數符合函數的要求,如果不複合函數的要求,函數将拒絕執行,寫函數時應該對傳入函數的參數進行檢查
- 優點
- 傳統的server/client模式下,對被調用者要求嚴格,導緻了調用者的品質低劣,契約式程式設計中,調用者和被調用者地位平等,保證了雙方的代碼品質,提高軟體工程的效率和品質
- 在多人合作程式設計中,使用契約式程式設計,有利于團隊程式設計接口的統一性,更容易劃分子產品,便于子產品之間的對接
- 缺點
- 契約式程式設計需要一定的機制驗證契約成立與否,對程式語言有一定的要求
- 契約式程式設計沒有被标準化,項目之間的定義和修改的不同,可能會給代碼帶來混亂
- 融入作業
- 按照要求設計和實作generate和solve等接口,傳入符合要求的參數執行函數可以獲得正确的結果。事先對傳入的result,number等參數進行參數檢查,如果不符合要求将會進行異常處理,不會進行下一步的操作,保證運作結果的正确性。
計算子產品部分單元測試展示
單元測試代碼 | 測試的函數 | 測試資料構造思路 |
---|---|---|
| create_sudoku_puzzle(int n) |
|
| generate() |
|
| solve() |
|
通過的單元測試
單元測試覆寫率
有些用來更友善看到結果的輸出到控制台的函數沒有被調用,是以不能達到100%
類别 | 設計目标 | 單元測試樣例 | 應用場景 |
---|---|---|---|
指令行參數(該部分使用指令行測試) | 處理非法的參數 | | 輸入未定義的參數 |
處理超出範圍或格式不正确的數字 | | n,c,r,m後面的參數不正确 | |
處理個數錯誤的參數 | | 參數重複出現,參數後面沒有數字 | |
處理錯誤的參數組合 | | 參數組合錯誤,即不屬于定義的6種組合 | |
讀入的數獨題 | 處理不存在的檔案路徑 | sudoku.exe -s "non.txt" | 檔案不存在 |
處理格式不正确的檔案 | sudoku.exe -s "non.exe" | 處理檔案格式不正确的情況 | |
處理非法的數獨題 | | 處理數獨題内容不正确的情況 | |
處理無解的數獨題 | | 處理輸入的數獨題無解的情況 |
界面子產品的設計
按照需求将頁面劃分為四個部分:起始狀态的難度選擇子產品,數獨題子產品,功能按鈕子產品,時間、記錄展示子產品,界面中的各種控件使用.ui檔案生成,控件的切換由transGUI控制,信号由coreConnect控制。
功能比較簡單,主要有提示、暫停,可以在開始時進行難度選擇,也可以在遊戲過程中點選傳回按鈕重新選擇難度,或者詢問後退出。遊戲界面顯示目前用時和本難度最高紀錄。點選送出回報是否正确,并詢問是有繼續遊戲。
代碼說明&實作過程
編輯UI檔案,自動生成空間的代碼
class Ui_sudokuGUIClass
{
public:
QWidget *centralWidget;
QWidget *verticalLayoutWidget_2;
QVBoxLayout *panelLayout;
//...資料成員
void setupUi(QMainWindow *sudokuGUIClass); // setupUi
void retranslateUi(QMainWindow *sudokuGUIClass); // retranslateUi
};
在transGUI中對各控件的狀态進行設定,實作頁面的轉換,與計算子產品的對接也設定在這一部分
class transGUI : public QObject{
Q_OBJECT
public:
QTimer *timer;
QLineEdit *sudokuLineEdit[9][9];
transGUI(Ui_sudokuGUIClass UI);
void writeRecord();
signals:
public slots:
void play();
void updateTime();
void stop();
void goOn();
void option();
void quit();
void quitCancel();
void inform();
void submit();
private:
Ui_sudokuGUIClass ui;
Core core; //計算子產品
QTime recTime;
QLabel *sudokuLabel[9][9];
int **answer, **puzzle;
int mode = 0; //難度級别
FILE *fp; //記錄檔案
QTime recTimes[3]; //最高記錄
void readRecord();
};
在coreConnect中實作信号函數和槽函數的連結。
class coreConnect :public QObject{
Q_OBJECT
public:
coreConnect(Ui_sudokuGUIClass UI, transGUI *Trans);
private:
Ui_sudokuGUIClass ui;
transGUI *trans;
QTimer *timer;
void startConnect();
void timeConnect();
void resetTimeConnect();
void backConnect();
void quitConnect();
void quitCancelConnect();
void informConnect();
void submitConnect();
void setTimer();
void againConnect();
signals:
private slots:
void reSetTimer();
};
設計
需要用到計算子產品的部分有擷取數獨題、提示、結果驗證的部分。為了更好的使用者體驗,産生的數獨都是标準數獨,是以在生成數獨時已經有了唯一解,在擷取數獨題将數獨題存入puzzle數組的同時也将數獨的答案存儲進answer數組,友善提示和送出時使用。
對接
遊戲中難度的選擇對應-m參數,是以點選遊戲開始時,選擇的難度級别作為參數m,調用generate(int number, int mode, int** result)函數,因為一次隻産生一個數獨遊戲,是以number設為1,在計算子產品中設定play參數,分别将數獨終局和數獨遊戲存入result。
實作的功能
提示:使用者點選空格後點選提示按鈕,在右側提示資訊處會出現此處應填的數字
暫停:點選暫停按鈕,停止計時
傳回:退回難度選擇頁面,重新選擇難度級别
退出:點選退出按鈕,彈出對話框詢問是否确認退出
送出:進行結果驗證,告知使用者答案是否正确,詢問繼續遊戲還是退出
在周二課上決定結對。
最開始在放假前完成了代碼複審的部分,發現兩個人思路思維方式非常不一樣,對方對性能有很高的追求,思維方式比較獨特,有很多神奇的腦洞;我思路比較窄,也比較循規蹈矩。兩個人最大的共同點是個人項目都沒有寫注釋,也都沒有寫GUI。。。
國慶節期間兩人大部分時間都不在學校,基本沒有任何結對開展工作,雙方獨立進行,對方進行了算法設計,我學習了GUI和生成dll的方法。非常不好的一點是沒有一起對項目進行規劃和設計,基本順其自然。
假期快結束時危機感上升,開始進入一有時間就一起寫代碼的狀态,以對方的代碼為基礎,按照對方設計的算法兩人共同完成了核心部分。接下來對方主要進行性能優化、異常處理,我完成了參數處理,對代碼進行了測試和單元測試,完成了GUI。
結對照片
項目 | ||
---|---|---|
|
| |
對方 |
|
|
自己 |
|
|
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | 60 | 30 |
| | ||
Development | 開發 | 1380 | 1590 |
| | 300 | 120 |
| | 180 | |
| | ||
| | ||
| | 240 | |
| | 1800 | 2400 |
| | ||
| | ||
Reporting | 報告 | ||
| | ||
| | ||
| | ||
Sum | 合計 | 3240 | 3480 |
合作小組
劉暢 15061183
王辰昱 15231177
出現的問題
如果不改動GUI代碼:合作小組使用我們的Core.dll每次生成的puzzle都是相同的;我們使用合作小組的Core.dll無法獲得puzzle。
用我們的測試子產品測試合作小組的solve(int[] puzzle, int[] solution)時,如果輸入的puzzle不合法(如同一行中有兩個同樣的數字),沒有傳回false
原因
合作小組對非法數組進行了異常處理,會抛出一個InvalidPuzzleException,但是沒有把Exception包含在Core.dll中,雙方異常處理的位置和方式不同
我們小組的GUI和Core之間存在标準接口以外的互動,在Core中自定義了很多東西,導緻合作小組無法正常使用。根本原因是我們小組在做GUI的部分時對Core的使用不規範,沒有完全按照定義的接口使用而是設定了其他變量,是以原有的GUI的代碼也無法使用其他小組的Core.dll。
改進
對GUI界面部分的代碼進行修改,調用Core的标準接口。
另外我們的異常處理都在計算子產品外部進行,即預設傳入Core的接口的參數合法,應該對參數進行檢查,使它有一定的異常處理能力。
另外應該向合作小組學習,他們的Core子產品非常簡潔,包裝非常好除了必要的接口沒有多餘的東西,可以子產品劃分應該也做得非常好,我們不應該把計算子產品、輸入輸出都混雜在Core裡面。
總結
- 沒有對項目進行充分的設計和時間規劃,在需求分析和設計上花費的時間明顯不足,導緻對進度的把握很不好。
- 從對方身上可以看到一些獨特的思維方式以及非常執著的精神品質,非常的值得學習。
- 結對雙方沒有争論但是對課程的了解、關注的重點不一緻。