本博客记录湖南大学2021级软件工程导论个人项目互评与代码分析
成员:
评价人:软件5班 高义林
项目作者:软件5班 谢宇鑫
需求:
- 命令行输入用户名和密码,两者之间用空格隔开(程序预设小学、初中和高中各三个账号,具体见附表),如果用户名和密码都正确,将根据账户类型显示“当前选择为XX出题”,XX为小学、初中和高中三个选项中的一个。否则提示“请输入正确的用户名、密码”,重新输入用户名、密码;
- 登录后,系统提示“准备生成XX数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录):”,XX为小学、初中和高中三个选项中的一个,用户输入所需出的卷子的题目数量,系统默认将根据账号类型进行出题。每道题目的操作数在1-5个之间,操作数取值范围为1-100;
- 题目数量的有效输入范围是“10-30”(含10,30,或-1退出登录),程序根据输入的题目数量生成符合小学、初中和高中难度的题目的卷子。同一个老师的卷子中的题目不能与以前的已生成的卷子中的题目重复;
- 在登录状态下,如果用户需要切换类型选项,命令行输入“切换为XX”,XX为小学、初中和高中三个选项中的一个,输入项不符合要求时,程序控制台提示“请输入小学、初中和高中三个选项中的一个”;输入正确后,显示“”系统提示“准备生成XX数学题目,请输入生成题目数量”,用户输入所需出的卷子的题目数量,系统新设置的类型进行出题;
- 生成的题目将以“年-月-日-时-分-秒.txt”的形式保存,每个账号一个文件夹。每道题目有题号,每题之间空一行;
常见BUG:
- 登录缺少文字提示
- 切换出题类型后无法出卷;
- 每次登录仅能出卷一次
- 退出登录后无法重新登录
- 连续快速出卷导致多套题目写入同一个txt文件
代码架构:
共有6个实例类,1个抽象类
Main
Account
FileOperations
QuestionGenerator——{EasyMathProblemGenerator, NormalMathProblemGenerator, HardMathProblemGenerator}
代码分析:
public static void main(String[] args) {}
public static void main(String[] args) {
BufferedWriter bw;
try {
bw = new BufferedWriter(new FileWriter("./Account.txt"));
bw.write(
"张三1 123 小学\n"
+ "张三2 123 小学\n"
+ "张三3 123 小学\n"
+ "李四1 123 初中\n"
+ "李四2 123 初中\n"
+ "李四3 123 初中\n"
+ "王五1 123 高中\n"
+ "王五2 123 高中\n"
+ "王五3 123 高中");
bw.flush();
bw.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
sysCore();
}
在项目根目录下Account.txt下覆写给定的账号与对应的密码,使用try catch块包裹处理异常,之后进入sysCore方法执行登录等操作。
private static void sysCore() {}
private static void sysCore() {
System.out.println("欢迎登录数学试卷自动生成系统,请输入用户名,密码。输入exit退出系统。");
if (login()) {
System.out.println("准备生成" + currentUser.getMode() + "数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录):");
Scanner input = new Scanner(System.in);
if (generate(input.next())) {
sysCore();
}
}
}
输出提示文字,调用login方法,如果login方法返回为真,输出登录成功后的提示文字,调用generate方法,之后递归调用本函数实现退出登录后的二次登录。
private static boolean login() {}
private static boolean login() {
Scanner fileWriter;
File file = new File("./Account.txt");
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
try {
fileWriter = new Scanner(new FileReader("./Account.txt"));
} catch (FileNotFoundException e) {
System.out.println("请检查文件完整性");
return login();
}
Scanner input = new Scanner(System.in);
String userName = input.next();
if (userName.equals("exit")) {
return false;
}
String userPassword = input.next();
while (fileWriter.hasNext()) {
String user = fileWriter.nextLine();
String[] split = user.split(" ");
if (split[0].equals(userName) && split[1].equals(userPassword)) {
System.out.println("登录成功,当前选择为" + split[2] + "出题,输入‘切换为XX’更改难度(XX为小学、初中和高中三个选项中的一个)");
currentUser.setUserName(split[0]);
currentUser.setMode(split[2]);
return true;
}
}
System.out.println("请输入正确的用户名、密码");
return login();
}
从Account.txt读取的账户信息与scanner读取的输入对比实现登录,登录成功后将账号信息写入Main的静态成员currentUser并返回登录成功
如果输入为exit则返回false回到sysCore方法,如果输入不正确,递归调用login方法实现登录。存在多次登录失败导致爆栈的风险。
private static boolean generate(String order) {}
private static boolean generate(String order) {
if (order.length() == 5) {
String mode = order.substring(3, 5);
if (order.startsWith("切换为")
&& (mode.equals("小学") || mode.equals("初中") || mode.equals("高中"))) {
currentUser.setMode(mode);
System.out.println("准备生成" + currentUser.getMode() + "数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录):");
Scanner in = new Scanner(System.in);
String input = in.next();
if (input.startsWith("切换为")) {
return generate(input);
}
return doGenerate(input);
} else {
System.out.println("请输入'切换为小学/初中/高中'");
Scanner input = new Scanner(System.in);
return generate(input.next());
}
} else if (order.length() < 3) {
return doGenerate(order);
} else {
System.out.println("请输入正确信息");
Scanner input = new Scanner(System.in);
return generate(input.next());
}
}
在sysCore方法中登录成功后调用此方法,本方法主要实现出题模式的切换,或者在输入符合要求的数字时,将题目数量作为参数调用doGenerate方法。
递归调用本方法实现多次模式切换或出题,逻辑简明易懂。
private static boolean doGenerate(String order) {}
private static boolean doGenerate(String order) {
try {
int nums = Integer.parseInt(order);
if (Integer.parseInt(order) == -1) {
return true;
}
if (10 <= nums && nums <= 30) {
if (generateQuestions(currentUser.getMode(), nums)) {
System.out.println("准备生成" + currentUser.getMode() + "数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录):");
Scanner input = new Scanner(System.in);
return generate(input.next());
}
} else {
System.out.println("请输入10~30之间的数字");
Scanner input = new Scanner(System.in);
return generate(input.next());
}
} catch (Exception e) {
System.out.println("请输入正确信息");
Scanner input = new Scanner(System.in);
return generate(input.next());
}
return false;
}
该方法根据提供的参数判断调用generatQuestions方法,或是输出错误信息,之后回到generat方法
private static boolean generateQuestions(String mode, int nums) throws Exception {}
private static boolean generateQuestions(String mode, int nums) throws Exception {
if (10 <= nums && nums <= 30) {
switch (mode) {
case "小学":
easyMathProblemGenerator.generate(nums);
break;
case "初中":
normalMathProblemGenerator.generate(nums);
break;
case "高中":
hardMathProblemGenerator.generate(nums);
break;
}
return true;
} else {
throw new Exception();
}
}
该方法根据提供的参数调用对应类型的实例成员,并抛出调用过程中可能出现的异常
public abstract class QuestionGenerator {}
/** 题目生成抽象类 */
public abstract class QuestionGenerator {
/** 随机数实例 */
public final Random r = new Random();
/** 运算符号 */
public final char[] operator = new char[] {'+', '-', '*', '/'};
/**
* 保存题目信息
*
* @param toSave 需要保存的题目
*/
public void save(StringBuilder toSave) {
LocalDateTime dateTime = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss");
try {
FileOperations.writeQuestions(
".//user//"
+ Main.currentUser.getUserName()
+ "//"
+ Main.currentUser.getMode()
+ "//"
+ dateTime.format(formatter)
+ ".txt",
String.valueOf(toSave));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 查重判断
*
* @param nums 重新生成题目数量
* @param question 被查重题目
* @param i 题号
* @return 返回是否出现重复
*/
public boolean checkQuestion(int nums, StringBuilder question, int i) {
try {
if (FileOperations.checkQuestions(String.valueOf(question))) {
System.out.println("很不幸,出现了重复题目,正在为您重新生成试卷");
this.generate(nums);
return true;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
question.insert(0, Integer.toString(i + 1) + '、');
question.append("\n\n");
return false;
}
/**
* 生成题目抽象方法
*
* @param nums 生成数量
*/
public abstract void generate(int nums);
}
QuestionGenerator类中定义了checkQuestion和save两个方法,分别实现查重和试卷存储的任务。将基础运算符使用静态数组存储并设为final防止重写。
QuestionGenerator抽象类中还定义了generate抽象方法,并在三个继承类中分别实现,设计了合理的代码结构,灵活应用了继承和抽象,实现了三种不同难度的出卷需求。
public class EasyMathProblemGenerator extends QuestionGenerator {}
/** 小学题目生成 */
public class EasyMathProblemGenerator extends QuestionGenerator {
/**
* 实现抽象方法生成小学题目
*
* @param nums 生成题目数量
*/
public void generate(int nums) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
StringBuilder question = new StringBuilder();
for (int i = 0; i < nums; ++i) {
int rd = r.nextInt(7) + 3;
if (rd % 2 == 0) ++rd;
for (int k = 0; k < rd; ++k) {
if (k % 2 == 0) {
question.append(r.nextInt(100) + 1);
} else {
question.append(operator[r.nextInt(4)]);
}
}
if (checkQuestion(nums, question, i)) return;
save(question);
question.delete(0, question.length());
}
System.out.println("试卷已经生成");
}
}
public class NormalMathProblemGenerator extends QuestionGenerator {}
/**
* 初中题目生成
*/
public class NormalMathProblemGenerator extends QuestionGenerator {
/**
* 实现抽象方法生成初中题目
*
* @param nums 生成题目数量
*/
public void generate(int nums) {
StringBuilder question = new StringBuilder();
for (int i = 0; i < nums; ++i) {
int rd = r.nextInt(7) + 3, sum = 0;
if (rd % 2 == 0) ++rd;
for (int j = 0; j < rd; ++j) {
if (j % 2 == 0) {
int num = r.nextInt(100) + 1;
StringBuilder unit = new StringBuilder(Integer.toString(num));
if (j == rd - 1 && sum == 0) {
unit.append("^2");
} else {
if (r.nextInt(2) == 1) {
unit.append("^2");
++sum;
}
if (r.nextInt(2) == 1) {
unit.insert(0, "sqrt(");
unit.append(")");
++sum;
}
}
question.append(unit);
} else {
question.append(operator[r.nextInt(4)]);
}
}
if (checkQuestion(nums, question, i)) return;
save(question);
question.delete(0, question.length());
}
System.out.println("试卷已经生成");
}
}
public class HardMathProblemGenerator extends QuestionGenerator {}
/** 高中题目生成 */
public class HardMathProblemGenerator extends QuestionGenerator {
/**
* 实现抽象方法生成高中题目
*
* @param nums
*/
public void generate(int nums) {
StringBuilder question = new StringBuilder(), toSave = new StringBuilder();
for (int i = 0; i < nums; ++i) {
int rd = r.nextInt(7) + 3, sum = 0;
if (rd % 2 == 0) ++rd;
for (int j = 0; j < rd; ++j) {
if (j % 2 == 0) {
int num = r.nextInt(100) + 1;
StringBuilder unit = new StringBuilder(Integer.toString(num));
if (j == rd - 1 && sum == 0) {
unit.insert(0, "sin(");
unit.append(")");
} else {
if (r.nextInt(3) == 0) {
unit.insert(0, "sin(");
unit.append(")");
++sum;
}
if (r.nextInt(3) == 1) {
unit.insert(0, "cos(");
unit.append(")");
++sum;
}
if (r.nextInt(3) == 2) {
unit.insert(0, "tan(");
unit.append(")");
++sum;
}
}
question.append(unit);
} else {
question.append(operator[r.nextInt(4)]);
}
}
if (checkQuestion(nums, question, i)) return;
save(question);
question.delete(0, question.length());
}
System.out.println("试卷已经生成");
}
}
在小学试卷出卷部分中,为了防止快速出卷导致的多张卷子写入同一txt文件,谢宇鑫同学独创性的添加了1秒的线程延迟,有效解决了这一问题,但是由于急于提交,这一修改并没有应用到初中和高中难度的出卷部分中。
在特殊运算符的添加过程中,他采用随机数判断是否添加运算符,为避免出现小概率的题目不符合要求,设置了特殊条件判断,当该题直到最后一位仍没有出现过特殊运算符,直接为最后一位添加固定的运算符。这也不失为一种巧妙的解决方式。
测试:
经过初步测试没有发现bug,能够正常处理用户名、密码前、中、后的多余空格,能够正常切换试题难度,能够多次出卷,能够出题后再次切换且不报错
理论上因为递归调用可能存在爆栈风险,但是谢宇鑫同学多数采用尾递归方式,有效规避了这一风险,值得学习。
总结:
谢宇鑫同学的代码可读性有所欠缺,但是注释非常完备弥补了这一缺点。
灵活运用了抽象和继承,代码复用程度高。
合理利用private、static、final关键字,封装完善,减少了越权调用。
题目查重部分非常完备,考虑到了多种情况。
明文保存登陆账号、密码存在被篡改危险,有待改善。
标签:题目,nums,int,导论,question,互评,软件工程,new,public From: https://www.cnblogs.com/scarletMoon/p/17717051.html