结对编程——中小学数学卷子自动生成程序队友项目互评
一、简介
- 项目名称:中小学数学卷子自动生成程序
- 编程语言:JAVA
- 完成情况:很好地完成了项目的需求,功能完整,同时增加了一些在实际使用中可能需要的其他功能,例如管理员管理以及注册新的用户,同时每个页面的提示信息都非常完整,在不需要他人协助的情况下,可以完全根据屏幕上的指示信息进行操作,即使用户输入了错误的不相关的指令也可以显示对应的提示信息。
- 以下是对结对编程队友海日娜同学个人项目的代码分析。
二、项目需求分析
- 用户登录:命令行输入用户名和密码,如果用户名和密码都正确,则登录成功。
- 难度切换:通过输入切换为xx(XX为小学、初中和高中三个选项中的一个)可以切换题目难度。
- 自动生成试卷:当输入操作数后可输出对应难度的题目,且生成的题目将以“年-月-日-时-分-秒.txt”的形式保存,每个账号一个文件夹。每道题目有题号,每题之间空一行。
三、代码主体架构
队友代码分了三个类,结构非常清晰,在组之类的结构时也考虑到了面向对象的特点。
- User.java:定义了User类来对用户进行定义
- FileLoad类: 用于读取文件信息
- Main:主函数,用来运行整个项目
四、核心代码分析
1. User类:
定义了User类,并设置了set()和get()函数,还增加了转换成字符串的函数,方便调取用户的信息,还增加了一个match的函数用来判断用户名与密码是否匹配。
以下是部分代码
toString()函数可以便捷地打印用户信息。
public String toString() {
return name + ' ' + pwd + ' ' + type;
}
matchpwd()函数可以对用户的数据进行匹配,可以验证登录用户时的密码是否填写正确。
boolean matchpwd(String name, String pwd) {
if (name.equals(this.getName()) && pwd.equals(this.getPwd())) {
return true;
} else {
return false;
}
}
2. FileLoad类:
使用list存储txt文件中的用户数据,在对用户进行管理时可以更加的高效和便捷,同时使用空格符号分隔txt文件信息时增加了一个对数组的长度判断,可以防止当用户信息出现错误时,造成数组越界的现象。同时还使用了抛出异常的操作,提醒了方法的使用者可能要抛出的异常。
以下是部分代码
Load()函数用来将.txt文件中的内容储存到列表中,便于后续登录用户或者增添用户,同时将用户信息储存在list中也优化了程序的使用性能,不用每次都遍历一遍.txt文件来获取用户信息。
public void load(InputStream in) throws IOException {
// 将获取的字节流转换为字符流并包装成缓冲流
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line = "";
while ((line = br.readLine()) != null) {
User user = parseTxt(line);
list.add(user);
}
}
parseTxt()函数用来分隔每一行的用户信息,同时增加了一个if判断,用来检测临时储存用户信息的数组的长度,如果用户数据出现差错,例如出现“张三1 高中”这样的情况时,如果不增加该if判断则会导致数组越界进而造成程序崩溃。
private User parseTxt(String line) {
User user = new User();
String[] data = line.split(" ");
int b = 0;
b = data.length;
if (b >= 2) {
user.setName(data[0]);
user.setPwd(data[1]);
user.setType(data[2]);
}
return user;
}
load()函数用来加载数据并解析过程中IO错误。
public void load(String fileName) throws IOException {
File f = new File(fileName);
InputStream is = new FileInputStream(f);
load(is);
}
Main:
Main作为该程序的主体,有着很多设计巧妙的地方。
在Main中首先有一个menu()方法展示了首页的大部分信息,函数多次使用try...catch方法.,保证当程序发生错误的时候能继续执行下去,不会立即崩溃。
在读取文件创建文件夹的时候,使用了相对路径,使得程序源码在不同的主机上可以正常的创建文件运行。
FileLoad fileload = new FileLoad();
fileload.load("users/users.txt");
在输入操作指令cur时,增加了try,catch来捕获异常,由于代码编写为cur = cin.nextInt,如果不增加try,catch方法,当用户输入其它类型的数据时会造成程序直接崩溃。
int cur = 0;
try {
cur = cin.nextInt();
} catch (Exception e) {
System.out.println("输入有误,请重试");
menu();
}
signup()函数为用户注册的函数,同时队友考虑到了用户名和密码中不能含有空格的隐藏bug,如果用户名或者密码中存在空格将会导致用户信息读取错误,当用户输入注册的用户名时,可以根据之前储存在list中的内容高效的识别,如果此处使用哈希表来储存数据可以更高效的实现用户的重复判断,但考虑到该程序系统用户数量偏少,使用list也是一个不错的想法。
Scanner cin = new Scanner(System.in);
System.out.print("请输入用户名:");
String newUser = cin.nextLine();
while(newUser.contains(" ")) {
System.out.println("\n** 用户名不可含有空格,请重新输入! **");
newUser = cin.nextLine();
}
boolean flag = true;
for (User user : list) {
if (newUser.equals(user.getName())) {
System.out.println("\n** 用户名重复,请重新输入! **");
flag = false;
signup();
break;
}
}
Login()函数为程序的主要函数之一,实现了用户的登录功能,队友使用了toCharArray的方法来分隔字符串,以达到读取用户信息的目的,此处也可以使用String类中split()方法,可以更简便的实现数据的分隔。
int j = 0;
for (int i = 0; i < str1.length; i++) {
if (str1[i] == ' ') {
j = i;
break;
}
}
modifyType()函数中实现了用户出题类型的切换,该代码片段考虑到了程序的直观性,以及用户使用的便捷性,用户既可以输入汉字切换题目类型,也可以输入序号来切换题目类型,大大得便利的用户的使用。
if (str.equals("切换为小学") || str.equals("0")) {
type = "小学";
} else if (str.equals("切换为初中") || str.equals("1")) {
type = "初中";
} else if (str.equals("切换为高中") || str.equals("2")) {
type = "高中";
} else {
System.out.println("请输入小学、初中和高中三个选项中的一个");
modifyType(name, type);
}
menu2(name, type);
getProblem()函数是生成题目的函数,也是本程序最难的一个地方。队友根据小学、初中、高中运算符号的逐级递增,将三种类型分开考虑。总体思路是先随机生成任意操作数,并将其转换成字符串数组,便于增加特殊符号。对于初中题目则以50%概率将操作数后加“2”,另50%概率将操作数前加“根号”,以此类推。
static String getProblem(String type) {
String signs[] = new String[] { "+", "-", "*", "/" };
Random random = new Random();
int cnt = 0; //操作数个数
int specCnt = 0; //特殊符号个数
String[] numbers = null;
String problem = "";
switch (type) {
case "小学":
cnt = random.nextInt(4) + 2;
numbers = new String[cnt];
for (int i = 0; i < cnt; i++) {
numbers[i] = String.valueOf(random.nextInt(100) + 1);
}
break;
case "初中":
cnt = random.nextInt(5) + 1;
numbers = new String[cnt];
for (int i = 0; i < cnt; i++) {
numbers[i] = String.valueOf(random.nextInt(100) + 1);
}
specCnt = random.nextInt(cnt) + 1; //特殊符号
boolean[] flag = new boolean[cnt];
for (int i = 0; i < specCnt; i++) {
int j = random.nextInt(cnt);
if (!flag[j]) {
if (random.nextBoolean()) {
numbers[j] = numbers[j] + "²";
} else {
numbers[j] = "√" + numbers[j];
}
flag[j] = true;
}
}
break;
case "高中":
cnt = random.nextInt(5) + 1;
numbers = new String[cnt];
for (int i = 0; i < cnt; i++) {
numbers[i] = String.valueOf(random.nextInt(100) + 1);
}
specCnt = random.nextInt(cnt) + 1; //特殊符号
flag = new boolean[cnt];
for (int i = 0; i < specCnt; i++) { //选一个加特殊符号
int j = random.nextInt(cnt); //加特殊符号的数字
if (i == 0) {
int choice = random.nextInt(3);
if (choice == 0 && !numbers[j].contains("°")) {
numbers[j] = "sin" + numbers[j] + "°";
} else if (choice == 1 && !numbers[j].contains("°")) {
numbers[j] = "cos" + numbers[j] + "°";
} else if (choice == 2 && !numbers[j].contains("°")) {
while(numbers[j] == "90") { //tan90不存在
j = random.nextInt(cnt);
}
numbers[j] = "tan" + numbers[j] + "°";
}
} else {
if (!flag[j]) {
int choice = random.nextInt(5);
if (choice == 0) {
numbers[j] = numbers[j] + "²";
} else if (choice == 1) {
numbers[j] = "√" + numbers[j];
} else if (choice == 2 && !numbers[j].contains("°")) {
numbers[j] = "sin" + numbers[j] + "°";
} else if (choice == 3 && !numbers[j].contains("°")) {
numbers[j] = "cos" + numbers[j] + "°";
} else if (choice == 4 && !numbers[j].contains("°")) {
while (numbers[j] == "90") { //tan90不存在
j = random.nextInt(cnt);
}
numbers[j] = "tan" + numbers[j] + "°";
}
flag[j] = true;
}
}
}
break;
default:
break;
}
接下来加括号,分为含两个、三个及四个数字的括号,分别考虑合法位置的情况,实现括号随机生成与多层嵌套。最后再将括号和符号与之前的操作数数组合并,从而生成题目。
int left[] = new int[cnt];
int right[] = new int[cnt];
int j = 0;
//含两个数的括号
int two1 = 10; //第一个左括号起始数字
int two2 = 10; //第二个
if (cnt > 2) {
j = random.nextInt(2);
if (j == 0){ //生成第一个小括号
two1 = random.nextInt(cnt - 1);
left[two1] += 1; //two1左括号
right[two1 + 1] += 1; //two1+1右括号
}
if(cnt > 3) {
//增加一个随机数选择是否生成第二个小括号
j = random.nextInt(2);
if (j == 0) {
two2 = random.nextInt(cnt - 1);
if ((two2 >= (two1 + 2)) || (two2 <= (two1 - 2))) {
left[two2] += 1;
right[two2 + 1] += 1;
}
}
}
}
//含三个数的括号
int three = 0;
if (cnt > 3) {
j = random.nextInt(2);
if (j == 0) {
three = random.nextInt(cnt - 2);
if (((two1 - 2) != three) && ((two1 + 1) != three)) {
if (((two2 - 2) != three) && ((two2 + 1) != three)) {
left[three] += 1;
right[three + 2] += 1;
}
}
}
}
//含四个数的括号
int four = 0;
if (cnt > 4) {
j = random.nextInt(2);
if (j == 0) {
four = random.nextInt(2);
if (four == 0 && right[4] == 0) {
left[four] += 1;
right[four + 3] += 1;
} else if (four == 1 && left[0] == 0) {
left[four] += 1;
right[four + 3] += 1;
}
}
}
//将数字与括号合并
for (int i = 0; i < cnt - 1; i++) {
while (left[i] != 0) {
numbers[i] = "(" + numbers[i];
left[i] -= 1;
}
while (right[i] != 0) {
numbers[i] = numbers[i] + ")";
right[i] -= 1;
}
}
while (right[cnt - 1] != 0) {
numbers[cnt - 1] = numbers[cnt - 1] + ")";
right[cnt - 1] -= 1;
}
//将数字与符号合并
for (int i = 0; i < cnt - 1; i++) {
problem += numbers[i] + signs[i];
}
problem += numbers[cnt - 1] + "=";
return problem;
}
五、代码规范
- 代码风格符合Google编码规范,编码采用utf-8的标准,缩进、运算符和空格使用符合标准,函数定义采用域运算符。
- 类名和函数名可以体现其对应的功能,注释较为详细,阅读起来十分流畅。
- 代码简洁美观,复用性较好。
六、功能测试与验证
1、运行程序,显示主菜单
2、输入2进入注册页面,根据页面提示信息输入用户名以及密码,同时测试是否可以对不相干的输入进行识别,例如我这里输入了账户类型为大学,页面出现提示信息,请输入小学等选项中的一个,重新输入高中,账号注册成功。
3、返回主菜单进入用户登录页面
4、输入账号密码,使用空格分开,成功登录。
5、输入1切换题目类型。
6、输入切换为小学,将用户类型切换为小学。
7、输入2生成题目,再次输入题目数量为30,显示出题完成。
再次切换题目类型,分别生成初中高中题目文件。
题目符号规范,且生成的括号符合要求。高中和初中的难度下分别设置了一个操作数的计算题。
七、交互方式
- 控制台操作,不同的页面会产生相应的提示信息,具有良好的用户体验;
- 整体逻辑清晰架构完整,便于拓展其他的功能,部分代码可以重新设计,提升代码复用率。
八、优点
- 整体代码结构清晰,阅读起来十分的流畅,功能分块,各个模块可以互相调用,可拓展性强。
- 预防错误输入,当用户输入错误信息时可以提供相应的提示信息。
- 考虑了三角函数通常与角度值所对应给三角函数所带的参数加上了角度的符号
- 生成的括号随机且正确,由于是先产生合法的括号再插入到题目中,所以减少了再次验证括号的步骤。同时生成的题目完全符合要求,不同的难度出题效果极佳。还考虑到了tan90°不存在以及cos90°不能作除数的特殊情况,规避了很多的错误。
- 用户体验好,根据控制台输出的提示信息可以流畅地完成从用户登录到生成试卷的过程,同时在完全实现题目要求的内容后还扩展了注册用户的功能。
- 生成试卷时输出到预设的相对路径,可以保证代码在不同的主机上直接运行。
九、缺点
- 在检查题目是否重复时可以使用哈希表来存储题目信息,这样可以减少遍历文件的次数,从而大大提升程序的效率。
- Main中的函数较多,个别可以独自封装成类,使调用函数更方便,减少可能的模块间干扰,从而提高程序的可复用性和可维护性。