结对项目-数独程序扩展
**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博客作者一去、二三里的黑色炫酷风格。
基本风格见下图
- 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对控件进行对齐
效果见下图
- 游戏界面采用Layout嵌套Layout的形式进行布局管理。我们先设计了一个mainLayout作为最外层Layout,将其他Layout竖直放入mainLayout。
其他Layout见下图
- 为保持数独棋盘排列的紧密,在棋盘周围加了spacer把棋盘上的格子挤压到一起,且能保持形状。
- 为保证比例的美观,游戏窗体被强制固定,无法进行缩小与放大。
小结: 设计布局过程有些小曲折,一开始由于没有经验,不知道该如何用代码该出想要的布局效果,也想过不使用代码修改布局,直接在界面上拖拽。但考虑到代码的灵活性,还是决定使用代码,放弃了拖拽设计(下次有机会做UI,希望尝试下拖拽设计和代码设计结合的形式)。好在有博客和Qt官方文档的支持,还是成功学会了Qt的布局设计,做出了当前这个效果。
界面模块与计算模块的对接
generate()方法
主要在开始新游戏的时候使用,首先用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
}
}
}
对于已经有数字的位置,则设置按钮不可用,一个样例的盘面如下:
solve()方法
主要用在提示功能上,首先判断是否可解,如果可解则在相应的位置上给出提示,不可解则给出相应的提示,部分代码如下:
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转换位二维指针即可。
软件发布阶段
-
用户反馈
- 发现一个排名系统的bug
- 每次填入一个数字,想要立即修改得再次点击空格
- Readme未添加支持的平台
-
解决方案
- 已更新软件,修复了bug
- 将每次填入数字就弹起空格,改为用户点其他空格时弹起空格
- Readme中添加平台说明与运行说明
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 5 | 5 |
・ Estimate | ・ 估计这个任务需要多少时间 | 5 | 5 |
Development | 开发 | 2170 | 3740 |
・ Analysis | ・ 需求分析 (包括学习新技术) | 360 | 480 |
・ Design Spec | ・ 生成设计文档 | 30 | 20 |
・ Design Review | ・ 设计复审 (和同事审核设计文档) | 10 | 10 |
・ Coding Standard | ・ 代码规范 (为目前的开发制定合适的规范) | 30 | 20 |
・ Design | ・ 具体设计 | 120 | 30 |
・ Coding | ・ 具体编码 | 1200 | 2700 |
・ Code Review | ・ 代码复审 | 240 | 180 |
・ Test | ・ 测试(自我测试,修改代码,提交修改) | 180 | 300 |
Reporting | 报告 | 130 | 190 |
・ Test Report | ・ 测试报告 | 5 | 5 |
・ Size Measurement | ・ 计算工作量 | 5 | 5 |
・ Postmortem & Process Improvement Plan | ・ 事后总结, 并提出过程改进计划 | 120 | 180 |
合计 | 2305 | 3935 |