个人项目队友代码分析——中小学数学卷子自动生成程序

发布时间 2023-09-19 20:38:08作者: BlueClancy

代码作者:马千凌

评价者: 胡信航

一、项目需求

1.1 用户:

  • 小学、初中和高中数学老师
账户类型 账户 密码
小学 张三1 123
张三2 123
张三3 123
初中 李四1 123
李四2 123
李四3 123
高中 王五1 123
王五2 123
王五3 123

1.2 功能

  • 命令行输入用户名和密码,两者之间用空格隔开;

  • 用户输入所需出的卷子的题目数量,系统默认将根据账号类型进行出题。每道题目的操作数在1-5个之间,操作数取值范围为1-100;

  • 题目数量的有效输入范围是“10-30”(含10,30,或-1退出登录),程序根据输入的题目数量生成符合难度的题目的卷子;

  • 同一个老师的卷子中的题目不能与以前的已生成的卷子中的题目重复;

  • 命令行输入“切换为XX”,XX为小学、初中和高中三个选项中的一个,输入正确后,用户输入所需出的卷子的题目数量,系统新设置的类型进行出题;

  • 生成的题目将以“年-月-日-时-分-秒.txt”的形式保存,每个账号一个文件夹。每道题目有题号,每题之间空一行。

    小学 初中 高中
    难度要求 +,-,*./ 平方,开根号 sin,cos,tan
    备注 只能有+,-,*./和() 题目中至少有一个平方或开根号的运算符 题目中至少有一个sin,cos或tan的运算符

二、项目结构

2.1 整体结构介绍

该同学的个人项目结构比较清晰。在idea为项目提供的最基础的文件之外,还额外添加了output输出目录和JavaDoc目录。整体工程文件思路清晰,让人一目了然。

2.2 主要模块介绍

2.2.1 src

src是存放源代码的文件夹,可以看到,该同学的src下有3个包,solution,test和user,还有一个单独存放的Main.java

solution包下有2个类,FuctionsInterface是接口,用来定义一些抽象方法,而Functions则是此接口的实现类。在该同学的设计中,Functions包含了整个登录和出题所需要的方法。

user包下共有5个文件,包括一个记录账号的文本文件Accounts.txt,一个抽象类User,和3个继承类Junior,Primary,Senior 分别代表初中,小学和高中老师类。每个难度的题目生成也放在这3个类中。

test包下含有1个test类,这个类是用于查重功能的测试。

此外src目录下还有Main类用于运行文件

2.2.2 output

在output目录下,存放的是以用户名为名称的文件夹,不同的文件夹为不同的用户保存每一次出的试卷,以文本的形式存储,并且此处文件夹是动态可生成的。一个用户在出题后会自动添加对应的文件夹和试卷。

2.2.3 javadoc

Javadoc文件夹是idea根据使用者在项目中添加的javadoc注释自动生成的。点击index.html 即可查看JavaDoc API 文档。

image-20230919112510444 image-20230919112510444

2.3 程序运行流程

三、具体代码分析

3.1 User类

public abstract class User {
  private String username;
  private int rank;
  private String school;

User作为一个抽象类,代表了所有等级的老师。

它包含3个变量,username表示用户名,rank代表学校水平(用int表示),school代表学校水平(用中文表示),此处设计可以再改进,只用一个int即可,可添加一个方法使rank转化为String,不然可能会添加内存的占用。

3.1.1 构造方法 User()

public User(String name, int rk) {
  username = name;
  rank = rk;
  if (rk == 1) {
    school = "小学";
  } else if (rk == 2) {
    school = "初中";
  } else if (rk == 3) {
    school = "高中";
  }
}

该方法按照传入的用户名、学校分级构造新的User对象。根据传入的int不同,school的取值也相应发生改变。

3.1.2 基础方法 getRank,getSchool,getUsername

此3个方法用于获取对应信息

3.1.3 抽象方法 generateSingleQuiz

public String generateSingleQuiz() {
    return null;
  }

此方法用于生成符合要求的单条题目。

3.2 Primary 小学老师类

public class Primary extends User {

  String operators = "+-*/";  // 四则运算运算符

  public Primary(String name) {
    super(name, 1);
  }

此类包含1个成员变量operators,用来记录四则运算运算符

构造函数使用super调用父类User的构造函数

3.2.1 generateSingleQuiz()

  @Override
  public String generateSingleQuiz() {
    int leftBr = -1; //左括号位置
    int rightBr = -1;  //位置右括号
    Random random = new Random();
    int numOperands = random.nextInt(4) + 2; // 随机生成2到5个操作数
    StringBuilder problem = new StringBuilder();
    for (int i = 0; i < numOperands; i++) {
      char operator = operators.charAt(random.nextInt(4)); // 随机选择加减乘除操作符
      int operand = random.nextInt(100) + 1; // 随机生成1到100的操作数
      problem.append(operator).append(" ");
      // 无左括号、非最后一个操作数的情况下有50%生成左括号
      if (((leftBr == -1) && (i > 0) && (i <= numOperands - 2) && random.nextInt(2) == 1)
              && (problem.charAt(problem.length() - 2) != '/')) {
        problem.append("(");
        leftBr = i; //记录左括号位置
      }
      problem.append(operand);
      // 有左括号、无右括号、括号间至少包含2个元素时有50%生成右括号;若有左括号、无右括号且已经是式子最后则直接加上右括号
      if ((((leftBr != -1) && (rightBr == -1) && (i >= leftBr + 1) && (random.nextInt(2) == 1))
              || ((leftBr != -1) && (rightBr == -1) && (i == numOperands - 1)))
              && (problem.charAt(problem.length() - 2) != '/')) {
        problem.append(")");
        rightBr = i;  //记录右括号位置防止左括号不闭合
      }
      problem.append(" "); // 每一组均以操作数加操作符的风格添加到problem
    }
    problem.append("=");
    return problem.substring(1);
  }
}

该代码用于生成单个小学题目。

这段代码的思路是先调用random,随机生成操作数的个数,再进入for循环,在每次for循环中,都会用random生成1-100的数字和一个0-3的数以选择操作符,先加入符号,再加入数字,每次循环都以符号加数字的组合加入问题。此外,还通过一系列条件来判断左右括号是否生成并随机选择生成的位置。并且保证左右括号能闭合。最后使用substring来截取生成的问题,去除最前方操作数的符号。

题目生成的这段代码,思路比较明确,按照题目从前到后依次生成,逻辑上比较流畅。

3.3 Junior 初中老师类

3.3.1 generateSingleQuiz()

  @Override
  public String generateSingleQuiz() {
    Random random = new Random();
    int numOperands = random.nextInt(5) + 1;
    int leftBr = -1; //左括号位置
    int rightBr = -1;  //右括号位置
    boolean hasPowerOperation = false;  //是否有幂操作
    StringBuilder problem;
    do {
      problem = new StringBuilder();
      for (int i = 0; i < numOperands; i++) {
        char operator = operators.charAt(random.nextInt(4));
        int operand = random.nextInt(100) + 1;
        problem.append(operator).append(" ");
        // 无左括号、非最后一个操作数的情况下有50%生成左括号
        if (((leftBr == -1) && (i > 0) && (i <= numOperands - 2) && random.nextInt(2) == 1)
                && (problem.charAt(problem.length() - 2) != '/')) {
          problem.append("(");
          leftBr = i; //记录左括号位置
        }
        int powerOperationIndex = random.nextInt(6);
        if (powerOperationIndex >= 4) { // 1/6概率生成平方,1/6概率生成次方
          hasPowerOperation = true;
          if (powerOperationIndex == 4) {
            problem.append(operand).append("² ");
          } else {
            problem.append("√(").append(operand).append(") ");
          }
        } else {  // 2/3概率不使用幂运算
          problem.append(operand).append(" ");
        }
        // 有左括号、无右括号、括号间至少包含2个元素时有50%生成右括号;若有左括号、无右括号且已经是式子最后则直接加上右括号
        if ((((leftBr != -1) && (rightBr == -1) && (i >= leftBr + 1) && (random.nextInt(2) == 1))
                || ((leftBr != -1) && (rightBr == -1) && (i == numOperands - 1)))
                && (problem.charAt(problem.length() - 2) != '/')) {
          problem.deleteCharAt(problem.length() - 1).append(") ");
          rightBr = i;  //记录右括号位置防止左括号不闭合
        }
      }
    } while (!hasPowerOperation); // 如果生成的题目没有幂运算则重新生成
    problem.append("=");
    return problem.substring(2);  // 移除开头的多余运算符并返回
  }
}

在此类中,唯一有所不同的是generateSingleQuiz函数,在初中题目的生成中,添加了平方和根号的运算。这里作者使用了一个0-5的随机数来控制是否生成根号和平方,保证了每个操作数1/6概率生成平方,1/6概率生成根号,并用一个哨兵变量来检查这个问题中是否有幂次的运算。如此,就以一种非常巧妙的方法得到了带有幂次运算的题目。

3.4 Senior 高中老师类

3.4.1 generateSingleQuiz()

@Override
  public String generateSingleQuiz() {
    Random random = new Random();
    int numOperands = random.nextInt(5) + 1;
    int leftBr = -1; //左括号位置
    int rightBr = -1;  //右括号位置
    boolean hasPowerOperation = false;  //是否有幂操作
    StringBuilder problem;
    do {
      problem = new StringBuilder();
      for (int i = 0; i < numOperands; i++) {
        char operator = operatorsFour.charAt(random.nextInt(4));
        int operand;
        do {  // 不生成90防止在cos,tan的运算中出错
          operand = random.nextInt(100) + 1;
        } while (operand == 90);
        problem.append(operator).append(" ");
        // 无左括号、非最后一个操作数且不在除号后面时的情况下有50%生成左括号
        if (((leftBr == -1) && (i > 0) && (i <= numOperands - 2) && random.nextInt(2) == 1)
                && (problem.charAt(problem.length() - 2) != '/')) {
          problem.append("(");
          leftBr = i; //记录左括号位置
        }
        int powerOperationIndex = random.nextInt(7);
        if (powerOperationIndex < 3) { // 3/7概率生成三种三角函数
          hasPowerOperation = true;
          problem.append(operatorsTrig[powerOperationIndex]).append(operand).append("°) ");
        } else {  // 4/7概率不使用三角运算
          problem.append(operand).append(" ");
        }
        // 有左括号、无右括号、括号间至少包含2个元素时有50%生成右括号;若有左括号、无右括号且已经是式子最后则直接加上右括号
        if ((((leftBr != -1) && (rightBr == -1) && (i >= leftBr + 1) && (random.nextInt(2) == 1))
                || ((leftBr != -1) && (rightBr == -1) && (i == numOperands - 1)))
                && (problem.charAt(problem.length() - 2) != '/')) {
          problem.deleteCharAt(problem.length() - 1);
          problem.append(") ");
          rightBr = i;  //记录右括号位置防止左括号不闭合
        }
      }
    } while (!hasPowerOperation); // 如果生成的题目没有三角运算则重新生成
    problem.append("=");
    return problem.substring(2);  // 移除开头的多余运算符并返回
  }
}

同理,在Senior中的generateSingleQuiz函数也是使用了类似的方法,使用一个随机生成数来控制三角函数出现概率,每个操作数有3/7概率生成三种三角函数,4/7概率不使用三角运算。

3.5 Functions 用户交互、数据处理类

public class Functions implements FunctionsInterface {
    
  public static User user;
    
  public static String pathname = "src/User/Accounts.txt";

Functions 类实现FunctionsInterface接口内的方法。

它包含2个变量,user代表了当前用户,而pathname则代表文件保存的路径。

3.5.1 checkExistence()

@Override
public int checkExistence(String username) {
  try (FileReader reader = new FileReader(pathname);
       BufferedReader bufferedReader = new BufferedReader(reader)
  ) {
    String line;
    while ((line = bufferedReader.readLine()) != null) {  // 以行为单位遍历账号文件
      String[] info = line.split("\t"); // 将读取的每行切分为账号、密码、学校分级
      if (username.equals(info[0])) { // 存在匹配账号直接返回1
        return 1;
      }
    }
  } catch (IOException e) {
    e.printStackTrace();
  }
  return 0; //找不到账号则返回0
}

该函数用于检查用户是否存在。

通过访问相对路径,函数访问 src/User文件夹下的Accounts.txt文件,通过FileReader,逐行读取用户信息,并使用split函数分隔每行,将其分为账号、密码和学校分级,如果匹配到相同的用户名,返回1,否则返回0。

3.5.2 checkPassword()

@Override
public int checkPassword(String username, String password) {
  try (FileReader reader = new FileReader(pathname);
       BufferedReader br = new BufferedReader(reader)
  ) {
    String line;
    while ((line = br.readLine()) != null) {
      String[] info = line.split("\t");
      if (!username.equals(info[0])) {
        continue; // 用户名不匹配则继续遍历文件
      }
      if (password.equals(info[1])) { // 用户名、密码均匹配
        return Integer.parseInt(info[2]); // 返回1,2,3表示小学、初中、高中
      } else {
        return 0; // 返回0找不到用户
      }
    }
  } catch (IOException e) {
    e.printStackTrace();
  }
  return 0;
}

该函数用于检查用户名、密码是否正确。

和前一个函数相似,通过访问相对路径,函数访问Accounts.txt文件,通过FileReader,逐行读取用户信息,并使用split函数分隔每行,如果匹配到相同的用户名和密码,返回对应的学校水平,否则返回0。

这个函数其实与前面那个非常相似,出现了重复代码,可以合并。

3.5.3 login()

@Override
public void login() { // 第一部分,用户登录
  Scanner scanner = new Scanner(System.in);
  System.out.println("请在同一行输入用户名和密码,以空格隔开:");
  String line;
  while (true) {
    line = scanner.nextLine();
    if (line.equals("退出") || line.equals("exit")) {
      System.exit(0); // 退出整个程序
    }
    String[] info = line.split(" ");  // 分割账号和密码
    if (info.length != 2) { // 输入格式不正确
      System.out.println("输入不符合规范,请重新输入:");
      continue;
    }
    if (checkExistence(info[0]) == 0) { // 检查用户存在性
      System.out.println("用户不存在,请输入正确的用户名:");
      continue;
    }
    int checkResult = checkPassword(info[0], info[1]);
    if (checkResult == 0) { // 检查密码正确性
      System.out*.println("密码错误,请输入正确的密码:");
      continue;
    } else if (checkResult == 1) {  // 根据checkPassword返回值创建对应类
      user = new Primary(info[0]);
    } else if (checkResult == 2) {
      user = new Junior(info[0]);
    } else if (checkResult == 3) {
      user = new Senior(info[0]);
    }
    init();
  }
}

该函数用于登录。

对于登录部分,先检验输入的格式。在登陆信息之后读取整行,并用空格分割,当长度不对时,提醒输入不符合规范,当用户不存在,密码错误时,都给出相应的提示。而如果正确,则创建对应的老师对象,并且进行init();

3.5.4 init()

  @Override
  public void init() { // 第二部分,命令处理
    System.out.println("目前登录为:" + user.getUsername() +" ,");
    System.out.println("准备生成" + user.getSchool() + "数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录):");
    Scanner scanner = new Scanner(System.in);
    int state;
    while (true) {
      state = eval(scanner.nextLine()); // 获取控制台的整行输入并使用eval函数进行解析
      switch (state) {  // 根据不同返回结果执行不同操作
        case 0 -> System.out.println("已经是" + user.getSchool() + "难度,重新输入指令或出题数量:");
        case 1, 2, 3 -> {
          return;
        }
        case -1 -> System.out.println("请输入小学、初中和高中三个选项中的一个:");
        case -2 -> System.out.println("输入中不能包含空格,请重新输入:");
        case 4 -> {
          login();  // 重新登陆
          return;
        }
        case 5 -> System.out.println("请输入10-30之间的题目数量:");
        default -> {
          quizGeneration(state);  // 在相应的文件夹内生成包含指定数量题目的文件·
          System.out.println("继续输入命令:");  // 生成成功后继续循环
        }
      }
    }

  }

该函数用于登录后的命令处理。

给出登录成功信息之后读取整行,并用eval函数处理输入,返回不同的状态,并以此来进行对应的操作。

3.5.5 eval()

  @Override
  public int eval(String cmd) {
    String[] args = cmd.split("\\s+");
    if (args.length != 1) {
      return -2;  // 判断输入是否有空格
    }
    int sum;
    try {
      sum = Integer.parseInt(cmd);
      if (sum == -1) {
        return 4;   // 重新登录
      }
      if (sum < 10 || sum > 30) {
        return 5;   // 不在范围内
      }
    } catch (NumberFormatException e) {
      if ((cmd.equals("切换为小学") && user.getRank() == 1)
              || (cmd.equals("切换为初中") && user.getRank() == 2)
              || (cmd.equals("切换为高中") && user.getRank() == 3)) {
        return 0; // 无效的切换命令,如在小学难度下再次切换为小学
      } else if (cmd.equals("切换为小学")) {
        switchSchool(user.getUsername(), 1);
        return 1;
      } else if (cmd.equals("切换为初中")) {
        switchSchool(user.getUsername(), 2);
        return 2;
      } else if (cmd.equals("切换为高中")) {
        switchSchool(user.getUsername(), 3);
        return 3;
      } else {
        return -1;  // 无意义命令
      }
    }
    return sum; // 返回题目要求的题量
  }

该函数用于处理登录后的命令。

先对输入的命令格式进行判断,如果有空格,直接返回-2;先判断输入是否为数字,如果输入-1,则返回4,表示需要重新登录;如果输入>30或<10,则返回5,表示不在范围内,数字规范则返回自身。如果不是数字,判断是否为重复设置或正常设置,如果正确输入,则使用switchSchool函数来切换难度。

3.5.6 switchSchool()

@Override
  public void switchSchool(String name, int rk) {
    // 通过将user重新指定为不同的类来切换出题模式
    if (rk == 1) {
      System.out.println("已切换为小学难度。");
      user = new Primary(name);
    } else if (rk == 2) {
      System.out.println("已切换为初中难度。");
      user = new Junior(name);
    } else if (rk == 3) {
      System.out.println("已切换为高中难度。");
      user = new Senior(name);
    }
    init();
  }

该函数通过将user重新指定为不同的类来切换出题模式。

3.5.7 quizGeneration()

@Override
  public void quizGeneration(int sum) { // 第三部分,生成题目
    String outputFolderPath = "output/" + user.getUsername(); // 文件夹名
    File outputFolder = new File(outputFolderPath);
    if (!outputFolder.exists()) { // 检查文件夹存在性,没有就创建
      outputFolder.mkdirs();
    }
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
    String timestamp = sdf.format(new Date());
    String fileName = outputFolderPath + "/" + timestamp + ".txt";  // 符合要求的文件名
    try {
      // 创建文件并写入题目
      FileWriter writer = new FileWriter(fileName);
      Set<String> generatedQuiz = historyRecord(outputFolder);  // 获取该用户此前生成的所有题目并加入HashSet
      for (int i = 0; i < sum; i++) {
        String quiz;
        do {
          quiz = user.generateSingleQuiz();
        } while (generatedQuiz.contains(quiz)); // 在不重复的前提下生成题目
        if (isSimpleQuiz(quiz)) { // 如果只有2个操作数,则同时向集合加入操作数调转后的式子
          generatedQuiz.add(swapOperand(quiz));
        }
        generatedQuiz.add(quiz);  // 将新生成的题目加入历史题目集
        writer.write((i + 1) + ". " + quiz + "\n\n"); // 向文件写入题目
      }
      writer.close();
      System.out.println("题目已生成并保存到文件:" + fileName);  // 控制台提示
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

该函数可根据题目数量生成对应难度的题目,创建文件夹和文件并将题目写入文件.

首先函数检查文件夹是否存在,没有就创建,并根据时间生成符合要求的文件名,调用historyRecord函数读取该用户此前生成的所有题目并加入HashSet,调用用户的generateSingleQuiz方法生成题目,并检查是否重复,重复就重新生成题目。通过判断之后,就将新生成的题目加入历史题目集,而如果题目只有2个操作数,则同时向集合加入操作数调转后的式子,防止出现1+2,2+1的重复题目。最后将题目写入文件。

3.5.8 historyRecord()

@Override
  public HashSet<String> historyRecord(File outputFolder) {
    HashSet<String> generatedQuiz = new HashSet<>();
    File[] files;
    if (outputFolder.exists() && outputFolder.isDirectory()) {
      files = outputFolder.listFiles(); // 列出文件夹内的所有文件
      // 遍历每一个文件并将题目的去除序号后加入历史题目集
      if (files != null) {
        for (File file : files) {
          if (file.isFile()) {
            try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
              String line;
              while (true) {
                line = reader.readLine();
                if (line == null) { // 文件末尾判断
                  break;
                }
                if (line.equals("")) {  // 题间空行判断
                  continue;
                }
                line = removeIndex(line); //读取的题目去除序号
                generatedQuiz.add(line); //加入集合
                if (isSimpleQuiz(line)) { //处理两数加/乘的情况
                  generatedQuiz.add(swapOperand(line));
                }
              }
            } catch (IOException e) {
              e.printStackTrace();
            }
          }
        }
      }
    } else {
      System.err.println("文件夹不存在或不是文件夹。");
    }
    return generatedQuiz;
  }

该函数用于将当前用户所有曾生成过的题目加入一个HashSet中,用于后续的比对.

代码先列出文件夹内的所有文件然后遍历每一个文件,将题目去除序号、空行后加入历史题目集,如果是2个操作数加/乘的情况,就调用swapOperand来添加操作数反转后的题目到集合中。

3.5.9 removeIndex(String input)

@Override
  public String removeIndex(String input) {
    String pattern = "^\\d+\\.\\s+";  // 题目格式“数字序号+空格+题目本体”
    return input.replaceAll(pattern, "");
  }

该函数使用正则表达式匹配数字和点号以去除题目前的序号和空格

3.5.10 isSimpleQuiz(String input)

@Override
  public boolean isSimpleQuiz(String input) {
    int countPlusMul = 0; // 加、乘法出现次数
    int countSubDiv = 0;  // 减、除法出现次数
    for (char ch : input.toCharArray()) {
      if (ch == '+' || ch == '*') {
        countPlusMul++;
      } else if (ch == '-' || ch == '/') {
        countSubDiv++;
      }
    }
    // 仅在式子只有一个加、乘法时返回真
    return countPlusMul == 1 && countSubDiv == 0;
  }

该函数判断是否是2个操作数加/乘的情况,以免出现重复的情况。

3.5.11 swapOperand(String input)

@Override
public String swapOperand(String input) {
  Pattern pattern = Pattern.compile("([^+*]+)\\s*([+*])\\s*([^+*]+)\\s*=");
  Matcher matcher = pattern.matcher(input);  // 匹配表达式

  if (matcher.find()) {
    return matcher.group(3) + matcher.group(2) + " " + matcher.group(1) + "=";
  }
  return null;
}

该函数使用通配符来捕获数值和运算符,将2个操作数加/乘的情况进行操作数的反转,并且返回String类型

3.6 Test测试类

 public static void main(String[] args) throws InterruptedException {
    user = new Primary("Test");
    String outputFolderPath = "output/" + user.getUsername(); // 文件夹名
    System.out.println("当前用户: " + user.getUsername() + "\n用户文件保存在: " + outputFolderPath);
    HashSet<String> set = historyRecord(new File(outputFolderPath));
    System.out.println("以下是该用户的生成记录:");
    Thread.sleep(1000);
    for (String s : set) {
      System.out.println(s);
    }
    System.out.print("\n");
    String quiz = "84 + 63 =";
    Thread.sleep(1000);
    System.out.println("其中一条记录为 " + quiz + "\n将操作数调换后的结果 " + swapOperand(quiz) + "进行查重:");
    quiz = swapOperand(quiz);
    if (set.contains(quiz)) {
      System.out.println(quiz + " 是重复题目\n");
    }
    Thread.sleep(1000);
    quiz = "34 * 83 - 83 / 94 / 70 =";
    System.out.println("对另一条记录 " + quiz + " 进行查重:");
    if (set.contains(quiz)) {
      System.out.println(quiz + " 是重复题目\n");
    }
    Thread.sleep(1000);
    quiz = "33 * 83 - 83 / 94 / 70 =";
    System.out.println("把上面的问题改为 " + quiz + " 进行查重:");
    if (!set.contains(quiz)) {
      System.out.println(quiz + " 不是重复题目\n");
    }
    Thread.sleep(1000);
    System.out.println("该查重系统运行正常。");
  }

在该类中,main方法用于测试查重功能,提供了几个测试案例,通过swapOperand的调用验证2个操作数交换的情况,通过对set的比较,验证set中是否可以查到题目。

四、代码测试

4.1 登录测试

4.1.1 不符合规范的登录方式

4.1.2 正常登录

4.2试卷生成测试

4.2.1 非规范数字测试

4.2.2正常数字输入

image-20230919112343442

4.2.3 非规范切换测试

4.2.4 正确切换测试

image-20230919112510444 image-20230919112543963

4.2.5 退出测试

4.2.6 查重测试

五、优点与缺点

5.1 优点分析

  • 文件结构清晰,看到目录即可大致知晓各个文件的功能,Main类单独存放,方便测试人员运行,设计友好
  • 在几乎所有方法上都添加了Javadoc说明,方便测试人员理解方法的作用
  • 几乎所有类都使用了接口和抽象类,方便代码的修改,方法说明写在接口中,代码更整洁,优美
  • 在代码内部添加了很多注释,易于理解
  • 对于不同的错误输入,给出了不同的提出,考虑到了各种情况,用户交互好,无BUG
  • 使用HashSet,记录已有的题目,巧妙采用合理的数据结构,题目生成随机,通过一个随机数,巧妙地控制了括号,平方、三角函数等特殊运算的出现概率
  • 代码符合google java规范,方法起名精确易懂,功能明确,同时代码思路明确
  • 提供了查重的测试案例,方便测试人员测试、
  • 考虑到了被除数可能为0和tan90°等特殊情况

5.2 缺点分析

  • 有些方法代码相似,重复部分多,可考虑拆分公共部分,提高代码的复用性
  • 初中、高中题目生成未考虑到真实情况,虽然符合出题要求但是实际上难以得到正确答案,如tan89°等等
  • 将登录、操作、出题都放在同一个类中,可以根据功能不同拆分成不同的类,实现功能的分离实现
  • 将单独的出题放在放在用户的继承类中,切换难度时需要new新的继承类对象出来
  • 用户类中分别int和String表示学校类型,数据冗余,可能会添加内存的占用