结对项目-数独程序扩展

**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得到相应的updown传入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()方法参数lowerupper不合法情况: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()方法

主要在开始新游戏的时候使用,首先用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)

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转换位二维指针即可。

软件发布阶段

  • 用户反馈

    1. 发现一个排名系统的bug
    2. 每次填入一个数字,想要立即修改得再次点击空格
    3. Readme未添加支持的平台
  • 解决方案

    1. 已更新软件,修复了bug
    2. 将每次填入数字就弹起空格,改为用户点其他空格时弹起空格
    3. 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
posted @ 2017-10-15 14:14  captainYi  阅读(218)  评论(2编辑  收藏  举报