湖南大学结对编程个人项目互评
分析对象:鲁旭同学的个人项目
分析人:王明扬
一、项目需求
用户:
小学、初中和高中数学老师。
功能:
1、命令行输入用户名和密码,两者之间用空格隔开(程序预设小学、初中和高中各三个账号,具体见附表),如果用户名和密码都正确,将根据账户类型显示“当前选择为XX出题”,XX为小学、初中和高中三个选项中的一个。否则提示“请输入正确的用户名、密码”,重新输入用户名、密码;
2、登录后,系统提示“准备生成XX数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录):”,XX为小学、初中和高中三个选项中的一个,用户输入所需出的卷子的题目数量,系统默认将根据账号类型进行出题。每道题目的操作数在1-5个之间,操作数取值范围为1-100;
3、题目数量的有效输入范围是“10-30”(含10,30,或-1退出登录),程序根据输入的题目数量生成符合小学、初中和高中难度的题目的卷子(具体要求见附表)。同一个老师的卷子中的题目不能与以前的已生成的卷子中的题目重复(以指定文件夹下存在的文件为准,见5);
4、在登录状态下,如果用户需要切换类型选项,命令行输入“切换为XX”,XX为小学、初中和高中三个选项中的一个,输入项不符合要求时,程序控制台提示“请输入小学、初中和高中三个选项中的一个”;输入正确后,显示“”系统提示“准备生成XX数学题目,请输入生成题目数量”,用户输入所需出的卷子的题目数量,系统新设置的类型进行出题;
5、生成的题目将以“年-月-日-时-分-秒.txt”的形式保存,每个账号一个文件夹。每道题目有题号,每题之间空一行;
二、项目结构及代码分析
2.1 项目目录结构
src目录下只有一个包main,所有的类在这个目录下定义。
这个包下有两个接口,LoginService和ExerciseService,LoginImpl和ExerciseImpl分别实现了对应接口中的方法,Output类中定义了主函数。
与src同级的目录有一个user目录,里面对应每个用户建立了一个文件夹,用于保存生成的试卷。
2.2代码分析
2.2.1 Output.java文件
里面定义了主函数,主要作用是通过调用在实现类中已经写好的方法生成用户交互页面,处理用户的输入。
可以看到在代码中主要使用了一个循环去保证程序不会中断,系统不会退出,而退出条件是检测到用户输入了0。
在循环内部,首先使用了一个循环检测登录,调用LoginImpl中的方法检测输入的登录账号,只有成功登陆以后才会进入下一步,也就是系统主界面,在此时也可以通过输入0退出系统。程序中还分割了输入的字符串,把用户名当成了函数传给了下面使用的输入检测方法,下面同样使用了循环检测输入,当输入数字10-30之间时会根据类型生成题目,也可以输入切换为XX切换出题类型,输入-1退出主界面,重新进入外部循环,重复上述过程。
总的来说,代码逻辑清晰,功能完整合理,但是这部分代码可以不用写在主函数,可以在LoginImpl实现类中完成。
package main;
import java.io.IOException;
import java.util.Scanner;
/**
* 主函数.
*
* @author lx
* @CreateDate 2023/9/11
* @ProjectDetails [lxPrivateProject]
*/
public class Output {
/**
* 主函数.
*
* @param args 命令行参数
* @author lx
* @date 2023/9/12 13:05
*/
public static void main(String[] args) throws IOException {
System.out.println("请输入用户名密码进行登录,用户名和密码之间用一个空格分割。(输入0退出系统)");
LoginImpl login = new LoginImpl();
Scanner sc = new Scanner(System.in);
String input = sc.nextLine();
int type = 0;
while (!input.equals("0")) {
int user = login.loginCheck(input);
while (login.afterLogin(user)) {
input = sc.nextLine();
user = login.loginCheck(input);
if (input.equals("0")) {
break;
}
}
if (input.equals("0")) {
break;
}
String[] userInfo = input.split(" ");
if (userInfo[0].contains("张三")) {
type = 0;
} else if (userInfo[0].contains("李四")) {
type = 1;
} else {
type = 2;
}
input = sc.nextLine();
if (input.equals("0")) {
break;
}
int result = login.inputCheck(input, userInfo[0], type);
while (result != 3) {
if (result != 4) {
type = result;
}
input = sc.nextLine();
if (input.equals("0")) {
break;
}
result = login.inputCheck(input, userInfo[0], type);
}
}
}
}
2.2.2 LoginService.java文件
这个接口里定义了三个方法分别是登录校验方法LoginCheck,afterLogin方法,用于在登录后输出提示信息,包括登录错误需要重新登陆的信息,inputCheck方法,用于在登录后进入主界面是检测输入,根据不同的输入调用不同的方法并给出提示信息。
package main;
import java.io.IOException;
/**
* 登录校验.
*
* @author lx
* @CreateDate 2023/9/12
* @ProjectDetails [lxPrivateProject]
*/
public interface LoginService {
/**
* 登录校验.
*
* @param input 输入的账号密码
* @return Integer
* @author lx
* @date 2023/9/12 15:09
*/
public Integer loginCheck(String input);
/**
* 用于在输入校验后输出提示信息.
*
* @param in login方法的返回值,表示账号类型
* @return boolean
* @author lx
* @date 2023/9/12 16:27
*/
public boolean afterLogin(Integer in);
/**
* 检查输入是否正确并给出提示.
*
* @param content 输入的内容
* @param fileName 生成的题目要保存的文件夹
* @param type 生成题目类型
* @return Integer
* @author lx
* @date 2023/9/13 20:11
*/
public Integer inputCheck(String content, String fileName, Integer type) throws IOException;
}
2.2.3 LoginImpl.java文件
这个实现类首先定义了一个成员变量,这个对象是ExerciseImpl实现类,因为在这个实现类中需要调用有关练习题的操作。
ExerciseImpl exerciseImpl = new ExerciseImpl();
这个实现类也实现了上面接口里定义的三个方法。
loginCheck方法具体作用是将输入的账号密码与文本文件中以及存储的账号密码进行比较,成功匹配到相同的则说明登录成功,返回具体账号所在行,如果是在2-4行,则说明是小学老师账号,在6-8行则说明是初中老师账号,在10-12行则说明是高中老师账号。如果没有成功匹配到,那么返回值为0。
afterLogin方法传入了一个参数,就是loginCheck方法的返回值,如果是0的话,那么就代表账号密码有错误,给出相应提示,如果密码正确,那么会根据相应传入的值判断是小学初中还是高中,并给出相应登陆成功的提示。
inputCheck函数传入三个参数,分别是输入的内容,试卷保存的文件名,账号类型,这个函数在用户登陆成功进入主界面后调用,content就是主界面输入的内容,文件名就是用户登录的账户名,小学初中高中分别对应type=0,1,2传入。函数中使用了try/catch,首先进入try代码段,尝试把content转换为int类型数据,如果传入的内容不是个整形,那么会触发异常,进入catch代码段,然后再判断content中是否含有关键词“切换为”,如果有,再继续判断是否是切换为XX格式,如果是返回对应type值,如果不是那么输出提示信息。如果不含有“切换为”,那么会提示”请正确输入需求“。而如果没有触发异常,在try代码段中,会判断输入的数字是否在10-30之间,如果是那么调用saveExercise方法生成试卷,如果输入的是-1,那么给出提示信息“已退出登录,请重新输入账号密码”,同时返回3,如果都不是,那么给出提示“输入题目数量在10-30之间,请重新输入”。
package main;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
/**
* 登录相关功能实现.
*
* @author lx
* @CreateDate 2023/9/12
* @ProjectDetails [lxPrivateProject]
*/
public class LoginImpl implements LoginService {
private final ExerciseImpl exerciseImpl = new ExerciseImpl();
@Override
public Integer loginCheck(String input) {
try {
BufferedReader in = new BufferedReader(new FileReader("userinfo.txt"));
String str;
int i = 0;
while ((str = in.readLine()) != null) {
i++;
if (str.equals(input) && i != 1 && i != 5 && i != 9) {
return i;
}
}
} catch (IOException e) {
System.out.println("error");
}
return 0;
}
@Override
public boolean afterLogin(Integer in) {
if (in == 0) {
System.out.println("请输入正确的用户名、密码");
return true;
}
String type;
if (in > 1 && in < 5) {
type = "小学";
} else if (in > 5 && in < 9) {
type = "初中";
} else {
type = "高中";
}
System.out.println("当前选择为" + type + "出题");
System.out.println("准备生成" + type + "数学题目,请输入生成题目数量(输入0退出系统,输入-1将退出当前用户,重新登录,可输入切换为XX切换出题类型):");
return false;
}
@Override
public Integer inputCheck(String content, String fileName, Integer type) throws IOException {
try {
int num = Integer.parseInt(content);
if (num >= 10 && num <= 30) {
exerciseImpl.saveExercise(num, fileName, type);
} else if (num == -1) {
System.out.println("已退出登录,请重新输入账号密码");
return 3;
} else {
System.out.println("输入题目数量在10-30之间,请重新输入");
}
return 4;
} catch (NumberFormatException e) {
if (content.contains("切换为")) {
if (content.equals("切换为小学")) {
System.out.println("准备生成小学数学题目,请输入生成题目数量");
return 0;
} else if (content.equals("切换为初中")) {
System.out.println("准备生成初中数学题目,请输入生成题目数量");
return 1;
} else if (content.equals("切换为高中")) {
System.out.println("准备生成高中数学题目,请输入生成题目数量");
return 2;
} else {
System.out.println("请输入小学、初中和高中三个选项中的一个");
}
} else {
System.out.println("请正确输入需求");
}
return 4;
}
}
}
2.2.4 ExerciseService.java文件
接口中定义了六个方法,exerciseGenerate1,exerciseGenerate2,exerciseGenerate3三个方法分别是用于生成小学初中高中题目,在需要时被调用。addBrackets方法是用于给生成的题目加上括号的,saveExercise方法是用来生成试卷并保存到对应目录的,checkExercise方法是用来对生成的题目进行查重的。
package main;
import java.io.IOException;
/**
* 有关习题操作的方法接口.
*
* @author lx
* @CreateDate 2023/9/12
* @ProjectDetails [lxPrivateProject]
*/
public interface ExerciseService {
/**
* 生成小学题目.
*
* @return String
* @author lx
* @date 2023/9/12 21:11
*/
public String exerciseGenerate1();
/**
* 生成初中题目.
*
* @return String
* @author lx
* @date 2023/9/12 21:11
*/
public String exerciseGenerate2();
/**
* 生成高中题目.
*
* @return String
* @author lx
* @date 2023/9/12 17:17
*/
public String exerciseGenerate3();
/**
* 给出完的题目加括号.
*
* @param exercise 需要加括号的题目
* @return String
* @author lx
* @date 2023/9/14 19:59
*/
public String addBrackets(String exercise);
/**
* 保存生成的题目.
*
* @param num 生成题目数量
* @param fileName 保存目录名
* @param type 0表示小学题目,1表示初中题目,2表示高中题目
* @author lx
* @date 2023/9/12 22:29
*/
public void saveExercise(Integer num, String fileName, Integer type) throws IOException;
/**
* 对生成的题目查重.
*
* @param exercise 生成的题目
* @param fileName 所要保存的目录名
* @return boolean
* @author lx
* @date 2023/9/13 20:27
*/
public boolean checkExercise(String exercise, String fileName);
}
2.2.5 ExerciseImpl.java文件
这个实现类首先定义了两个成员变量
String[] SYMBOL = {"+", "-", "*", "/", "²", "√", "sin", "cos", "tan"};
Random random = new Random();
分别是所用到的操作符和随机数。
这个实现类也实现了上面接口里定义的六个方法。
exerciseGenerate1方法实现了生成小学题目,首先随机一个操作数数量,最少为2个,然后采用for循环,循环次数为操作数数量,循环内部,每次随机生成一个操作数和操作符,将他们拼接起来,最后拼接上“=”。
exerciseGenerate2方法实现了生成初中题目,首先随机一个操作数数量,最少为1个,因为程序中设定了第一个操作数一定是²或者√操作,所以可以只有一个操作数,这样也满足题目中至少有一个平方或者开根号操作的要求。然后采用for循环,循环次数为操作数数量,循环内部,每次随机生成一个操作数和操作符,将他们拼接起来,最后拼接上“=”。
exerciseGenerate3方法实现了生成高中题目,首先随机一个操作数数量,最少为1个,因为程序中设定了第一个操作数一定是sin,cos或者tan操作,所以可以只有一个操作数,这样也满足题目中至少有一个三角函数运算的要求。然后采用for循环,循环次数为操作数数量,循环内部,每次随机生成一个操作数和操作符,将他们拼接起来,最后拼接上“=”。同时这段代码去除了tan90和/cos90这样不合理的操作,如果出现这些操作,那么会重新生成随机数,取代90这个随机数。
addBrackets方法传入了一个参数,就是生成的题目。实现了添加括号功能,当操作数为3个及以上时会尝试添加括号。添加括号的逻辑为随机生成一个左括号和右括号的位置,右括号位置一定>左括号位置,当左右括号分别位于题目前后,也重新生成,生成位置以后采用一个循环,分析如果加上括号以后,内部有哪些操作符,对应flag设置为1,这样下面就可以通过判断特殊情况看是否需要生成括号,当情况类似为-(2-1),/(2/1),(1+2),/(1+2),(1+2)*,(1+2)/,时,需要生成括号,那么就把刚才随机出来的的位置加上括号,返回新生成的题目,如果不是以上情况,那么就不加括号,返回原题目。
saveExercise方法传入了3个参数,分别是试卷要生成的题目数量,要保存的目录名,生成的题目类型。实现了生成试卷,并保存试卷到对应目录下的功能,首先利用了java中的date类生成了试卷,并格式化为yyyy-MM-dd-hh-mm-ss类型,然后拼接上".txt"生成要保存的文件名。然后利用了java.io 包下操作输入、输出的类,将文件保存到绝对路径user/fileName/filename下,fileName就是对应用户名的目录,filename就是刚生成的要保存的文件名。采用for循环,根据传入的类型调用相应的题目生成函数生成指定数量的题目,每次生成题目时,还需要调用checkExercise方法查重,如果重复那么循环重新生成题目,每次生成以后调用addBrackets方法随机添加括号,然后把他们写入对应文件中,完成以后关闭IO流,然后输出“生成成功”。
checkExercise方法传入了两个参数,分别是生成的题目和要保存的目录名。通过目录名拼接要遍历的目录,遍历该目录下的所有文件,查找是否有题目与生成的题目相同,这里直接用的是字符串比较,如果相同那么返回false,这里还有一个特殊情况就是两个操作数时顺序不同也算重复,这个方法通过交换两个操作数的题目中的操作数再进行一次比较,判断该重复的情况,如果都不存在,那么返回true。
package main;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;
import java.util.Random;
/**
* 有关习题操作的方法实现类.
*
* @author lx
* @CreateDate 2023/9/12
* @ProjectDetails [lxPrivateProject]
*/
public class ExerciseImpl implements ExerciseService {
private static final String[] SYMBOL = {"+", "-", "*", "/", "²", "√", "sin", "cos", "tan"};
private final Random random = new Random();
@Override
public String exerciseGenerate1() {
StringBuilder text = new StringBuilder();
int opNum = random.nextInt(4) + 2;
for (int i = 0; i < opNum; i++) {
int op = random.nextInt(4);
int ranNum = random.nextInt(100) + 1;
if (i != opNum - 1) {
text.append(ranNum).append(" ").append(SYMBOL[op]).append(" ");
} else {
text.append(ranNum);
}
}
text.append(" ").append("=");
return text.toString();
}
@Override
public String exerciseGenerate2() {
StringBuilder text = new StringBuilder();
String[] symbol2 = {"²", "√"};
int opNum = random.nextInt(5) + 1;
int op = random.nextInt(2);
int ranNum = random.nextInt(100) + 1;
if (op == 0) {
text.append(ranNum).append(symbol2[op]);
} else {
text.append(symbol2[op]).append(ranNum);
}
if (opNum != 1) {
int r1 = random.nextInt(4);
text.append(" ").append(SYMBOL[r1]);
}
for (int i = 1; i < opNum; i++) {
op = random.nextInt(6);
ranNum = random.nextInt(100) + 1;
if (op >= 4) {
if (op == 4) {
text.append(" ").append(ranNum).append(SYMBOL[op]);
} else {
text.append(" ").append(SYMBOL[op]).append(ranNum);
}
if (i != opNum - 1) {
int r1 = random.nextInt(4);
text.append(" ").append(SYMBOL[r1]);
}
} else {
if (i != opNum - 1) {
text.append(" ").append(ranNum).append(" ").append(SYMBOL[op]);
} else {
text.append(" ").append(ranNum);
}
}
}
text.append(" ").append("=");
return text.toString();
}
@Override
public String exerciseGenerate3() {
StringBuilder text = new StringBuilder();
String[] symbol3 = {"sin", "cos", "tan"};
int opNum = random.nextInt(5) + 1;
int op = random.nextInt(3);
int ranNum = random.nextInt(100) + 1;
while (op == 2 && ranNum == 90) { //去除tan90
ranNum = random.nextInt(100) + 1;
}
text.append(symbol3[op]).append(ranNum);
if (opNum != 1) {
int r1 = random.nextInt(4);
text.append(" ").append(SYMBOL[r1]);
}
for (int i = 1; i < opNum; i++) {
op = random.nextInt(9);
ranNum = random.nextInt(100) + 1;
while (((op == 8) || (op == 7 && text.charAt(text.length() - 1) == '/')) && ranNum == 90) {
ranNum = random.nextInt(100) + 1; //去除tan90和/cos90
}
if (op >= 4) {
if (op == 4) {
text.append(" ").append(ranNum).append(SYMBOL[op]);
} else {
text.append(" ").append(SYMBOL[op]).append(ranNum);
}
if (i != opNum - 1) {
int r1 = random.nextInt(4);
text.append(" ").append(SYMBOL[r1]);
}
} else {
if (i != opNum - 1) {
text.append(" ").append(ranNum).append(" ").append(SYMBOL[op]);
} else {
text.append(" ").append(ranNum);
}
}
}
text.append(" ").append("=");
return text.toString();
}
@Override
public String addBrackets(String exercise) {
String[] num = exercise.split(" ");
int opNum = num.length / 2;
if (opNum > 2) {
int left = random.nextInt(opNum - 1);
int right = random.nextInt(opNum);
while (left >= right || (left == 0 && right == opNum - 1)) {
right = random.nextInt(opNum);
}
int flag = 0;
for (int i = left * 2 + 1; i < right * 2; i++) {
if (Objects.equals(num[i], "+")) {
flag = 1;
}
if (Objects.equals(num[i], "-")) {
flag = 2;
}
if (Objects.equals(num[i], "*")) {
flag = 3;
}
if (Objects.equals(num[i], "/")) {
flag = 4;
}
}
String rightOp = num[right * 2 + 1];
String leftOp = " ";
if (left != 0) {
leftOp = num[left * 2 - 1];
}
if ((flag == 2 && leftOp.equals("-")) || (flag == 4 && leftOp.equals("/"))
|| ((flag == 1 || flag == 2) && (leftOp.equals("*") || leftOp.equals("/")
|| rightOp.equals("*") || rightOp.equals("/")))) {
num[left * 2] = "(" + num[left * 2];
num[right * 2] = num[right * 2] + ")";
StringBuilder result = new StringBuilder();
for (int i = 0; i < num.length; i++) {
result.append(num[i]).append(" ");
}
return result.toString();
}
}
return exercise;
}
@Override
public void saveExercise(Integer num, String fileName, Integer type) throws IOException {
Date dateNow = new Date();
SimpleDateFormat ft = new SimpleDateFormat("yyyy-MM-dd-hh-mm-ss");
String filename = ft.format(dateNow) + ".txt";
File file = new File("user/" + fileName + "/" + filename);
if (!file.exists()) {
file.createNewFile();
}
FileWriter fw = new FileWriter(file);
BufferedWriter bw = new BufferedWriter(fw);
String exercise;
for (int i = 0; i < num; i++) {
if (type == 0) {
exercise = exerciseGenerate1();
while (!checkExercise(exercise, fileName)) {
exercise = exerciseGenerate1();
}
} else if (type == 1) {
exercise = exerciseGenerate2();
while (!checkExercise(exercise, fileName)) {
exercise = exerciseGenerate2();
}
} else {
exercise = exerciseGenerate3();
while (!checkExercise(exercise, fileName)) {
exercise = exerciseGenerate3();
}
}
exercise = addBrackets(exercise);
exercise = (i + 1) + ". " + exercise;
bw.write(exercise);
bw.newLine();
bw.newLine();
bw.flush();
}
bw.close();
fw.close();
System.out.println("生成成功!");
}
@Override
public boolean checkExercise(String exercise, String fileName) {
File file = new File("user/" + fileName);
String[] child = file.list();
String[] num2 = exercise.split(" ");
if (child == null) {
System.out.println("目录不存在或它不是一个目录");
} else {
for (int i = 0; i < child.length; i++) {
try {
BufferedReader in = new BufferedReader(new FileReader("user/"
+ fileName + "/" + child[i]));
String str;
while ((str = in.readLine()) != null) {
if (str.trim().isEmpty()) {
continue;
}
String[] text1 = str.split(" ");
String[] num1 = text1[1].split(" ");
if (num1.length == 4 && num2.length == 4 && num1[0].equals(num2[2])
&& num1[2].equals(num2[0])) {
return false;
}
if (text1[1].equals(exercise)) {
return false;
}
}
} catch (IOException e) {
System.out.println("error");
}
}
}
return true;
}
}
三、程序测试
运行主函数及登录校验
生成题目
切换出题类型
输入-1退出登录
输入0退出系统
验证查重功能
这里修改查重判定代码,让其输出重复
if (num1.length == 4 && num2.length == 4 && num1[0].equals(num2[2])
&& num1[2].equals(num2[0])) {
System.out.println("重复");
return false;
}
if (text1[1].equals(exercise)) {
System.out.println("重复");
return false;
}
这里修改主函数直接测试题目
public static void main(String[] args) throws IOException {
ExerciseImpl exercise=new ExerciseImpl();
exercise.checkExercise("2 + 73 / 44 =","张三1");
exercise.checkExercise("49 / 70 =","张三1");
}
输出结果如下,两个重复,验证查重成功
所有功能测试均没有问题
四、优缺点分析及总结
-
优点
- 代码逻辑清晰,各个方法完成功能单一,便于阅读与理解;
- 代码符合编程规范,每个方法都有javadoc注释;
- 采用了相对路径,保证在所有电脑上均可以使用;
- 功能实现思路清晰,层次分明,添加括号,判断重复等功能都单独提取出来当成一个方法,易于阅读和修改;
- 在实现过程中,充分考虑了实际情况,括号不是完全随机添加,只添加有意义的括号,不会生成像1+(2+3)=这样的题目。
-
缺点
- 可移植性不太好,如果新增用户,那么代码中判断老师类型的代码需要修改,同时需要手动创建对应老师的目录,代码中并没有主动给用户创建一个存放试卷的目录;
- 主函数中代码太多,可以再写一个方法实现界面功能;
- 登录时只采用了简单的判断字符串是否相同的方式,只允许账号和密码之间有一个空格,用户体验感不好;
- 部分方法的实现行数较多,可以再进行拆分一下,这样便于后续修改;
- 部分方法中if语句较多,复用性不强,可以再优化一下。
-
总结
该同学代码整体逻辑清晰,功能完整实现,代码符合编程规范,但是针对这些缺点还是可以继续优化一下。