1、在文章開頭給出Github項目位址。
https://github.com/si1entic/Sudoku-2.git
2、在開始實作程式之前,在下述PSP表格記錄下你估計将在程式的各個子產品的開發上耗費的時間。
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
Planning | 計劃 | 10 | |
· Estimate | · 估計這個任務需要多少時間 | ||
Development | 開發 | 2380 | |
· Analysis | · 需求分析 (包括學習新技術) | 60 | |
· Design Spec | · 生成設計文檔 | ||
· Design Review | · 設計複審 (和同僚稽核設計文檔) | 20 | |
· Coding Standard | · 代碼規範 (為目前的開發制定合适的規範) | ||
· Design | · 具體設計 | ||
· Coding | · 具體編碼 | 2160 | |
· Code Review | · 代碼複審 | 30 | |
· Test | · 測試(自我測試,修改代碼,送出修改) | 160 | |
Reporting | 報告 | 95 | |
· Test Report | · 測試報告 | ||
· Size Measurement | · 計算工作量 | 5 | |
· Postmortem & Process Improvement Plan | · 事後總結, 并提出過程改進計劃 | ||
合計 | 2585 |
3、看教科書和其它資料中關于Information Hiding, Interface Design, Loose Coupling的章節,說明你們在結對程式設計中是如何利用這些方法對接口進行設計的。
對于一個類、一個方法,在設計初先約定好其輸入參數,會取用的值,會改變的值,傳回的結果,并盡量做到方法中參數的來源使用傳參而不是直接去用類中的變量。
根據方法功能劃分各個不同方法,盡量避免硬代碼的使用。
在允許範圍内,盡可能減少各個方法之間的依賴關系。
比如對于數獨生成算法、數獨檢查算法,傳入參數清晰,UI部分不需關注計算部分具體的實作方式,這也為不同項目的子產品交換打下基礎。
4、計算子產品接口的設計與實作過程。設計包括代碼如何組織,比如會有幾個類,幾個函數,他們之間關系如何,關鍵函數是否需要畫出流程圖?說明你的算法的關鍵(不必列出源代碼),以及獨到之處。
Core子產品主要可分為三部分,一是随機生成終盤,二是按要求挖空,三是求解數獨題目。是以主要分為三個類,其中FinalMaker類的make函數采用每行随機填數的方法生成一個終盤(為了可玩性犧牲了絕對不重複性,雖然理論上可能生成等效數獨但機率極低),PuzzleSovlver類的求解函數采用效率極高的DLX算法,而Core類通過調用這兩類的函數來實作随機生成終盤、求解數獨、保證唯一解挖空功能。最後一個功能應該是最難實作的,這裡我們采取的辦法是:先生成終盤,再挖空,然後求解看有沒有多解,有則重新挖。流程圖如下:

5、閱讀有關UML的内容,畫出UML圖顯示計算子產品部分各個實體之間的關系(畫一個圖即可)。
6、計算子產品接口部分的性能改進。記錄在改進計算子產品性能上所花費的時間,描述你改進的思路,并展示一張性能分析圖(由VS 2015/2017的性能分析工具自動生成),并展示你程式中消耗最大的函數。
主要分析最複雜的生成唯一解數獨的功能。生成數量少時還好,個數大于1000就明顯慢到無法接受的地步。分析發現findSolutions()函數耗時極長,于是針對它做了修改,在發現有第二個解時就抛出一個int,在最外面通過try catch來接收這個抛出,進而跳出了多重的遞歸,大大減少了判斷是否有唯一解的時間。下面是在-n 10000 -r 40~50 -u參數下的性能分析圖:
消耗最大的函數時Input類的handle函數,負責調用其他函數實作功能。而各功能函數中,耗時較多的是生成随機數獨的make()和檢查唯一解的checkUnique()。但需要說明的是,當挖空數在50以上時,程式耗時會大大加長,原因在于挖較多空時需要大量地調用checkUnique函數,導緻消耗激增,暫時沒有更好的解決方法。
7、看Design by Contract, Code Contract的内容,
描述這些做法的優缺點, 說明你是如何把它們融入結對作業中的。
契約式設計優點:
1、對團隊各成員之間了解各自的方法很有幫助,特别是對于大的團隊
2、對于新入這個團隊的成員了解之前的代碼很有幫助
3、也是對自己編碼時的一種限制,關注了這些方面,相對不易出現問題
契約式設計缺點:
1、一定程度上降低效率
8、計算子產品部分單元測試展示。展示出項目部分單元測試代碼,并說明測試的函數,構造測試資料的思路。并将單元測試得到的測試覆寫率截圖,發表在部落格中。要求總體覆寫率到90%以上,否則單元測試部分視作無效。
Core單元的功能為生成和求解數獨,對于生成的測試主要分三個方面:一是生成的題目是否合法(比如某行是否會出現兩個"1"之類的),二是挖空數是否在規定範圍之内。可通過下面的函數檢測:
```
bool checkValid(int final[9][9], int row, int col, int& blanks)
{
int value = final[row][col];
if (value == 0)
blanks++;
return true;
}
for (int i = row / 3 * 3; i < row / 3 * 3 + 3; i++) // 檢測該塊是否已有該數字
for (int j = col / 3 * 3; j < col / 3 * 3 + 3; j++)
if (final[i][j] == value)
if (!(i == row&&j == col))
return false;
for (int i = 0; i < 9; i++) // 檢測該行該列是否已有該數字
if ((i != col&&final[row][i] == value) || (final[i][col] == value&&i != row))
三是判斷是否有唯一解,這裡直接調用PuzzleSolve::checkUnique()進行檢查。 測試代碼:
[TestMethod]
void TestGenerate1()
srand((unsigned)time(NULL));
Core c;
const int number = 100;
for (int mode = 1; mode <= 3; mode++) // 周遊三個難度
int result[number][81];
c.generate(number, mode, result);
int game[9][9];
int blanks;
for (int i = 0; i < number; i++) // 周遊生成的題目
memcpy(game, result[i], sizeof(game));
blanks = 0;
for (int j = 0; j < 81; j++)
Assert::IsTrue(checkValid(game, j / 9, j % 9, blanks), L"合法性出錯");
switch (mode)
case 1:
Assert::IsTrue(blanks >= 20 && blanks <= 30, L"難度1挖空範圍出錯");
break;
case 2:
Assert::IsTrue(blanks >= 31 && blanks <= 45, L"難度2挖空範圍出錯");
case 3:
Assert::IsTrue(blanks >= 46 && blanks <= 55, L"難度3挖空範圍出錯");
default:
};
[TestMethod]
void TestGenerate2()
{
Core c;
const int number = 100, lower = 20, upper = 30;
int result[number][81];
c.generate(number, lower, upper, false, result);
int game[9][9];
int blanks;
for (int i = 0; i < number; i++) // 周遊生成的題目
{
memcpy(game, result[i], sizeof(game));
blanks = 0;
for (int j = 0; j < 81; j++)
Assert::IsTrue(checkValid(game, j / 9, j % 9, blanks), L"合法性出錯");
Assert::IsTrue(blanks >= lower && blanks <= upper, L"挖空範圍出錯");
}
};
[TestMethod]
void TestGenerate3()
{
Core c;
PuzzleSovlver ps;
const int number = 100, lower = 40, upper = 55;
int result[number][81];
c.generate(number, lower, upper, true, result);
int game[9][9];
int blanks;
for (int i = 0; i < number; i++) // 周遊生成的題目
{
memcpy(game, result[i], sizeof(game));
blanks = 0;
for (int j = 0; j < 81; j++)
Assert::IsTrue(checkValid(game, j / 9, j % 9, blanks), L"合法性出錯");
Assert::IsTrue(blanks >= lower && blanks <= upper, L"挖空範圍出錯");
Assert::IsTrue(ps.checkUnique(game), L"唯一性出錯");
}
};
[TestMethod]
void TestSolve()
{
Core c;
int puzzle[1][81];
int final[9][9];
int blanks = 0;
c.generate(1, 1, puzzle);
Assert::IsTrue(c.solve(puzzle[0], puzzle[0]), L"求解失敗");
memcpy(final, puzzle, sizeof(final));
for (int j = 0; j < 81; j++)
Assert::IsTrue(checkValid(final, j / 9, j % 9, blanks), L"合法性出錯");
c.generate(1, 2, puzzle);
Assert::IsTrue(c.solve(puzzle[0], puzzle[0]), L"求解失敗");
memcpy(final, puzzle, sizeof(final));
for (int j = 0; j < 81; j++)
Assert::IsTrue(checkValid(final, j / 9, j % 9, blanks), L"合法性出錯");
c.generate(1, 3, puzzle);
Assert::IsTrue(c.solve(puzzle[0], puzzle[0]), L"求解失敗");
memcpy(final, puzzle, sizeof(final));
for (int j = 0; j < 81; j++)
Assert::IsTrue(checkValid(final, j / 9, j % 9, blanks), L"合法性出錯");
c.generate(1, 20, 55, false, puzzle);
Assert::IsTrue(c.solve(puzzle[0], puzzle[0]), L"求解失敗");
memcpy(final, puzzle, sizeof(final));
for (int j = 0; j < 81; j++)
Assert::IsTrue(checkValid(final, j / 9, j % 9, blanks), L"合法性出錯");
c.generate(1, 50, 55, true, puzzle);
Assert::IsTrue(c.solve(puzzle[0], puzzle[0]), L"求解失敗");
memcpy(final, puzzle, sizeof(final));
for (int j = 0; j < 81; j++)
Assert::IsTrue(checkValid(final, j / 9, j % 9, blanks), L"合法性出錯");
puzzle[0][0] = puzzle[0][1] = 1;
Assert::IsFalse(c.solve(puzzle[0], puzzle[0]), L"解出非法數獨");
};
```
單元測試覆寫率:
9、計算子產品部分異常處理說明。在部落格中詳細介紹每種異常的設計目标。每種異常都要選擇一個單元測試樣例釋出在部落格中,并指明錯誤對應的場景。
針對generate和solve接口的參數,異常可分為以下四類。
- NumberException:-n/-c參數的number範圍出錯
[TestMethod] void TestNumberException() { Core c; int result[1][81]; try { c.generate(-1, 1, result); // number傳入-1 Assert::Fail(L"number範圍出錯"); } catch (NumberException& e) { cout << e.what() << endl; } try { c.generate(INT_MAX, 20, 30, true, result); // number傳入最大int值 Assert::Fail(L"number範圍出錯"); } catch (NumberException& e) { cout << e.what() << endl; } };
- ModeException :-m參數的mode範圍出錯
[TestMethod] void TestModeException() { Core c; int result[1][81]; try { c.generate(1, 0, result); // mode傳入0 Assert::Fail(L"mode範圍出錯"); } catch (ModeException& e) { cout << e.what() << endl; } try { c.generate(1, 4, result); // mode傳入4 Assert::Fail(L"mode範圍出錯"); } catch (ModeException& e) { cout << e.what() << endl; } };
- RangeException :-r參數的range範圍出錯
[TestMethod] void TestRangeException() { Core c; int result[1][81]; try { c.generate(1, -1, 20, false, result); // lower傳入-1 Assert::Fail(L"range範圍出錯"); } catch (RangeException& e) { cout << e.what() << endl; } try { c.generate(1, 50, 40, false, result); // lower比upper傳入-1 Assert::Fail(L"range範圍出錯"); } catch (RangeException& e) { cout << e.what() << endl; } try { c.generate(1, 20, 56, false, result); // upper傳入56 Assert::Fail(L"range範圍出錯"); } catch (RangeException& e) { cout << e.what() << endl; } };
- ValidException :傳入非法數獨報錯
[TestMethod] void TestValidException() { Core c; int result[1][81]; c.generate(1, 3, result); result[0][0] = result[0][1] = 1; try { c.solve(result[0], result[0]); // 傳入非法數獨 Assert::Fail(L"解出非法數獨"); } catch (ValidException& e) { cout << e.what() << endl; }; };

10、界面子產品的詳細設計過程。在部落格中詳細介紹界面子產品是如何設計的,并寫一些必要的代碼說明解釋實作過程。
QtGuiApplication2.cpp/h 界面部分
界面分為菜單欄、主界面,最佳紀錄界面和說明界面四部分。
菜單欄中有New和Help兩個界面,New中提供選擇難度以及最佳紀錄檢視功能,Help對應于說明界面。通過QAction實作動作的,并用connect進行綁定
例:
QtGuiApplication2.cpp
private:
QAction *easyOpenAction;
QMenu *menuNew;
void easyOpen();
…………
menuNew = menuBar()->addMenu(tr("&New"));
menuNew->addAction(easyOpenAction);
easyOpenAction = new QAction(tr("Easy"), this);
connect(easyOpenAction, &QAction::triggered, this, &QtGuiApplication2::easyOpen);
主界面劃分為上下兩個部分。上部分是一些目前狀态及重置按鈕的顯示,下部分為遊戲主界面,其中又分九個小格,通過對Margin參數的設定,實作小九宮格之間的空隙。
QGridLayout *mainLayout; // 主界面
QGridLayout *topLayout; // 上部分
QGridLayout *midLayout; // 遊戲部分
QGridLayout *midLayoutIn[3][3]; // 小九宮格
…………
for (int i = 0; i < 3; i++) // 将小九宮格加入遊戲部分
{
for (int j = 0; j < 3; j++)
{
midLayoutIn[i][j] = new QGridLayout();
midLayoutIn[i][j]->setMargin(2); // 空隙
midLayout->addLayout(midLayoutIn[i][j], i, j, 0);
}
}
for (int i = 0; i < 81; i++) // 向小九宮格中加入小格子
{
midLayoutIn[i / 9 / 3][i % 9 / 3]->addWidget(sudo[i], i / 9, i % 9, 0);
connect(sudo[i], SIGNAL(tip_clicked()), this, SLOT(tipClick()));
connect(sudo[i], SIGNAL(textChanged(const QString& )), this, SLOT(sudoTableEdit()));
// 如果檢測到參數改變,則調用相應方法,方法中會對填入的數進行一個簡單判斷,并檢查該數獨是否完全正确
}
最佳紀錄界面中為最佳紀錄的展示以及重置功能,實作基本同上,使用 recordLayout->show();彈出新視窗
說明界面中則用一個标簽對程式進行簡單介紹
MineEditLine.cpp/h 重寫的單行輸入框控件
對于提示功能,由于需要具體确定格子位置,最終選擇了在格子上右鍵,會彈出一個菜單欄,其中有tip選項,點選tip獲得該格子的提示的方式。為此,我通過MineEdlitLine繼承了QEditLine類,重寫了其中的contextMenuEvent方法,并在選中tip時放出一個tip_click()的信号,主視窗通過接收到這個信号,來執行相關操作。
MineEditLine.cpp
void MineLineEdit::contextMenuEvent(QContextMenuEvent *event)
{
//清除原有菜單
pop_menu->clear();
if (this->isReadOnly()) { // 如果不可填,就不彈出菜單
return;
}
pop_menu->addAction(tipAction);
pop_menu->exec(QCursor::pos());
event->accept();
}
…………
connect(tipAction, &QAction::triggered, this, &MineLineEdit::tip);
…………
emit tip_clicked();
ps: 由于是文本框模式,需要限制輸入,具體實作大緻如下
QRegExp rx("[1-9]");
sudo[i]->setMaxLength(1);
sudo[i]->setValidator(new QRegExpValidator(rx, sudo[i]));
11、界面子產品與計算子產品的對接。詳細地描述UI子產品的設計與兩個子產品的對接,并在部落格中截圖實作的功能。
計算子產品執行個體
QtGuiApplication2.cpp
private: // Core中對應的子產品
Core sudoku;
FinalMaker sudoCheck1;
PuzzleSovlver sudoCheck2;
UI子產品設計與對接
UI中在數獨生成、提示生成、簡單檢查填入數是否合法的部分使用到了計算子產品。
數獨生成
點選start/restart按鈕後,通過 sudoku.generate(1, model, result); 調用計算子產品中的數獨生成,再通過一一将數以對應方式呈現到界面上,實作初始遊戲界面的生成。
提示生成
在檢查到tip_click()信号後,調用相關方法,通過數獨求解方法生成tip,并以藍色顯示在對應位置上。如果目前數獨不合法或不可解,則彈出對應提示。
void QtGuiApplication2::tipClick()
{
MineLineEdit *mle = qobject_cast<MineLineEdit*>(sender());
int i = mle->accessibleName().toInt();
qDebug() << "tip clicked:" << i;
int solution[81];
bool f = false;
try
{
f = sudoku.solve(result[0], solution);
}catch(ValidException e)
{
QMessageBox::information(this, tr("tip"), tr("Already Wrong"));
return;
}
if (f)
{
mle->setText(QString::number(solution[i]));
result[0][i] = solution[i];
sudoTable[i / 9][i % 9] = solution[i];
mle->setStyleSheet("color: blue;");
}else
{
QMessageBox::information(this, tr("tip"), tr("Already Wrong"));
}
}
簡單檢查填入數是否合法
當檢查到某格中被編輯,調用sudoTableEdit方法,檢查該格在規則中是否可填入。如果否,則以紅色顯示該填入數字。在每次數字更改後,檢查數獨是否完全正确,如果是,則遊戲結束。
void QtGuiApplication2::sudoTableEdit()
{
…………
sudoTable[i / 9][i % 9] = 0;
if (sudoCheck1.checkValid(sudoTable, i / 9, i % 9, x))
{
result[0][i] = mle->text().toInt();
sudoTable[i / 9][i % 9] = mle->text().toInt();
mle->setStyleSheet("color: black;");
}else {
result[0][i] = mle->text().toInt();
sudoTable[i / 9][i % 9] = mle->text().toInt();
mle->setStyleSheet("color: red;");
}
if (isfilled(sudoTable))
{
if (sudoCheck2.checkValid(sudoTable))
{
stopmTimer();
QString *qstr = new QString();
readInit(QString::number(model), *qstr);
if (qstr->toInt() > timeTimer)
{
writeInit(QString::number(model), QString::number(timeTimer));
QMessageBox::information(this, tr("Congratulations"), tr("You Win! New Record!"));
}else
{
QMessageBox::information(this, tr("Congratulations"), tr("You Win!"));
}
for (int i = 0; i < 81; i++)
{
sudo[i]->setReadOnly(true);
}
}
}
}
12、描述結對的過程,提供非擺拍的兩人在讨論的結對照片。
一開始算是意料之外的結對吧,由于之前定的結對隊友的團隊解散而來到我們團隊,在軟工課上新尋了隊友。談論一般在各個課結束後,留在教室中讨論,平常便是微信QQ方式。
13、看教科書和其它參考書,網站中關于結對程式設計的章節,例如:
http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html
說明結對程式設計的優點和缺點。
優點:
兩人一起程式設計,會各有所長,充分利用兩人的優勢點,能有效提高開發效率。
互相監督,互相學習。一人程式設計難免會有疏忽遺漏,以及一些沒有想到的方面。兩人較一人更不容易犯錯,也更容易發現軟體中的bug。
可以友善地讨論。往往能有新穎的點子想法。
在接口方面,由于兩人程式設計,會開始注意這一塊,同時相對團隊交流友善。
缺點:
兩人之間習慣、性格之間的磨合不一定順利,如果兩人之間沖突較大反而會有負面作用。
相比兩人單獨程式設計,一定程度上降低了效率。
結對的每一個人的優點和缺點在哪裡 (要列出至少三個優點和一個缺點)。
行動力強
代碼的結構風格很好
性格不錯
對要求以外的部分不是很關心(笑)
14、在你實作完程式之後,在附錄提供的PSP表格記錄下你在程式的各個子產品上實際花費的時間。
2420 | |||
180 | |||
120 | |||
1440 | |||
100 | |||
2025 | |||