天天看點

結對項目-數獨程式擴充

**GitHub位址:

Step1~3: https://github.com/Aria-K-Alethia/SE-Sudoku-Pair

Step4: https://github.com/Aria-K-Alethia/SE-Sudoku-Pair/tree/dev-combine

Step5: https://github.com/Aria-K-Alethia/SE-Sudoku-Pair/tree/dev-product (使用者體驗測試請使用此版本)

**

接口設計說明

我們将求解與生成的功能封裝為dll檔案,隻給出頭檔案供使用者參考,而不提供具體實作細節。這樣的封裝有效避免了使用者誤操作導緻的功能性問題,減輕使用者負擔。

計算子產品接口的設計與實作過程

我們主要的算法邏輯都集中在

Sudoku

這個類當中。

  • 數獨求解的部分我們使用回溯的思想進行解決。回溯方法

    traceBackSolve()

    對第

    i

    行第

    j

    列元素及其後方(先向右,到最右則折返換行)空格進行求解,每次求解嘗試從1到9,檢測1到9每個數字是否适合在此格子中填入(行、列、宮不重複),并在嘗試中遞歸調用

    traceBackSolve()

    方法,進而驗證每次嘗試的正确性。求解數獨的接口

    solve()

    方法負責調用

    traceBackSolve()

    方法進行求解,并做一二維數組的轉換。
  • 在生成數獨接口

    generate(int number, int lower, int upper, bool unique, int result[][])

    中,我們采用先生成終盤,再從終盤中挖空的形式進行數獨生成。首先調用

    generateCompleteN()

    這個已經實作的生成終盤方法,得到

    number

    個終盤,再使用

    digHoles()

    方法進行挖空。挖空政策一共有兩種,一種為從頭數獨第一個數開始,一種為随機選擇。随機挖空由于速度較快,但容易出現挖出來的盤有多解的情況,我們隻在unique為假的情況下使用它。unique為真時,采用順序挖空的政策,以從左到右,從上到下的順序進行挖空,每次挖空之後,将原始數字用1到9中其他數字進行替換,并調用

    solve()

    對數獨進行求解,若能解出,則證明此空不能挖,否則可挖,繼續向後挖空。
  • 第二個生成數獨接口二

    generate(int number, int mode, int result[][LEN*LEN])

    中,我們利用了第一個

    generate()

    方法,根據

    mode

    得到相應的

    up

    down

    傳入

    generate()

    ,便可得到結果。

UML圖

結對項目-數獨程式擴充

計算子產品接口部分的性能改進

參數-c

下圖展示了生成1000000個完整數獨的性能分析

結對項目-數獨程式擴充

由于這次繼承了我上次的代碼,是以代碼本身已經被優化過。

5.272秒,幾乎所有的時間都花費在回溯遞歸上,速度已經可以接受。

一個可能的優化是在判斷重複的時候使用位操作。

參數-s

下圖展示了解1000個數獨時候的性能分析:

結對項目-數獨程式擴充

首先注意到checksolve花費較長時間,這個函數原來使用了3×9的時間來判斷,注意到這個方法的下界是1×9,遂更改了實作方式:

int row, col;
    row = getBlock(i);
    col = getBlock(j);
    for (int a = 1; a <= LEN; ++a) {
        if ((board[i][a] == k + '0') || (board[a][j] == k + '0') 
                || (board[row + ((a - 1) / 3)][col + ((a - 1) % 3)] == k + '0'))
            return false;
    }	
           

不過,這是常數級别的優化,是以效果很差,改進之後再次性能分析發現效果微弱。

一個可能的改進是使用bitmap來優化。

參數-n

直接在-u模式下測試,由于當r的參數的值變大的時候生成10000個解的時間幾乎不可接受,是以選擇較低的數值,下圖是指令-n 10000 -r 25~55的效能分析:

結對項目-數獨程式擴充

24秒

熱路徑主要集中于solve函數,判斷原因還是由于遞歸時造成的指數級增長的函數調用,在不更改現有結構的情況下已經很難改進。

改進效能花費了30分鐘。

契約式程式設計的優缺點

  • 優點:

    使用者和被調用者地位平等,雙方必須彼此履行義務,才可以行駛權利。調用者必須提供正确的參數,被調用者必須保證正确的結果和調用者要求的不變性。雙方都有必須履行的義務,也有使用的權利,這樣就保證了雙方代碼的品質,提高了軟體工程的效率和品質。

  • 缺點:

    對于程式語言有一定的要求,契約式程式設計需要一種機制來驗證契約的成立與否;契約式程式設計并未被标準化,是以項目之間的定義和修改各不一樣,給代碼造成很大混亂。

計算子產品部分單元測試展示

solve()方法

測試思路:給出一個題目,和答案對比。

ret = sudoku.solve(puzzle, temp);
Assert::AreEqual(ret, true);
for (int i = 0; i < 81; ++i) {
	Assert::AreEqual(temp[i], solution[i]);
}
           

generate()方法

測試思路:對-r指令,首先在生成之後用solve函數測試是否可解,然後計算遊戲中的空的個數,判斷是否滿足要求;對-u指令,在-r的基礎之上用回溯法求出解的個數,如果個數大于1,則出錯,測試-m的時候也是類似的方式。

下面是測試-n 10 -r lower~upper -u 的部分代碼:

sudoku.generate(10, lower, upper, true, result);
for (int i = 0; i < number; ++i) {
	Assert::AreEqual(sudoku.solve(result[i], solution), true);
	int solutionNumber = sudoku.countSolutionNumber(result[i], 2);
	Assert::AreEqual(solutionNumber, 1);
	int count = 0;
	for (int j = 0; j < 81; ++j) {
		if (result[i][j] == 0) count++;
	}
	Assert::AreEqual(count <= upper && count >= lower, true);
}

           

測試異常

測試思路:設定一個bool型變量exceptionThrown(初始值為false)以及異常的條件,隻要catch到異常,就将exceptionThrown設定為true,然後進行斷言。

下面是測試SudokuCountException的代碼:

bool exceptionThrown = false;
try { // Test first SudokuCountException
	sudoku.generate(-1, 1, result);
}
catch (SudokuCountException& e) {
	exceptionThrown = true;
	e.what();
}
Assert::IsTrue(exceptionThrown);
           

這裡generate方法生成的數獨個數不能是負數,是以會抛出異常。

測試輸入參數的分析

測試思路:用strcpy_s初始化argv,設定argc,然後進行調用相關方法進行分析和斷言。

下面是測試指令-n 1000 -m 2的代碼:

InputHandler* input;
strcpy_s(argv[3], length, "-n");
strcpy_s(argv[4], length, "1000");
strcpy_s(argv[1], length, "-m");
strcpy_s(argv[2], length, "2");
argc = 5;
input = new InputHandler(argc, argv);
input->analyze();
Assert::AreEqual(input->getMode(), 'n');
Assert::AreEqual(input->getNumber(), 1000);
Assert::AreEqual(input->getHardness(), 2);
delete input;
           

這裡打亂了參數的順序,其他參數的組合也是用類似的方法來測試的。

參數解析魯棒性測試

我們的program中,參數錯誤的情況下會直接報錯然後退出,同時輸入分析在完成之後一般不會改變,是以我們直接在控制台中進行了測試,主要看是否有相應的輸出,錯誤種類參看下圖:

Error Code 異常說明 錯誤提示
1 參數數量不正确 bad number of parameters.
2 參數模式錯誤 bad instruction.expect -c or -s or -n
3 -c指令的數字範圍錯誤 bad number of instruction -c
4 -s指令找不到檔案 bad file name
5 -s指令的puzzle.txt中的數獨格式錯誤 bad file format
6 -s指令的puzzle.txt中的數獨不可解 bad file can not solve the sudoku
9 -r指令後的數字範圍有錯誤 the range of -r must in [20,55]
10 -m指令後的模式有錯誤 the range of -m must be 1,2 or 3
11 11 -m指令與-u或-r指令同時出現 -u or -r can not be used with -m
12 c指令的參數範圍錯誤 the number of -c must in [1,1000000]
13 -n指令的參數範圍錯誤 the number of -n must in [1,10000]
14 -n指令的參數類型錯誤 the parameter of -n must be a integer
18 -n不能單獨使用 parameter -n cann't be used without other parameters

其中code不連續是因為有的code替換成了exception。

一些測試情景可以參考下圖:

結對項目-數獨程式擴充

單元測試覆寫率分析

結對項目-數獨程式擴充

總的覆寫率約為94%

沒有測到的代碼主要是Output相關的代碼,已經在7.5節進行了說明。

異常處理說明

  • SudokuCountException

    :處理兩個

    generate()

    方法的參數

    number

    超出1~10000範圍的異常

    單元測試:

    int result[1][81];
    bool exceptionThrown = false;
    try { // Test first SudokuCountException
    	sudoku.generate(-1, 1, result);
    }
    catch (SudokuCountException& e) {
    	exceptionThrown = true;
    	e.what();
    }
    Assert::IsTrue(exceptionThrown);
               
  • LowerUpperException

    :處理

    generate()

    方法參數

    lower

    upper

    不合法情況:lower > upper;lower < 20;upper > 55
    //test LowerUpperException,case 1
    		exceptionThrown = false;
    		try {
    			sudoku.generate(1, 1, 50, true, result);
    		}
    		catch (LowerUpperException& e) {
    			exceptionThrown = true;
    			e.what();
    		}
    		Assert::IsTrue(exceptionThrown);
    		//test LowerUpperException,case 2
    		exceptionThrown = false;
    		try {
    			sudoku.generate(1, 20, 56, true, result);
    		}
    		catch (LowerUpperException& e) {
    			exceptionThrown = true;
    			e.what();
    		}
    		Assert::IsTrue(exceptionThrown);
    		//test LowerUpperException,case 3
    		exceptionThrown = false;
    		try {
    			sudoku.generate(1, 50, 1, true, result);
    		}
    		catch (LowerUpperException& e) {
    			exceptionThrown = true;
    			e.what();
    		}
    		Assert::IsTrue(exceptionThrown);
               
  • ModeRangeException

    generate()

    方法模式參數超過[1,3]區間範圍
    //test ModeRangeException
    exceptionThrown = false;
    
    try {
    	sudoku.generate(1, -1, result);
    }
    catch (ModeRangeException& e) {
    	exceptionThrown = true;
    	e.what();
    }
    Assert::IsTrue(exceptionThrown);
               

界面詳細設計

風格:

  • 界面風格采用QSS檔案統一修改。QSS代碼改自csdn部落格作者一去、二三裡的黑色炫酷風格。

    基本風格見下圖

![](http://images2017.cnblogs.com/blog/1238514/201710/1238514-20171015144300230-107509760.png)

  • Hint按鈕風格:
QPushButton#blueButton {
        color: white;
}
QPushButton#blueButton:enabled {
        background: rgb(0, 165, 235);
        color: white;
}
QPushButton#blueButton:!enabled {
        background: gray;
        color: rgb(200, 200, 200);
}
QPushButton#blueButton:enabled:hover {
        background: rgb(0, 180, 255);
}
QPushButton#blueButton:enabled:pressed {
        background: rgb(0, 140, 215);
}
           
  • 數獨棋盤單元格風格(普通格、角落格、宮邊緣格):
QPushButton#puzzleButton {
	border-width: 1px;
	border-style: solid;
	border-radius: 0;
}
QPushButton#puzzleButtonTLCorner {
    	border-radius: 0;
	border-top-left-radius: 4px;
	border-width: 1px;
    	border-style: solid;
}
QPushButton#puzzleButtonTRCorner {
	border-radius: 0;
	border-top-right-radius: 4px;
    	border-width: 1px;
    	border-style: solid;
}
QPushButton#puzzleButtonBLCorner {
	border-radius: 0;
	border-bottom-left-radius: 4px;
    	border-width: 1px;
    	border-style: solid;
}
QPushButton#puzzleButtonBRCorner {
	border-radius: 0;
	border-bottom-right-radius: 4px;
    	border-width: 1px;
    	border-style: solid;
}
QPushButton#puzzleButtonRE {
	border-radius: 0;
	border-width: 1px;
	border-right-width: 3px;
    	border-style: solid;
}
QPushButton#puzzleButtonBE {
	border-radius: 0;
    	border-width: 1px;
	border-bottom-width: 3px;
    	border-style: solid;
}
QPushButton#puzzleButtonBRE {
	border-radius: 0;
    	border-width: 1px;
        border-right-width:3px;
	border-bottom-width: 3px;
	border-style: solid;
}
           

小結:界面風格不是我們在設計UI時最早考慮的部分,本來打算風格隻進行簡單修改,隻用setStyleSheet()方法來設計界面風格。不過後來發現自帶的界面實在太醜,于是決定借鑒已有的風格,針對項目要求進行調整,最終效果還算不錯。

布局

  • 布局設計采用純代碼的設計,使用Layout進行對齊。
  • 歡迎、幫助與選擇難度界面統一使用QVBoxLayout對控件進行對齊

    效果見下圖

![](http://images2017.cnblogs.com/blog/1238514/201710/1238514-20171015145002246-15250941.png)

![](http://images2017.cnblogs.com/blog/1238514/201710/1238514-20171015145043574-1735130071.png)

![](http://images2017.cnblogs.com/blog/1238514/201710/1238514-20171015145100605-1678680702.png)

  • 遊戲界面采用Layout嵌套Layout的形式進行布局管理。我們先設計了一個mainLayout作為最外層Layout,将其他Layout豎直放入mainLayout。

    其他Layout見下圖

![](http://images2017.cnblogs.com/blog/1238514/201710/1238514-20171015145113199-1093191476.png)

  • 為保持數獨棋盤排列的緊密,在棋盤周圍加了spacer把棋盤上的格子擠壓到一起,且能保持形狀。
  • 為保證比例的美觀,遊戲窗體被強制固定,無法進行縮小與放大。

小結: 設計布局過程有些小曲折,一開始由于沒有經驗,不知道該如何用代碼該出想要的布局效果,也想過不使用代碼修改布局,直接在界面上拖拽。但考慮到代碼的靈活性,還是決定使用代碼,放棄了拖拽設計(下次有機會做UI,希望嘗試下拖拽設計和代碼設計結合的形式)。好在有部落格和Qt官方文檔的支援,還是成功學會了Qt的布局設計,做出了目前這個效果。

界面子產品與計算子產品的對接

主要在開始新遊戲的時候使用,首先用generate中生成數獨遊戲,然後再轉換成QString顯示在界面的button上,部分代碼如下:

int result[10][LEN*LEN];
	sudoku->generate(10, degOfDifficulty, result);
	QString temp;
	QString vac("");
	for (int i = 0; i < LEN; ++i) {
		for (int j = 0; j < LEN; ++j) {
			if (result[target][i*LEN + j] == 0) {
				tableClickable[i][j] = true;
				puzzleButtons[i][j]->setText(vac);
				puzzleButtons[i][j]->setEnabled(true);
				puzzleButtons[i][j]->setCheckable(true); // Able to be checked
			}
			else {
				tableClickable[i][j] = false;
				puzzleButtons[i][j]->setText(temp.setNum(result[target][i*LEN + j]));
				puzzleButtons[i][j]->setEnabled(false); // Unable to be editted
			}
		}
	}
           

對于已經有數字的位置,則設定按鈕不可用,一個樣例的盤面如下:

![](http://images2017.cnblogs.com/blog/1228116/201710/1228116-20171014234905730-989361797.png)

主要用在提示功能上,首先判斷是否可解,如果可解則在相應的位置上給出提示,不可解則給出相應的提示,部分代碼如下:

if (sudoku->solve(board, solution)) {
        puzzleButtons[currentX][currentY]->setText(QString::number(solution[currentX*LEN + currentY]));
        puzzleButtons[currentX][currentY]->setChecked(false); // Set button unchecked
        checkGame();
    } else {
        QMessageBox::information(this, tr("Bad Sudoku"), tr("Can not give a hint.The current Sudoku\
 is not valid\nPlease check the row,rolumn or 3x3 block to correct it."));
    }
           

描述結對的過程

我們結對的過程總體來說算是不錯的,成功完成了基本功能要求與附加的Step4、Step5。我們的大部分工作在國慶期間完成,那段時間嚴格遵守結對程式設計規範,一人敲代碼,另一人在一旁幫助稽核代碼與提供思路,每一小時進行工作交換,每次交換都把代碼push到Github上,記錄這一步工作的結果。我們用了三天時間實作了邏輯部分的完善與測試,并搭建起了UI的三個頁面架構,總體效率還算不錯。期間也遇到過找不着源頭的bug,費了我們不少時間,不過好在是兩個人合力查資料、想辦法,最終還是解決了問題。國慶過後由于兩人的時間不太能湊得上,我們便将工作分工,一人主攻功能,一人主攻界面,一步步推進項目并達到預期目标。

以下為我們二人結對程式設計時的照片。

結對項目-數獨程式擴充

結對程式設計的優缺點以及兩人各自優缺點

結對程式設計優缺點

    • 互相幫助,互相教對方,可能得到能力上的互補。
    • 實時複審,增強代碼品質,并有效的減少bug。
    • 降低學習成本。一邊程式設計,一邊共享知識和經驗,有效地在實踐中進行學習。
    • 共同讨論,可能更快更有效地解決問題
    • 對于有不同習慣的程式設計人員,可以在起工作會産生麻煩。
    • 需要精力高度集中,容易産生疲勞。
    • 不合适的溝通會導緻團隊的不和諧,降低效率。
    • 結對程式設計可能出現思維趨同,導緻有些bug久久找不出來。
    • 若對工作領域十分熟悉,結對程式設計可能會降低效率。

隊員優缺點

  • 15061119
    • 1.極高的編碼效率

      2.專注于解決每個問題

      3.充滿責任心與工作熱情

    • 1.編碼風格不太統一
  • 15061104
    • 1.能了解支援partner

      2.能力較強

      3.解決了我一直苦惱的設計問題

    • 1.某種程度上,欠缺一些積極性

跨組合作出現的問題

合作小組學号:

15061111

15061129

問題1:dll生成的環境不同

  • 問題描述

    我們組的dll在64位下生成,而合作小組的是在32位下生成的,這樣導緻子產品不可調用。

  • 解決方案

    重新生成了64位的dll,問題解決。

問題2:接口名不同

  • 我們合作小組的接口為:
SODUCORE_API void generate_m(int number, int mode, int **result);
SODUCORE_API void generate_r(int number, int lower, int upper, bool unique, int **result);
SODUCORE_API bool solve_s(int *puzzle, int *solution);
           

而我們自己的接口為:

void generate(int number, int lower, int upper, bool unique, int result[][LEN*LEN]);
void generate(int number, int mode, int result[][LEN*LEN]);
bool solve(int puzzle[], int solution[]);
           

這就導緻改變計算子產品之後需要改名字。

  • 把相應接口的名稱更換即可

問題3:參數規格不同

  • 注意到在13.2.2的雙方的接口中,我們組定義result位二維數組,而合作小組定義為二維指針,這就導緻參數錯誤。
  • 将result轉換位二維指針即可。

軟體釋出階段

  • 使用者回報
    1. 發現一個排名系統的bug
    2. 每次填入一個數字,想要立即修改得再次點選空格
    3. Readme未添加支援的平台
    1. 已更新軟體,修複了bug
    2. 将每次填入數字就彈起空格,改為使用者點其他空格時彈起空格
    3. Readme中添加平台說明與運作說明

PSP表格

PSP2.1 Personal Software Process Stages 預估耗時(分鐘) 實際耗時(分鐘)
Planning 計劃
・ Estimate ・ 估計這個任務需要多少時間
Development 開發 2170 3740
・ Analysis ・ 需求分析 (包括學習新技術) 360 480
・ Design Spec ・ 生成設計文檔 30 20
・ Design Review ・ 設計複審 (和同僚稽核設計文檔)
・ Coding Standard ・ 代碼規範 (為目前的開發制定合适的規範)
・ Design ・ 具體設計 120
・ Coding ・ 具體編碼 1200 2700
・ Code Review ・ 代碼複審 240 180
・ Test ・ 測試(自我測試,修改代碼,送出修改) 300
Reporting 報告 130 190
・ Test Report ・ 測試報告
・ Size Measurement ・ 計算工作量
・ Postmortem & Process Improvement Plan ・ 事後總結, 并提出過程改進計劃
合計 2305 3935