结对项目-数独程序扩展(要求细化更新)
DeadLine:2017.10.15 3:00pm
我们在第一个作业中,用各种语言实现了一个命令行的生成数独终局和求解数独的小程序。我们看看如果要把我们的小程序升级为能稳定运行,给用户提供服务的软件,应该怎么做。
1.结对项目
第一阶段目标:把生成数独终局与求解数独的功能封装为独立模块,并设计单元测试。
大家的代码都各有特色,大家写的“软件”也有一定的用处。如果现在我们要把这个功能放到不同的环境中去(例如,命令行,Windows图形界面程序,网页程序,手机App),就会碰到困难:许多同学的代码都散落在各个函数中,很难把剥离出来作为一个独立的模块运行以满足不同的需求。
我们看到,不同的代码解决不同层面的问题:
- 有些是计算数据的(例如生成数独)
- 有些是控制输入的(例如scanf,cin,图形界面的输入字段)
- 有些是数据可视化的(例如printf,cout,println,DrawText)
有些则更为特殊,是架构相关的(例如main函数,并不是所有的程序都需要某个特定格式的main)
这些代码的种类不同,混杂在一起对于后期的维护扩展很不友好,所以它们的组织结构就需要精心的整理和优化。
我们希望把生成数独终局/求解数独的功能能独立出来,成为一个独立的模块(class library, DLL, 或其它),这样的话,命令行和GUI的程序都能使用同一份代码。为了方便起见,我们称之为计算核心"Core模块",这个模块至少可以在几个地方使用:
- 命令行测试程序使用
- 在单元测试框架下使用
- 与数据可视化部分结合使用
把计算核心在单元测试框架中做过完备的测试后,我们就可以在算法层级保证了这个模块的正确性。
但我们知道软件并非只有计算核心,实际的软件是交付给最终用户的软件,除了计算核心外,还需要有一定的界面和必要的辅助功能。那么这个Core模块和使用它的其他模块之间是什么关系呢?它们要通过一定的API(Application Programming Interface)来和其他模块交流。这个API接口应该怎么设计呢?为了简单,我们可以从下面的最简单的接口开始:
void generate(int number, int[][] result)
这个函数接受一个整数number
和一个大小为number x 81
的二维数组作为输入,其中number
表示要求生成的数独的个数,result用来存储生成的number
个数独终局。
注意:本次作业不再限定左上角的数字。
假设我们用类Core
封装了这个接口,我们的测试程序可以是非常简单的:
//调用Core中封装好的函数
int result[100][81];
Core.generate(100, result);
bool valid = true;
for (int i = 0; i < 100 ; i += 1)
//对于第i个棋盘,左上角要求固定为1
valid &= (result[i][0]==1);
//我们断言valid为true,即所有生成的数独左上角都符合固定某个数字的要求
Assert(valid==true);
当然,我们这里的判断并不充分,没有验证数独终局本身的特性:每行每列每宫都只能由1-9不重复的数字组成,但同学们在测试时不能这样“偷懒”。
在本次作业中,我们希望大家在个人项目的基础上完成一个数独游戏软件,这个软件会为用户提供如下特色功能:
- 难度区分,每盘游戏用户都可以选择 容易/中等/难 三个等级。
- 提示功能,用户不会的时候可以点击‘提示’,程序就会提示当前空格应该填什么。
- 计时功能,能够保持用户的最佳记录。
为了实现上述软件,我们首先要在个人项目基础上增量改进,实现一个Core模块,并基于Core模块实现在命令行测试程序中支持下述命令行参数(原有命令行参数不变)
-m 参数设定难度
命令行中使用-n和-m参数分别控制生成数独游戏初始盘的数量与难度等级,
sudoku.exe -n [number] -m [mode]
-n和-m参数的限制范围不同,具体约定如下:
- [number]的范围限定为1 - 10000。
- [mode]的范围限定为1 - 3,不同的数字代表了数独游戏的难度级别,如下表所示:
编号 | 级别 |
---|---|
1 | 简单 |
2 | 入门 |
3 | 困难 |
请在博客中说明你对于不同难度级别的严格定义,并说明这样定义的理由。
例如下述命令将生成20个简单级别的数独游戏初始棋盘至文件sudoku.txt
中,挖空的地方用0表示:
sudoku.exe -n 20 -m 1
9 0 8 0 6 0 1 2 0
2 0 7 4 0 1 9 0 8
1 4 6 0 2 0 3 5 0
0 1 2 0 7 0 5 0 3
0 7 3 0 1 0 4 8 2
4 8 0 0 0 5 6 0 1
7 0 4 5 9 0 8 1 6
0 0 0 7 4 6 2 0 0
3 0 5 0 8 0 7 0 9
9 0 0 8 0 0 4 0 0
……
-r 参数设定挖空数量
命令行中使用-n参数控制生成数独游戏初始盘的数量,-r参数控制生成数独游戏初始盘中挖空的数量范围,使用-u参数控制生成数独游戏初始盘的解必须唯一,
sudoku.exe -n [number] -r [lower]~[upper] -u
-r参数的范围限制如下:
- [lower] 的值最小为20,
- [upper] 的值最大为55,
- [upper] >= [lower]。
如果命令行中有-u参数,则生成的数独游戏初始盘的解必须唯一;否则,则对解的数量不做限制。
注意: -m参数不与-r和-u参数同时出现,如果同时出现则提示参数的正确用法。
例如下述命令将生成20个挖空数在20到55之间的数独游戏初始盘至文件sudoku.txt
中,
sudoku.exe -n 20 -r 20~55
下述命令将生成20个挖空数在20到55之间并且解唯一的数独游戏初始盘至文件sudoku.txt
中,
sudoku.exe -n 20 -r 20~55 -u
现在,请同学们在个人项目的基础上进行增量修改,根据以上修改自己的数独项目。
完成上述接口后,我们要把之前程序中实现的其他功能也封装成独立的模块并一一进行测试,比如读取数独题目文件、输出打印等。建议大家在每一步只增量修改一个模块并做测试。这里的测试包括新模块的单元测试与原功能的回归测试。每实现一个新的功能,要保证以前运行正确的例子继续是正确的。通过这样的回归测试,可以保证自己实现的系统始终是满足预定状态约束的。(请看书中关于单元测试,回归测试的内容)在确认修改的功能正确之后再签入代码。
项目要求:
- 在个人项目上增量修改,实现
void generate(int number, int mode, int[][] result)
接口,对于输入的数独游戏的需求数量和难度等级,通过result
返回生成的数独游戏的集合。 - 对这一
generate
接口进行测试,把单元测试代码Push到Github上(注意避免把单元测试的结果Push到Github上)。 - 实现
void generate(int number, int lower, int upper, bool unique, int[][] result)
接口,生成number
个空白数下界为lower
,上界为upper
的数独游戏,并且如果unique
为真,则生成的数独游戏的解必须唯一,计算的结果存储在result
中。 - 对这一
generate
接口进行测试,把单元测试代码Push到Github上(注意避免把单元测试的结果Push到Github上)。 - 实现
bool solve(int[] puzzle, int[] solution)
接口,对于输入的数独题目puzzle
(数独矩阵按行展开为一维数组),返回一个bool
值表示是否有解。如果有解,则将一个可行解存储在solution
中(同样表示为数独矩阵的一维数组展开)。 - 对
solve
接口进行测试,把单元测试代码Push到Github上(注意避免把单元测试的结果Push到Github上)。 - 设计其他部分的接口,按照设计好的接口在个人项目基础上增量修改,同样把单元测试代码Push到Github上。
- 在完成这一阶段的任务之后,使用
git tag step1
标记第一阶段已经完成,并在Push到Github上时使用--tags
参数把tag也推送到Github,例如git push origin --tags
。
博客要求:
- 详细介绍你对于上述
Core
接口的实现,以及你为Core
模块设计的其他接口,并说明UI模块该如何使用这些接口。 - 选择部分单元测试代码发布在博客中,并说明测试的函数,构造测试数据的思路。
- 将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到90%以上,否则单元测试部分视作无效。
第二阶段目标:通过测试程序和API接口测试对于异常处理的支持
在上面我们只讨论了正确的输入下,我们对于程序输出的期待。但如果程序的输入出现了错误,比如命令行参数是“-10000”,或者是“1000000000000c”,你又该怎么办呢?要怎样才能告诉函数的调用者“你错了”?又该如何方便地告诉函数的调用者“哪里错了”?在这种时候,我们一般会定义各种异常(Exception),让Core
在碰到各种异常情况的时候,能给调用者充分的错误信息。当然,我们同样要进行增量修改:
项目要求:
- 设计好异常的种类与错误提示,例如让程序支持“生成数独数量”异常。
- 在
Core
模块中实现抛出异常的功能,并撰写测试用例:传进去一个错误的数独游戏,期望能捕获这个异常。如果没有,测试就报错。 - 回归测试所有以前的功能,保证以前的功能还能继续工作。
- 在完成这一阶段的任务之后,使用
git tag step2
标记第二阶段已经完成,并在Push到Github上时使用--tags
参数把tag也推送到Github。
博客要求:
- 在博客中详细介绍对哪些异常进行了处理以及每种异常的设计目标。
- 每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。
第三阶段目标:从计算模块到可用软件
在个人项目阶段已经有同学做了一些不错的GUI,但大部分同学只是增加了一个界面而已,并不能称得上是开发了一个软件。一个软件不仅需要有负责数据计算的Core
模块,用户体验友好的UI模块,还要有足够的健壮性、详细的运行说明等。
首先要开发一个用户体验友好的UI模块,并把计算核心与用户界面完美地对接起来。
项目要求:
- 新建一个工程,把
Core
核心模块作为一个DLL(动态链接库)引用在新工程中。 - 开发一个UI模块,实现如下需求:
- 随机生成数独游戏的功能,将会从用户体验角度对随机性进行测试。
- 每盘游戏用户都可以选择 容易/中等/难 三个等级,将会从用户体验角度对不同难度的游戏进行测试。
- 用户不会的时候可以在某个空格上点击‘提示’,程序会提示该空格处需要填什么数字。
- 计时功能,能够记录用户解数独棋盘的耗时,并保持用户的最佳记录。
- 将UI模块与
Core
模块对接。 - 在完成这一阶段的任务之后,使用
git tag step3
标记第三阶段已经完成,并在Push到Github上时使用--tags
参数把tag也推送到Github。
博客要求:详细地描述UI模块的设计与两个模块的对接,并在博客中截图实现的功能。
第四阶段目标:界面模块,测试模块和核心模块的松耦合【附加题】
既然各组同学都写了高质量的各个模块,而且模块之间的关系是明确定义的,一致的,那么,小组A的测试模块就可以测试小组B的核心模块;小组C的用户界面模块就可以和小组B的核心模块结合起来,正常运行。对吧?
那么现在,请你(假设为A)寻找另外一个小组(假设为B),与他们交换核心模块与界面模块,并测试一下下面的情况:
- A的核心模块,加上B的测试模块和用户界面模块
- B的核心模块,加上A的测试模块和用户界面模块
项目要求:
根据与合作小组对接过程中出现的问题,寻找并改进模块中的bug。这部分修改需要另开一个新的分支dev-combine
,并Push到Github上。
博客要求:
在博客中指明合作小组两位同学的学号,分析两组不同的模块合并之后出现的问题,为何会出现这样的问题,以及是如何根据反馈改进自己模块的。
第五阶段目标:通过增量修改的方式,改进程序,发布一个真正的软件【附加题】
在完成第四阶段的目标后,可以通过你们小组的界面模块和合作小组的计算模块组合成一个带界面的数独游戏。但它还不是一个完整的软件,你要为数独游戏增加必要的说明与引导步骤,比如怎么玩,如果卡住了怎么办。
项目要求:
把相关代码签入Github一个新的分支dev-product
。
博客要求:
把这个软件发布出来,在博客中发布下载地址。收集至少10位用户的反馈,并说明你在收到反馈后是怎样改进自己的产品的。
2.评分规则
博文部分得分点
博客共五十分
1)在文章开头给出Github项目地址。(1')
2)在开始实现程序之前,在下述PSP表格记录下你估计将在程序的各个模块的开发上耗费的时间。(0.5')
3)看教科书和其它资料中关于Information Hiding, Interface Design, Loose Coupling的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。(5')
4)计算模块接口的设计与实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。(7')
5)阅读有关UML的内容:https://en.wikipedia.org/wiki/Unified_Modeling_Language。画出UML图显示计算模块部分各个实体之间的关系(画一个图即可)。(2’)
6)计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。(3')
7)看Design by Contract, Code Contract的内容:
http://en.wikipedia.org/wiki/Design_by_contract
http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx
描述这些做法的优缺点, 说明你是如何把它们融入结对作业中的。(5')
8)计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到90%以上,否则单元测试部分视作无效。(6')
9)计算模块部分异常处理说明。在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。(5')
10)界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。(5')
11)界面模块与计算模块的对接。详细地描述UI模块的设计与两个模块的对接,并在博客中截图实现的功能。(4')
12)描述结对的过程,提供非摆拍的两人在讨论的结对照片。(1')
13)看教科书和其它参考书,网站中关于结对编程的章节,例如:
http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html
说明结对编程的优点和缺点。
结对的每一个人的优点和缺点在哪里 (要列出至少三个优点和一个缺点)。(5')
14)在你实现完程序之后,在附录提供的PSP表格记录下你在程序的各个模块上实际花费的时间。(0.5')
注:结对小组中两个人发布独立博客,其中2)、3)、5)、7)、13)、14)部分请独立完成,不允许雷同。项目的测试分数两人共享,博客的分数各自独立。附加题的相关要求请按附加题的要求补充在博客中。
程序部分得分点
程序共七十分
源代码管理评分(5'):
该评分主要通过源代码管理中的commit注释信息,增量修改的内容,是否有运行说明,每个阶段是否打上了标签等内容给分。(5')
第一阶段(20'):
该评分将进行这-c -s -n -m -u -r
六个参数的正确性测试
第二阶段(20'):
将针对上述六个参数进行鲁棒性测试,可能测试的内容包括且不限于:
错误的命令、错误的参数、大小写、错误的参数组合、错误的文件格式等。
要求必须正常结束,崩溃不得分。
错误无任何提示,不得分。
错误种类较多,提示合理,得正分。
第三阶段(25'):
1)随机生成数独游戏的功能,将会从用户体验角度对随机性进行测试。(6')
2)每盘游戏用户都可以选择 容易/中等/难 三个等级,将会从用户体验角度对不同难度的游戏进行测试。 (7')
3)用户不会的时候可以在某个空格上点击‘提示’,程序会提示该空格处需要填什么数字。(6')
4)计时功能,能够记录用户解数独棋盘的耗时,并保持用户的最佳记录。(6')
附加题得分点
附加题1(10'):
在结对项目博客中按照阶段四的博客要求添加相应内容(5')
最终的对接效果(5')
附加题2(10'):
在结对项目博客中按照阶段五的博客要求添加相应内容(5')
可玩性(5')
3.测试须知
组织目录
与个人项目类似,在结对项目中我们也会对大家的计算核心进行正确性的自行测试,需要大家遵循一定的规范。所有提交到Github上的项目均需要建立一个名字为BIN的文件夹,里面必须含有计算核心生成的可执行文件与相关的依赖库,请注意以下三点:
- 确保VS产生的临时文件和编译生成临时文件不被加入到Git代码仓库中(使用.gitignore文件管理哪些文件可以被忽略)。
- 确保命令行测试程序的名字为sudoku.exe,确保核心模块的DLL名字为Core.dll。
- 确保生成的棋盘文件sudoku.txt与可执行文件在同一目录下,生成文件时请使用相对路径!
一个示例组织目录如下所示:
/ SudokuProject(工程名字自行指定即可)
/ main.cpp
/ generator.cpp
/ BIN
/ Core.dll(Core模块)
/ Lib.dll(运行需要的[其他]动态链接库文件)
/ sudoku.exe
/ sudoku.txt (运行exe后生成)
参数约定
助教在测试时,将以命令行运行可执行文件的方式进行批量测试,参数及其约定如下:
参数名字 | 参数意义 | 范围限制 | 用法示例 |
---|---|---|---|
-c | 需要的数独终盘数量 | 1-1000000 | 示例:sudoku.exe -c 20 [表示生成20个数独终盘] |
-s | 需要解的数独棋盘文件路径 | 绝对或相对路径 | 示例: sudoku.exe -s game.txt [表示从game.txt读取若干个数独游戏,并给出其解答,生成到sudoku.txt中] |
-n | 需要的游戏数量 | 1-10000 | 示例:sudoku.exe -n 1000 [表示生成1000个数独游戏] |
-m | 生成游戏的难度 | 1-3 | 示例:sudoku.exe -n 1000 -m 1 [表示生成1000个简单数独游戏,只有m和n一起使用才认为参数无误,否则请报错] |
-r | 生成游戏中挖空的数量范围 | 20-55 | 示例:sudoku.exe -n 20 -r 20~55 [表示生成20个挖空数在20到55之间的数独游戏,只有r和n一起使用才认为参数无误,否则请报错] |
-u | 生成游戏的解唯一 | 示例:sudoku.exe -n 20 -u [表示生成20个解唯一的数独游戏,只有u和n一起使用才认为参数无误,否则请报错] |
[新]等价数独
需要注意的是,在个人项目的测试中,我们把等价但不相同的数独也算作了不重复的数独。但因为本次结对项目我们更加注重数独游戏的可玩性,所以在本次项目中,我们会屏蔽通过数字交换得到的等价数独,这部分重复的数独不计入最终结果。何为“通过数字交换得到的数独”呢,请看下面的例子:
9 5 8 3 6 7 1 2 4
2 3 7 4 5 1 9 6 8
1 4 6 9 2 8 3 5 7
6 1 2 8 7 4 5 9 3
5 7 3 6 1 9 4 8 2
4 8 9 2 3 5 6 7 1
7 2 4 5 9 3 8 1 6
8 9 1 7 4 6 2 3 5
3 6 5 1 8 2 7 4 9
将1
和9
的位置调换,可以得到一个新的“有效数独”。
1 5 8 3 6 7 9 2 4
2 3 7 4 5 9 1 6 8
9 4 6 1 2 8 3 5 7
6 9 2 8 7 4 5 1 3
5 7 3 6 9 1 4 8 2
4 8 1 2 3 5 6 7 9
7 2 4 5 1 3 8 9 6
8 1 9 7 4 6 2 3 5
3 6 5 9 8 2 7 4 1
那么每个测试点的最终得分计算公式为:
测试点正确性测试得分 × 不等价数独组数 ÷ 实际需求个数个数
由于只有数独终盘才存在“等价数独”的情况,所以我们将利用开发者提供的“求解数独”命令行接口求解开发者生成的数独游戏,将求解后的终盘作为判断不重复数据比例的依据。比如某个测试点sudoku.exe -n 100 -r 30~40
基准分为10分,通过开发者提供的“求解数独”命令行求解该命令生成的数独游戏后,发现求解出的数独终盘一共有40组(等价的数独都会归类到同一组中),那么最终开发者在该测试点的得分为 40/100 * 10 = 4 分。
请注意,由于开发者的求解数独接口直接关系到数独游戏的评分,请确保sudoku.exe -s puzzle_file_path
的接口依旧有效!
[新]错误处理
我们都知道健壮性对于软件来说是非常必要的,所以本次自动测试我们也会加入各种各样出错情况的测试。助教测试时将会选择不同种类的出错场景,要求开发者程序不会崩溃的情况下,能够尽可能精确报错(就像编译器一样)。你可以有“容错性”的出错设计,但必须输出必要的提示或说明。
4. FAQ
有任何疑问请直接在本博客下留言,我们会尽快回复。
5. 附录
附:PSP 2.1表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | ||
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | ||
· Design Spec | · 生成设计文档 | ||
· Design Review | · 设计复审 (和同事审核设计文档) | ||
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | ||
· Design | · 具体设计 | ||
· Coding | · 具体编码 | ||
· Code Review | · 代码复审 | ||
· Test | · 测试(自我测试,修改代码,提交修改) | ||
Reporting | 报告 | ||
· Test Report | · 测试报告 | ||
· Size Measurement | · 计算工作量 | ||
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | ||
合计 |