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

    1、在文章开头给出Github项目地址。

    https://github.com/si1entic/Sudoku-2.git

    2、在开始实现程序之前,在下述PSP表格记录下你估计将在程序的各个模块的开发上耗费的时间。

    PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
    Planning 计划 10
    · Estimate · 估计这个任务需要多少时间 10
    Development 开发 2380
    · Analysis · 需求分析 (包括学习新技术) 60
    · Design Spec · 生成设计文档 10
    · Design Review · 设计复审 (和同事审核设计文档) 20
    · Coding Standard · 代码规范 (为目前的开发制定合适的规范) 20
    · Design · 具体设计 20
    · Coding · 具体编码 2160
    · Code Review · 代码复审 30
    · Test · 测试(自我测试,修改代码,提交修改) 160
    Reporting 报告 95
    · Test Report · 测试报告 30
    · Size Measurement · 计算工作量 5
    · Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 60
    合计 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))
    return false;
    return true;
    }
    三是判断是否有唯一解,这里直接调用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挖空范围出错");
    break;
    case 3:
    Assert::IsTrue(blanks >= 46 && blanks <= 55, L"难度3挖空范围出错");
    break;
    default:
    break;
    }
    }
    }
    };

    [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接口的参数,异常可分为以下四类。

    1. 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;
          }
      };
      
    2. 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;
          }
      };
      
    3. 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;
          }
      };
      
    4. 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;
          };
      };
      
       ![](http://images2017.cnblogs.com/blog/1217918/201710/1217918-20171009192543309-1340684452.png)
      

    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表格记录下你在程序的各个模块上实际花费的时间。

    PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
    Planning 计划 10 5
    · Estimate · 估计这个任务需要多少时间 10 5
    Development 开发 2380 2420
    · Analysis · 需求分析 (包括学习新技术) 60 180
    · Design Spec · 生成设计文档 10 10
    · Design Review · 设计复审 (和同事审核设计文档) 20 5
    · Coding Standard · 代码规范 (为目前的开发制定合适的规范) 20 5
    · Design · 具体设计 20 120
    · Coding · 具体编码 2160 1440
    · Code Review · 代码复审 30 60
    · Test · 测试(自我测试,修改代码,提交修改) 160 100
    Reporting 报告 95 100
    · Test Report · 测试报告 30 60
    · Size Measurement · 计算工作量 5 10
    · Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 2025
    合计 2585
  • 相关阅读:
    成为 Team Leader 后我最关心的那些事
    《管理的实践》读书心得
    玩黑客学校CTF
    DHCP中继器
    test
    初窥XSS跨站脚本攻击
    TCP/IP模型
    逻辑漏洞-客户端验证的邮箱-Web渗透实例之中国教育部青少年普法网站逻辑漏洞
    逻辑漏洞-支付风险-大疆某处支付逻辑漏洞可1元买无人机
    逻辑漏洞-密码找回之验证码发给了客户端
  • 原文地址:https://www.cnblogs.com/donemeb/p/7670605.html
Copyright © 2011-2022 走看看