PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
---|---|---|
Planning | 计划 | 120 |
Estimate | · 估计这个任务须要多少时间 | 5 |
Development | 开发 | 10 |
Analysis | · 需求分析 (包括学习新技术) | 300 |
Design Spec | · 生成设计文档 | 30 |
Design Review | · 设计复审 (和同事审核设计文档) | 5 |
Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 |
Design | · 具体设计 | 30 |
Coding | · 具体编码 | 1440 |
Code Review | · 代码复审 | 60 |
Test | · 测试(自我测试,修改代码,提交修改) | 180 |
Reporting | 报告 | 10 |
Test Report | · 测试报告 | 10 |
Size Measurement | · 计算工做量 | 10 |
Postmortem & Process Improvement Plan | · 过后总结, 并提出过程改进计划 | 60 |
Total | 合计 | 2280 |
Information hiding is part of the foundation of both structured design and object-oriented design. In structured design, the notion of “black boxes” comes from
information hiding. In object-oriented design, it gives rise to the concepts of encapsulation and modularity, and it is associated with the concept of abstraction.
引自代码大全,见:http://www.cnblogs.com/magiccode1023/archive/2012/10/23/2736257.htmlhtml
不用多说,最基本的抽象原则,保证程序的健壮性和灵活性。
上述连接中也提到了,信息隐藏要隐藏的信息包括两方面:git
1.隐藏可能的复杂性。
2.隐藏外部改变会有危险的信息。github
隐藏复杂性中隐藏了分而治之的思想,而第2点则是为了程序的安全性。
代码中的不少地方都隐藏了复杂性:算法
for (int k = 1; k <= LEN; ++k) { if (Sudoku::count >= number) return; if (checkGeneratePos(i, j, k)) { //check if it is ok to set k on (i,j) board[i][j] = k + '0'; traceBackWriteFile(i, j + 1, number, outFile); //if can,recur to next place } }
上述代码是回溯函数中的一部分,checkGeneratePos(i,j,k)
就是一个典型的复杂性隐藏,它隐去了方法实现的内部细节,从而让咱们能专一于实现主要的算法。编程
咱们在coding中避免类成员和一些辅助函数暴露,因此将它们设置为private
:数组
private: char board[LEN + 1][LEN + 1]; void init(); inline void traceBackN(int i, int j, int n, int result[][LEN*LEN]); inline bool traceBackSolve(int i, int j); inline int getBlock(int i); void traceBackWriteFile(int i, int j, int number, fstream &outFile); void traceBackCountSolution(int i, int j, int *solutionNumber, int bound); void digHoles(int count, int mode, int lower, int upper, int result[][LEN*LEN]); static long int count;
上述方法或者类成员一旦在外部被调用就会以不正常的方式修改类,因此须要避免。
咱们设计方法的一个很重要的原则就是:这个方法具备单一的功能。有些时候总有一些控制方法会整合不少功能,这个时候咱们须要分解这些功能为一个个小的功能,并分别实现它们。安全
Interface Design简单来讲就是事先约定好模块的接口,此次实现的两个generate
接口以及solve
接口都是这个的体现,由于对接口有严格的规定,在step4咱们互换Core
模块的时候才没有太多的麻烦,很容易就完成了代码。框架
松耦合在下面连接中有较好的解释:
stackoverflow-Loose Coupling
其中心思想就是尽可能减小模块之间的“依赖”,最理想的状态大概是更换程序中任何一个方法或是函数都只须要在方法或函数内部进行更改,这也是抽象的一种体现,我的认为实现这个原则的最好方法就是在各个模块之间的输入输出之间增长抽象层,这就至关于“松弛”了各个模块之间的耦合。ide
咱们实现的solve
方法很好地说明了这一点:函数
bool Sudoku::solve(int puzzle[], int solution[]) throw(IllegalLengthException) { bool ret; convertToTwoDimension(puzzle); ret = traceBackSolve(1, 1); if (!check()) { return false; } convertToOneDimension(solution); return ret; }
这个方法中convertToOneDimension
会把当前数独的解复制到solution
中,咱们并非直接将类成员中的数组传出,咱们实际上就是在方法的输出上增长了抽象层。这样的方法使得若是当咱们将数独类成员中的数组换成1维数组(原来是2维)的时候,咱们只须要在convertToOneDimension
内部进行修改。
咱们主要的算法逻辑都集中在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图以下:
相互之间没有依存关系。
下图展现了生成1000000个完整数独的性能分析
因为此次继承了我上次的代码,因此代码自己已经被优化过。
5.272秒,几乎全部的时间都花费在回溯递归上,速度已经能够接受。
一个可能的优化是在判断重复的时候使用位操做。
下图展现了解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来优化。
直接在-u
模式下测试,因为当r的参数的值变大的时候生成10000个解的时间几乎不可接受,因此选择较低的数值,下图是指令-n 10000 -r 25~55
的效能分析:
24秒
热路径主要集中于solve
函数,判断缘由仍是因为递归时形成的指数级增加的函数调用,在不更改现有结构的状况下已经很难改进。
改进效能花费了30分钟。
契约式编程咱们已经在OO课上实践过,其中心思想为:在完成代码模块的时候,首先制定“契约”,它描述这个模块的一些性质,包括调用时候知足的前置条件(precondition)、方法完成时候知足的后置条件(postcondition)、方法抛出的异常(exception)以及方法知足的不变式(invariant),最后根据“契约”来完成代码。一个比较典型的契约式编程的例子就是Assert语句了。
这种编程方式首先假定全部输入都知足前置条件(precondition),而与其相反的防护式编程则假定输入会有全部可能的状况,包括正确和错误的。
很明显,契约式编程很是适合于在程序开发时使用,同时也有不少工具简化了这种编程方式,此次给出的参考连接中,Code Contracts for .NET就是一个这样的工具,这个工具能够自动检测模块中的契约来测试它们是否知足条件,从而实现Runtime checking,static checking等功能。不过,在程序发布的时候通常须要取消契约检查,由于它对性能也有必定影响。
契约式编程很是有助于错误的精肯定位,虽然绝大多数流行的程序语言在程序运行出错时都会在必定程度上给出提示,但咱们更但愿在早期发现程序的错误,而不是等到错误一层层传递到顶层才发现他们。
总结来讲,契约式编程在设计层面保证了程序的正确性,但当咱们将程序发布,咱们就必须作好准备应付各类可能的错误,而不是等待用户去知足契约了。
此次实现的Core计算模块的接口也是一种契约,好比generate(int number,int lower,int upper,bool unique,int result[][])
方法中,调用者须要知足参数的一些条件,而这个方法也须要知足在方法完成以后在result
数组中存储number
个规定的数独游戏的后置条件(postcondition),咱们在测试的时候,也是根据这些条件进行测试的。
测试思路:给出一个题目,和答案对比。
ret = sudoku.solve(puzzle, temp); Assert::AreEqual(ret, true); for (int i = 0; i < 81; ++i) { Assert::AreEqual(temp[i], solution[i]); }
测试思路:对-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节进行了说明。
下图展现了咱们对于异常的设计:
Error Code | 异常类 | 异常说明 | 错误提示 |
---|---|---|---|
8 | SudokuCountRangeException | generate(int number,int lower,int upper,bool unique,int result[][])中number范围错误 | number in generate(int number,int lower,int upper,bool unique,int result[][]) must in[1,10000] |
16 | LowerUpperException | generate(int number,int lower,int upper,bool unique,int result[][])中lower和upper的值错误 | the lower and upper in generate(int number,int lower,int upper,bool unique,int result[][]) must satisfy:lower<upper,lower > 20,upper < 55 |
17 | ModeRangeException | generate(int number, int mode, int result[][])函数中mode的值错误 | the number of mode must in [1,3] |
下面分别给出异常对应的测试样例,测试方法已经在以前说明。
int result[1][81]; bool exceptionThrown = false; try { // Test first SudokuCountException sudoku.generate(0, 1, result); } catch (SudokuCountException& e) { exceptionThrown = true; e.what(); } Assert::IsTrue(exceptionThrown); exceptionThrown = false; try { sudoku.generate(100000, 20, 50, true, result); } catch (SudokuCountException& e) { exceptionThrown = true; e.what(); } Assert::IsTrue(exceptionThrown);
上例中两次调用generate
函数,生成数量分别为0和100000,都会抛出异常。
//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);
上例中测试了upper
和lower
抛出异常的3种状况,分别是lower
超出范围,upper
超出范围和lower
、upper
不知足lower<upper的状况
//test ModeRangeException exceptionThrown = false; try { sudoku.generate(1, -1, result); } catch (ModeRangeException& e) { exceptionThrown = true; e.what(); } Assert::IsTrue(exceptionThrown);
上例中generate
调用的模式出错,只能是一、二、3,因此抛出异常。
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()方法来设计界面风格。不事后来发现自带的界面实在太丑,因而决定借鉴已有的风格,针对项目要求进行调整,最终效果还算不错。
欢迎、帮助与选择难度界面统一使用QVBoxLayout对控件进行对齐
效果见下图
为保证比例的美观,游戏窗体被强制固定,没法进行缩小与放大。
小结: 设计布局过程有些小曲折,一开始因为没有经验,不知道该如何用代码该出想要的布局效果,也想过不使用代码修改布局,直接在界面上拖拽。但考虑到代码的灵活性,仍是决定使用代码,放弃了拖拽设计(下次有机会作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 } } }
对于已经有数字的位置,则设置按钮不可用,一个样例的盘面以下:
主要用在提示功能上,首先判断是否可解,若是可解则在相应的位置上给出提示,不可解则给出相应的提示,部分代码以下:
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.")); }
此次咱们的结对过程比较顺利,双方都能作到互相理解支持,咱们的大部分工做在国庆期间完成,过程按照《构建之法》上讲到的,1小时切换一次。个人partner有些缺少积极性,因此虽然有点很差意思不过我会去督促他,这样就保证了效率,另外一方面我在UI设计上经验不足,个人partner解决了这个问题。我认为咱们基本实现了取长补短。
同时我也体会到了在高强度编程的时候,高频次地更换驾驶员和领航员的职责是颇有必要的,这样会缓解疲劳和压力,从而提升了代码的质量。
不过,在结对的过程当中,我也由于编程过程被人监督而有些不自在,感受没有彻底发挥本身的水平。
整体而言,我认为咱们发挥告终对编程的优点,但要进一步提升效率和质量,也许我和partner之间须要更多的磨合。
下面是队友的感觉:
咱们结对的过程整体来讲算是不错的,成功完成了基本功能要求与附加的Step四、Step5。咱们的大部分工做在国庆期间完成,那段时间严格遵照结对编程规范,一人敲代
码,另外一人在一旁帮助审核代码与提供思路,每一小时进行工做交换,每次交换都把代码push到Github上,记录这一步工做的结果。咱们用了三天时间实现了逻辑部分的> 完善与测试,并搭建起了UI的三个页面框架,整体效率还算不错。期间也遇到过找不着源头的bug,费了咱们很多时间,不过好在是两我的协力查资料、想办法,最终仍是> 解决了问题。国庆事后因为两人的时间不太能凑得上,咱们便将工做分工,一人主攻功能,一人主攻界面,一步步推动项目并达到预期目标。
如下为咱们二人结对编程时的照片。
在我看来,若是想要发挥结对编程的所有做用,就须要本人和partner之间加深了解和合做、互相不介意暴露问题、而且深入领会领航员和驾驶员的职责所在,取长补短,这样才能有好的结果。
咱们互评了优缺点,结果以下:
15061119
优势:
1.极高的编码效率。
2.专一于解决每一个问题。
3.充满责任心与工做热情。
缺点:
1.编码风格不太统一。
15061104
优势:
1.能理解支持partner。
2.能力较强。
3.解决了我一直苦恼的设计问题。
缺点:
1.某种程度上,欠缺一些积极性。
如下是个人自我评价:
在博客中指明合做小组两位同窗的学号,分析两组不一样的模块合并以后出现的问题,为什么会出现这样的问题,
以及是如何根据反馈改进本身模块的。
15061111
15061129
问题描述
咱们组的dll在64位下生成,而合做小组的是在32位下生成的,这样致使模块不可调用。
解决方案
从新生成了64位的dll,问题解决。
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[]);
这就致使改变计算模块以后须要更名字。
问题描述
注意到在13.2.2的双方的接口中,咱们组定义result位二维数组,而合做小组定义为二维指针,这就致使参数错误。
解决方案
将result转换位二维指针便可。
咱们在step4的基础上进行了增量开发,主要实现了:帮助、错误提示、快速存档读档以及继续游戏的功能。
咱们在主界面加入了帮助按钮,进入以后会显示数独的规则以及一个完整的数独:
用户点击return按钮能够返回到主界面。
错误提示就是当用户填入的数字不知足数独的约束条件的时候,对不知足的数字对标红,这样用户能够很容易发现本身的错误,参看下图:
上图中填入的1和同列以及同行的1冲突,因此显示为红色,更改以后颜色回复正常:
用户进入游戏以后若是想要保存当前的盘面,则只须要点击菜单栏的QuickSave进行存档,以后若是想回到存档时候的状态,则只须要点击QuicjLoad。
点击QuickSave存档:
继续游戏:
而后点击QuicjLoad相应的存档点:
恢复:
当用户上次未完成游戏直接退出,再一次进入游戏能够点击Continue来恢复界面:
点击以后恢复:
PSP2.1 | Personal Software Process Stages | 实际耗时(分钟) |
---|---|---|
Planning | 计划 | 180 |
Estimate | · 估计这个任务须要多少时间 | 5 |
Development | 开发 | 10 |
Analysis | · 需求分析 (包括学习新技术) | 300 |
Design Spec | · 生成设计文档 | 20 |
Design Review | · 设计复审 (和同事审核设计文档) | 5 |
Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 |
Design | · 具体设计 | 50 |
Coding | · 具体编码 | 2700 |
Code Review | · 代码复审 | 180 |
Test | · 测试(自我测试,修改代码,提交修改) | 240 |
Reporting | 报告 | 10 |
Test Report | · 测试报告 | 10 |
Size Measurement | · 计算工做量 | 10 |
Postmortem & Process Improvement Plan | · 过后总结, 并提出过程改进计划 | 60 |
Total | 合计 | 3790 |
此次做业中,我收获了不少,我学会了如何用Qt进行GUI设计、以及如何将程序导出成DLL进行复用,同时我也实践告终对编程。 经过此次的结对编程我对合做的优点和劣势有了更深的体会,若是两人的之间有足够的支持,并能积极改进自身的缺点,就能很好地进行合做,合做的重点就在于对事不对人、取长补短和理解包容。