zoukankan      html  css  js  c++  java
  • 结对项目-数独程序扩展

    **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- 1px;
    	border-style: solid;
    	border-radius: 0;
    }
    QPushButton#puzzleButtonTLCorner {
        	border-radius: 0;
    	border-top-left-radius: 4px;
    	border- 1px;
        	border-style: solid;
    }
    QPushButton#puzzleButtonTRCorner {
    	border-radius: 0;
    	border-top-right-radius: 4px;
        	border- 1px;
        	border-style: solid;
    }
    QPushButton#puzzleButtonBLCorner {
    	border-radius: 0;
    	border-bottom-left-radius: 4px;
        	border- 1px;
        	border-style: solid;
    }
    QPushButton#puzzleButtonBRCorner {
    	border-radius: 0;
    	border-bottom-right-radius: 4px;
        	border- 1px;
        	border-style: solid;
    }
    QPushButton#puzzleButtonRE {
    	border-radius: 0;
    	border- 1px;
    	border-right- 3px;
        	border-style: solid;
    }
    QPushButton#puzzleButtonBE {
    	border-radius: 0;
        	border- 1px;
    	border-bottom- 3px;
        	border-style: solid;
    }
    QPushButton#puzzleButtonBRE {
    	border-radius: 0;
        	border- 1px;
            border-right-3px;
    	border-bottom- 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
    Please 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
  • 相关阅读:
    大话设计模式---单一职责原则
    大话设计模式---策略模式
    计算机网络(二)
    计算机网络(一)
    栈与队列

    数据库面试题——基本概念
    链表
    【Essential c++】——(三)泛型编程风格
    【转载】学习JAVA WEB的路线
  • 原文地址:https://www.cnblogs.com/captainYi/p/7670936.html
Copyright © 2011-2022 走看看