一、代码复审Check List
1.概要部分
- 代码能符合需求和规格说明么?
1)规格部分
输出规格:
用一个文本文件(假设名字叫 sudoku.txt)的形式保存起来,每次生成的txt文件需要覆盖上次生成的txt文件,文件内的格式如下,数与数之间由空格分开,终局与终局之间空一行,行末无空格
-s 实际输出:
4 7 8 9 1 6 2 5 3 1 5 9 8 2 3 7 6 4 2 6 3 5 4 7 8 9 1 7 4 6 1 3 5 9 8 2 8 9 1 4 6 2 5 3 7 5 3 2 7 8 9 1 4 6 3 8 7 6 5 1 4 2 9 6 1 5 2 9 4 3 7 8 9 2 4 3 7 8 6 1 5 4 7 8 9 1 6 2 5 3 1 5 9 8 2 3 7 6 4 2 6 3 5 4 7 8 9 1 7 4 6 1 3 5 9 8 2 8 9 1 4 6 2 5 3 7 5 3 2 7 8 9 1 4 6 3 8 7 6 5 1 4 2 9 6 1 5 2 9 4 3 7 8 9 2 4 3 7 8 6 1 5 4 7 8 9 1 6 2 5 3 1 5 9 8 2 3 7 6 4 2 6 3 5 4 7 8 9 1 7 4 6 1 3 5 9 8 2 8 9 1 4 6 2 5 3 7 5 3 2 7 8 9 1 4 6 3 8 7 6 5 1 4 2 9 6 1 5 2 9 4 3 7 8 9 2 4 3 7 8 6 1 5 ...
-c 实际输出:
3 1 2 7 8 9 4 5 6 4 5 6 3 1 2 7 8 9 7 8 9 4 5 6 3 1 2 2 3 1 9 7 8 6 4 5 6 4 5 2 3 1 9 7 8 9 7 8 6 4 5 2 3 1 1 2 3 8 9 7 5 6 4 5 6 4 1 2 3 8 9 7 8 9 7 5 6 4 1 2 3 3 1 2 7 9 8 4 5 6 4 5 6 3 1 2 7 9 8 7 9 8 4 5 6 3 1 2 2 3 1 8 7 9 6 4 5 6 4 5 2 3 1 8 7 9 8 7 9 6 4 5 2 3 1 1 2 3 9 8 7 5 6 4 5 6 4 1 2 3 9 8 7 9 8 7 5 6 4 1 2 3 3 1 2 8 7 9 4 5 6 4 5 6 3 1 2 8 7 9 8 7 9 4 5 6 3 1 2 2 3 1 9 8 7 6 4 5 6 4 5 2 3 1 9 8 7 9 8 7 6 4 5 2 3 1 1 2 3 7 9 8 5 6 4 5 6 4 1 2 3 7 9 8 7 9 8 5 6 4 1 2 3 ...
2) 输入规格:
对于命令:
a. -c 100,结果如上
b. -c 0,
number should be positive
c. -c abc
invalid number
d. -c
invalid parameter number
e. -s XXXXliuchang_reviewSudokusubject.txt
输入内容:
4 7 0 9 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0 1 0 0 6 0 0 0 0 0 0 8 0 1 0 0 2 5 0 0 0 0 0 7 0 0 0 4 0 0 0 0 0 5 1 0 0 0 6 0 0 0 0 0 0 7 0 0 0 0 0 0 8 0 0 0 4 7 0 9 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0 1 0 0 6 0 0 0 0 0 0 8 0 1 0 0 2 5 0 0 0 0 0 7 0 0 0 4 0 0 0 0 0 5 1 0 0 0 6 0 0 0 0 0 0 7 0 0 0 0 0 0 8 0 0 0 ...
输出如上提到。
f. -s 0
number should be positive
e. -s abc
invalid number
f. -s
invalid parameter number
g. 无参数
invalid parameter number
h. -abc
unknown function
总体评价:
作为一个程序,能在不崩溃的前提下,处理正确的输入并输出符合规范的结果,处理异常的输入并返回报错信息,规格实现非常完善。
2) 需求部分:
1. 生成数独:
- 生成不重复的数独终局至文件
- 读取文件内的数独问题,求解并将结果输出到文件
- 在生成数独矩阵时,左上角的第一个数为:(学号后两位相加)% 9 + 1。例如学生A学号后2位是80,则该数字为(8+0)% 9 + 1 = 9,那么生成的数独棋盘应如下(x表示满足数独规则的任意数字)
1. 格式和不重复性:
运行 -c 100000,使用字典树存储和查重:
COUNT = 100000 class TrieNode:
"""A Trie tree specialized to store strings of sudoku solutions.""" desc = {} char = "" def __init__(self): self.desc = {} self.char = "" def insert(self, string):
"""
Each time we come to or create a lower node, directed by the first character of 'string'. If len(string) is 1, we assert the node does not exist.
""" if len(string) == 0: return elif len(string) == 1: if len(self.desc) > 0 and string[0] in self.desc: print("Same sudokus.") input() if string[0] not in self.desc: new_node = TrieNode() #print(new_node) self.desc[string[0]] = new_node new_node.char = string[0] self.desc[string[0]].insert(string[1:]) def display(self): for key, node in self.desc.items(): node.display() class Reader: pos = 0 f = None line = 1 def __init__(self, f): self.f = f def match(self, expect=" "): c = self.f.read(1) if c == " ": self.line += 1 if c == expect: return c, True else: return c, False if __name__ == "__main__": f_in = open(r"XXXXliuchang_reviewSudokuSudokusudoku.txt", "r") reader = Reader(f_in) root = TrieNode() for sudoku_count in range(COUNT): if sudoku_count%10000 == 0: print(sudoku_count) sudoku_str = "" for j in range(9): #line for k in range(9): char, v = reader.match() sudoku_str += char if k != 8: char, v = reader.match(" ") else: char, v = reader.match(" ") if not v: print("PE in line "+str(reader.line)) if sudoku_count != COUNT-1: char, v = reader.match(" ") if not v: print("PE in line " + str(reader.line)) assert(sudoku_str[0] == ) root.insert(sudoku_str)
结果:未报重复错误和格式错误。
2. 正确性:
调用check_validity()函数在输出前检查每一行每一列每一宫是否填满1-9。
bool check_validity(int s[9][9]) const{ int record = 0; const int mask = 0x1ff; // 9 ones for (int group_x = 0; group_x < 3; group_x++) { for (int group_y = 0; group_y < 3; group_y++) { record = 0; for (int x = 3 * group_x; x < 3 * group_x + 3; x++) { for (int y = 3 * group_y; y < 3 * group_y + 3; y++) { int v = s[x][y]; record |= 1 << (v-1); } } if (record != mask) { return false; } } } for (int line = 1; line <= 9; line++) { record = 0; for (int row = 1; row <= 9; row++) { int v = s[line][row]; record |= 1 << (v - 1); } if (record != mask) { return false; } } for (int row = 1; row <= 9; row++) { record = 0; for (int line = 1; line <= 9; line++) { int v = s[line][row]; record |= 1 << (v - 1); } if (record != mask) { return false; } } return true; }
结果:
全部正确。
- 代码设计是否有周全的考虑?
如上,针对正确和错误的输入进行了测试,程序保持了鲁棒性,说明有周全的考虑
- 代码可读性如何?
一般。
1) 名字的选择:
举例说明:
a) -c模式中,用于量产数独的模板"Templet"。首先,拼写错误,应该是"Template";第二,什么的template? 我可能会建议叫create_template或者gen(eration)_template。
b) “Templet” 中的函数 fill_line1, fill_line2, fill_line3,大概都是一个建立第一个模板的函数,里面硬编码了模板的一个宫,然后用fill_line去填写整个的模板。fill_line这个名字不知所云,也许改成"build_grip_template”好一点?
c) Template_sudoku 中的成员code。这个是一个全排列变化的自变量,一个长度为9的字符串,叫做code是否不知所云?可能叫seed会好点?
2) 代码的存放:
按照我理解的C++的规范,类的声明应该放在同名的.h中,类的(复杂)方法应该放在对应的同名.cpp中,然而本代码中所有类的声明都放在了Sudoku.h中,定义放在了Sudoku.cpp和puzzle_creator.cpp中,导致浏览非常不方便,需要经常上下滚动查找定义。
3) 代码的结构:
1.生成模式:
面向过程和面向对象的风格夹杂,文件io和算法执行夹杂,导致封装性非常不好。
代码流程解析:
main
读取-c和数字
调用create_sudoku
打开文件
Template_sudoku* tsudo = new Template_sudoku();
Templet* templet = new Templet();
fill_line1();
fill_line2();
fill_line3();
code进行全排列,填充模板
我们可以看到,假如硬编码需要程序运行时生成,是否应该把fill_line1这些函数放在create_sudoku中,把重要的过程放在调用的外层?假如硬编码不需要运行时生成,那么为什么不直接在编译时嵌入?
打开文件和写入文件这类与算法无关的行为,是否应该换个地方来写?打开文件适合放在main中,写入文件应该用专用函数来完成,这类似于“系统调用”的思想。
读入参数和数字这类trivial而比较烦的算法,应该用函数封装。
2. 解数独模式:
main
读入 -s 和文件地址
solve
在写入模式打开"sudoku.txt"
sudoku = new Subject_sudoku(code)
建立9个Group,分别代表行、列、宫。
根据题目把点的值输入到Group中
初始化这些Group
调用fill_sudoku()
获得当前解数量最少的框
去尝试:guess_value()
递归调用fill_sudoku
solve这块的可读性好很多,层次分明,对象初始化函数做了应该做的事情,然后fill_sudoku通过guess_value递归填写数独,逻辑比较清晰,代码简介。命名也比较明晰,比如Group明显代表格子的组,Box虽然可能不够地道(叫digit可能好点)。
4)注释
建议在比较重要的函数开头,把用的算法写出来。代码中有零星的注释,但是感觉作用不是给读者看而是给写程序的人整理思路的。
- 代码容易维护么?
可维护性不是很好。由于读入文件和输出文件都夹在在算法里,所以我们在封装的时候不得不再理解一遍算法才能知道应该把数组格式的输入和输出安排在算法的哪里才能不发生错误。这说明模块之间的独立性差。硬编码的模板信息和用户交互信息散落在函数的各个地方,导致难以查询和修改。
- 代码的每一行都执行并检查过了吗?
作者个人进行了严格的单元测试,对于-c模式进行了1-1000,000的测试,并且检查了数独的合法性以及数独的不重复性。
OpenCppCoverage测试显示代码覆盖率100%。
2. 设计规范部分
- 设计是否遵从已知的设计模式或项目中常用的模式?
C++类设计规范:http://blog.csdn.net/k346k346/article/details/49445137
1. 将类的定义放在头文件中实现。
基本满足,所有类的定义都在Sudoku.h中,但是不同的类没有分文件存放。
2. 尽量将数据成员申明为私有。
不满足。
class Templet { public: string line[9][3]; // line[block][row] const string line1_position_code = "012"; // not change string line2_position_code = "345"; // 3! = 6 types string line3_position_code = "678"; // 3! = 6 types Templet(); void fill_line(const string linetemp[], const string code, const int blockbegin); Templet* fill_line1(); Templet* fill_line2(); Templet* fill_line3(); bool change2next(); void show(); };
3.将成员函数放到类外定义。
满足。
- 有没有硬编码或字符串/数字等存在?
存在。
Templet* Templet::fill_line1() { const string linetemp[3] = { "012", "345", "678" }; fill_line(linetemp, line1_position_code, 0); return this; }
- 代码有没有依赖于某一平台,是否会影响将来的移植(如Win32到Win64)?
不依赖。代码只调用了<iostream>, <vector>等c++标准库,而c++具有跨平台性,因此不会影响移植。
- 开发者新写的代码能否用已有的Library/SDK/Framework中的功能实现?在本项目中是否存在类似的功能可以调用而不用全部重新实现?
不能。部分功能可以调用实现。
for (int i = 0; (c = para[i]) != '