【HNU个人项目互评】 基于java生成中小学数学卷子自动生成程序代码分析

发布时间 2023-09-20 20:32:38作者: 小蘑菇x

【评价者】:金颖希
【项目作者】:刘一凡
【使用语言】:Java

前言

【项目简介】本项目为中小学数学卷子自动生成程序,采用java编程语言实现用户登录、用户类型判断、生成题目、题目查重、切换用户类型以及题目保存等简单功能。
【评价标准】本文以代码分析为主,主要从代码的可读性、性能、可靠性、可扩展性、以及代码规范等角度对于每个类进行详细的分析。并在本文的最后附上相应的功能测试。

用户类接口User

/**
 * 用户类接口
 */
interface User {
  
  String getUserName();
  
  void setUserName(String name);
  
  String getPassword();
  
  void setPassword(String pwd);
  
  String getUserType();
  
  void setUserType(String type);
}

优点:
(1)解耦性:提供用户类接口将系统的各个模块解耦,减少模块之间的依赖关系。这样可以降低系统的耦合度,提高系统的可维护性和可扩展性。
(2)可拓展性:易于扩展功能,用户类接口定义了getUserName等方法,对外暴露了一定的功能。当需要增加新的功能时,可以通过在接口中添加新的方法或属性来扩展接口,而无需修改已有的代码。这样做不会影响到已经使用接口的其他模块,提供了良好的可扩展性。
(3)多态性:多态性:用户类接口支持多态性,即可以使用多种不同的实现类对象来实例化接口。这使得系统具有灵活性,可以根据需要替换不同的实现,以适应不同的业务场景和需求变化。

教师类接口实现Teacher

这段代码是一个教师类的实现,实现了用户类接口(User)。代码中包含了教师的基本信息和相关方法。

/**
 * 教师类,用于存储教师相关基本信息和对应方法。
 */
public class Teacher implements User {
  
  String name = null; //用户名
  String pwd = null; //用户密码
  String type = null; //用户类型(小学,初中,高中)
  
  public Teacher(String name, String pwd, String type) {
    this.name = name;
    this.pwd = pwd;
    this.type = type;
  }
  
  public Teacher() {
  }
  
  public String getUserName() {
    return this.name;
  }
  
  public void setUserName(String name) {
    this.name = name;
  }
  
  public String getPassword() {
    return this.pwd;
  }
  
  public void setPassword(String pwd) {
    this.pwd = pwd;
  }
  
  public String getUserType() {
    return this.type;
  }
  
  public void setUserType(String type) {
    this.type = type;
  }
}

优点:

(1)封装性:使用了私有成员变量(name, pwd, type)来存储教师的用户名、密码和类型,通过公有的getter和setter方法来访问和修改这些属性。这样可以保护数据的安全性,并提供了对外部代码友好的界面。
(2)代码规范:方法命名采用了驼峰命名法,能够清晰地表示其含义,易于阅读和理解。

客户端类Client

这段代码是一个客户端类,用于装载终端相关行为。主要包括了main方法和两个辅助方法(control和problemGenerate、modifyType)。这里针对方法来逐个分析。

control()方法

通过Scanner读取用户输入的选择,使用switch语句根据选择调用相应的方法。同时,该方法返回一个布尔值,用于控制是否进行下一轮操作。在方法中处理了输入错误的情况,并提供了相应的提示。

/**
   * @param teacher 登录的教师
   * @return true->可以循环进行下一轮操作
   */
  public static boolean control(Teacher teacher) {
    Scanner sc = new Scanner(System.in);
    while (true) {
      boolean myChoose = false;
      System.out.println(
          "请输入你的选择:\n" + "-1 -> 退出当前用户重新登陆,或退出系统\n" + " 0 -> 生成题目\n"
              + " 1 -> 切换类型");
      String choose = sc.nextLine();
      switch (choose) {
        case "-1": {
          myChoose = true; //退出生成题目的循环
          break;
        }
        case "0": {
          problemGenerate(teacher);
          break;
        }
        case "1": {
          modifyType(teacher);
          break;
        }
        default: {
          System.out.println("输入格式错误错误!,请重新输入");
        }
      }
      if (myChoose) {
        break;
      }
    }
    return true;
  }

优点:
代码逻辑清晰,通过用户输入的选择来进行相应的操作,并提供了退出循环和默认情况处理。

改进建议:
(1)健壮性:代码中没有考虑到用户输入非法字符的情况,可以增加相应的输入验证机制来提高代码的健壮性。
(2)命名:对于循环控制变量myChoose的命名,可以更加直观地表达其含义。

problemGenerate()方法

根据教师信息生成对应数量的题目。先通过UserCheck类验证教师的用户名和密码,然后根据用户类型调用不同的题目生成器(PrimGenerator、MidGenerator、HighGenerator)生成题目。最后输出成功信息。

/**
   * 根据教师信息生成对应题目
   *
   * @param teacher 登录的教师
   */
  public static void problemGenerate(Teacher teacher) {
    Scanner sc = new Scanner(System.in);
    UserCheck uc = new UserCheck();
    teacher = uc.userCheck(teacher.getUserName(), teacher.getPassword());
    
    int numOfProblem;
    while (true) {
      System.out.println(
          "\n准备生成" + teacher.getUserType() + "数学题目,请输入生成题目数量(10-30)");
      String Value = sc.nextLine();
      try {
        numOfProblem = Integer.parseInt(Value);
        if (numOfProblem < 10 || numOfProblem > 30) {
          System.out.println("题目输入数量错误,请重新输入数量(10-30)");
          continue;
        }
        break;
      } catch (NumberFormatException e) {
        System.out.println("你输入的不是一个整数,请检查格式");
      }
    }
    if (teacher.getUserType().equals("小学")) {
      PrimGenerator primGenerator = new PrimGenerator();
      primGenerator.generate(teacher.getUserName(), numOfProblem);
    } else if (teacher.getUserType().equals("初中")) {
      MidGenerator midGenerator = new MidGenerator();
      midGenerator.generate(teacher.getUserName(), numOfProblem);
    } else {
      HighGenerator highGenerator = new HighGenerator();
      highGenerator.generate(teacher.getUserName(), numOfProblem);
    }
    System.out.println("题目生成成功!请前往exam目录下指定文件夹查看。");
  }

优点:
健壮性:使用try-catch块捕获用户输入非整数的异常,并给出相应的提示信息,提高了代码的健壮性。

缺点:
(1)异常处理不足:虽然对用户输入的非整数进行了异常处理,但没有对其他潜在的异常情况进行处理,比如无法找到生成题目的文件夹等。
(2)耦合性较强:在方法中创建了UserCheck对象,并调用其userCheck方法进行教师身份验证。这种耦合性较强,可能会导致代码难以扩展和维护。

改进建议:
(1)使用工厂模式:可以使用工厂模式来根据教师类型动态创建相应的题目生成器,提高代码的扩展性和灵活性。
(2)降低依赖性:可以将教师身份验证的逻辑从该方法中剥离,使得该方法只负责生成题目。

modifyType()方法

实现了修改登录教师类型的功能。通过Scanner读取用户输入的切换选项,检测输入格式并调用FileOperator类进行文件操作,将修改后的类型保存到文件中。该方法提供了取消切换的选项,并给出相应的提示。

  /**
   * 实现修改登录教师的Type的功能
   *
   * @param teacher 登录的教师
   */
  public static void modifyType(Teacher teacher) {
    Scanner scanner = new Scanner(System.in);
    boolean modify = false;
    while (!modify) {
      System.out.println(
          "\n输入 (切换为XX) 切换类型选项,XX为小学、初中和高中三个选项中的一个。或输入(quit)以取消切换行为");
      String SwitchType = scanner.nextLine();
      //输入内容检测
      if (SwitchType.startsWith("切换为") && (SwitchType.substring(3).equals("小学")
          || SwitchType.substring(3).equals("初中") || SwitchType.substring(3).equals("高中"))) {
        FileOperator FO = new FileOperator();
        //切割为后两个汉字
        SwitchType = SwitchType.substring(3);
        FO.modifyType("lib/Teacher", teacher.getUserName(), SwitchType);
        modify = true;
        System.out.println("\n类型修改成功");
      } else if (SwitchType.equals("quit")) {
        modify = true;
        System.out.println("\n已退出切换模式");
      } else {
        System.out.println("格式错误!");
      }
    }
  }

优点:
文件操作:使用FileOperator类的modifyType方法,实现了对文件中教师类型的修改。将修改后的类型保存到文件中,确保数据的持久化。

缺点:
(1)硬编码问题:在代码中直接指定了文件路径("lib/Teacher"),这样会导致代码的可扩展性受限。可以考虑将文件路径作为参数传递,提高代码的灵活性。
(2)字符串处理不够健壮:对输入内容进行判断时,使用了字符截取和字符串比较的方式。如果输入的字符串中包含空格或其他非预期字符,可能导致判断失效。

main方法

使用了嵌套循环来进行用户交互,内层循环根据用户选择调用control方法或break退出循环,外层循环根据用户选择决定是否进行下一轮操作。此设计能够实现持续的用户交互。

 public static void main(String[] args) {
    boolean flag = true;
    while (flag) {
      while (true) {
        Init init = new Init();
        if (!init.goOrNot()) {
          flag = false;
          break;
        }
        Teacher teacher = init.init();
        if (teacher.getUserName().length() == 0) {
          break;
        }
        boolean quit = control(teacher);
        if (quit) {
          break;
        }
      }
    }
  }

改进建议:
异常处理:在合适的位置添加try-catch块,对可能出现的异常情况进行处理,例如输入错误、文件读写失败等,可以添加相应的异常处理机制。

界面实现类Init

这是一个用于实现进入系统生成题目前界面的类,同样对于该类下的方法逐个进行分析。

init()方法

这段代码是Init类中的init()方法的实现,用于初始化教师信息并进行验证。

/**
   * @return Teacher 登录教师的信息
   */
  public Teacher init() {
    Scanner scanner = new Scanner(System.in);
    
    UserCheck userCheck = new UserCheck();
    boolean pass = false;
    Teacher teacher = new Teacher("", "", "");
    //没有通过则持续验证
    do {
      System.out.println("\n请在下方输入用户名和密码(格式:用户名 密码),或输入quit取消登录");
      String line = scanner.nextLine(); //输入用户名和密码
      String[] information = line.split(" ");
      //输入quit
      if (information[0].equals("quit") && information.length == 1) {
        System.out.println("您已取消登录");
        pass = true;
        break;
      }
      // 格式不符合 X X
      if (information.length != 2) {
        System.out.println("格式错误,请重新输入");
        continue;
      }
      //到此格式正确
      teacher = userCheck.userCheck(information[0], information[1]);
      if (teacher.getUserName().length() == 0) {
        System.out.println("用户名或密码错误,请输入正确的用户名、密码");
      } else {
        System.out.println("登录成功,当前选择为" + teacher.getUserType() + "出题");
        pass = true;
      }
    } while (!pass);    //循环检测询问
    return teacher;
  }

优点:
(1)输入验证:通过对用户输入的用户名、密码和其他选项进行判断和处理,实现了输入验证功能,确保输入符合要求。
(2)用户交互:使用Scanner类获取用户输入,并根据输入结果给出相应的提示信息,提高了用户交互体验。
(3)返回结果:通过返回Teacher对象表示登录教师的信息,方便后续程序使用。

改进建议:
优化逻辑:将两次对quit选项的判断合并为一处处理,以简化代码结构,提高可读性。

goOrNot()方法

这段代码是Init类中的goOrNot()方法的实现,用于询问用户是否继续使用系统。

  /**
   * 询问用户是否继续使用系统
   *
   * @return true -> 继续使用系统,false -> 退出系统
   */
  public boolean goOrNot() {
    System.out.println("\n欢迎使用中小学数学卷子自动生成程序!");
    System.out.println("请输入:1->使用  2->退出 ");
    Scanner scanner = new Scanner(System.in);
    //不符合规范,则循环输入
    while (true) {
      String option = scanner.nextLine();
      if (option.equals("1")) {
        return true;
      } else if (option.equals("2")) {
        System.out.println("欢迎下次使用!");
        return false;
      } else {
        System.out.println("输入错误!\n请输入:1->使用  2->退出");
      }
    }
  }

优点:
(1)用户交互:通过输出信息和获取用户输入的方式,实现了与用户的简单交互。
(2)返回结果:通过布尔值表示用户是否继续使用系统的选择,方便控制程序的流程。

缺点:
循环输入不够健壮:在循环等待用户正确输入选项时,没有添加退出机制,可能导致无限循环。

题目格式产生类BasicProblem

这段代码是BasicProblem类中的basicProblemGenerate()方法的实现,用于产生随机的基础题目格式。

/**
 * 这是一个用于产生基本题目格式的类
 */
public class BasicProblem {
  
  /**
   * @param MinOperand 最小的操作数数量
   * @return 随机生成的题目String
   */
  public String basicProblemGenerate(int MinOperand) {
    Random random = new Random();
    String[] operators = {"+", "-", "*", "/"};
    //默认范围1-5(MinOperand == 1) ->初高中
    int operandCount = random.nextInt(5) + 1;
    //操作数最少为2时,范围变成2-5 ->小学
    if (MinOperand == 2) {
      operandCount = min(operandCount + 1, 5);
    }
    //题目字符串expression
    StringBuilder expression = new StringBuilder(" ");
    //stack用于存储括号数目
    Stack<Integer> stack = new Stack<>();
    //是否添加了括号
    int flagOfBracket;
    for (int j = 0; j < operandCount; j++) {
      int num = random.nextInt(100) + 1;
      String operator = operators[random.nextInt(4)];
      if (j != 0) {
        expression.append(" ").append(operator);
      }
      //若索引剩下两个数字,则不要括号
      if (random.nextBoolean() && stack.size() < operandCount - j && j != operandCount - 1
          && operandCount != 2) {
        expression.append(" (");
        stack.push(j);
        //添加了括号,标志位置1
        flagOfBracket = 1;
      } else {
        flagOfBracket = 0;
      }
      expression.append(" ").append(num);
      //flagOfBracket == 0,添加完(之后不能立即再加)
      if (!stack.isEmpty() && stack.peek() != j && random.nextBoolean() && flagOfBracket == 0) {
        expression.append(" )");
        stack.pop();
      }
    }
    //补充没有配对的括号
    while (!stack.isEmpty()) {
      expression.append(" )");
      stack.pop();
    }
    return expression.toString() + ' ';
  }
}

优点:
(1)输入验证:通过参数MinOperand来控制最小操作数数量,可以根据需求灵活调整生成的题目复杂度。
(2)括号添加:通过使用Stack来维护括号数目,能够在题目中合理地添加括号,满足功能要求。

查重类ExamCheck

这段代码是ExamCheck类的实现,用于检测生成的题目是否和已生成的题目重复。

/**
 * 这是用于检测题目是否和对应教师下已生成题目重复的类
 */
public class ExamCheck {
  
  public static boolean isNumeric(String str) {
    for (int i = str.length(); --i >= 0; ) {
      int chr = str.charAt(i);
      if (chr < 48 || chr > 57) {
        return false;
      }
    }
    return true;
  }
  
  /**
   * @param str 题目String
   * @return true->含有2个操作数
   */
  public static boolean haveTwoNumbers(String str) {
    String[] stringArray = str.split(" ");
    int numOfNumber = 0;
    for (String s : stringArray) {
      if (isNumeric(s)) {
        numOfNumber++;
      }
    }
    return numOfNumber == 2;
  }
  
  /**
   * @param s1 生成并待加入的题目
   * @param s2 已生成的题目
   * @return true -> 查重通过
   */
  public static boolean satisfySwapLaw(String s1, String s2) {
    //如果均有2个数字,则检测是否满足交换律查重
    if (haveTwoNumbers(s1) && haveTwoNumbers(s2)) {
      String[] stringOfS1 = s1.split(" ");
      String[] stringOfS2 = s1.split(" ");
      if (stringOfS1[3].equals(stringOfS2[3])) { //运算符相同
        //两两交换检测
        if (stringOfS1[2].equals(stringOfS2[2]) && stringOfS1[4].equals(stringOfS2[4])) {
          return false;
        } else {
          return !stringOfS1[2].equals(stringOfS2[4]) || !stringOfS1[4].equals(stringOfS2[2]);
        }
      }
    }
    // 如果没有两个数字或查重通过,则返回通过测试
    return true;
  }
  
  /**
   * 这是一个实现查重功能的方法
   *
   * @param problem 生成并待加入的题目
   * @param path    存储题目文件的路径
   * @return true->查重通过
   */
  public boolean check(String problem, String path) {
    
    // 创建File对象
    File dir = new File(path);
    
    // 获取文件夹下的所有文件
    File[] files = dir.listFiles();
    
    if (files != null) {
      for (File file : files) {
        if (file.isFile()) {
          try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
            String line;
            while ((line = reader.readLine()) != null) { //不为空行
              if (satisfySwapLaw(problem, line)) {
                if (problem.equals(line)) { //和出过的题目相同
                  return false;
                }
              } else { //交换律查重没有通过
                return false;
              }
            }
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
    }
    return true;
  }
}

优点:
(1)可读性好:代码结构清晰,将不同功能的方法进行了合理的拆分,提高了代码的可读性和可维护性。
(2)查重功能:satisfySwapLaw()方法用于判断两个题目是否满足交换律查重条件,满足功能要求。
(3)文件处理:通过File对象和BufferedReader实现对文件的读取,能够检查指定路径下的题目文件是否和待加入的题目重复。

抽象类ExamGenerator

这段代码定义了一个抽象类ExamGenerator,用于衍生出三种难度的题目类。

/**
 * 抽象类,可以衍生出三种难度的题目类
 */
abstract public class ExamGenerator {
  
  public void generate(String name, int numOfProblem) {
  }
}

优点:
(1)高度可扩展性:通过抽象类的方式,可以方便地衍生出不同难度的题目类,满足不同级别的需求。
(2)规范接口:通过定义generate()方法,规定了生成题目的接口,便于后续具体题目类的实现和调用。

抽象类继承小学题目生成类PrimGenerator

抽象类继承以小学题目生成为例,其他两类此处不赘述。PrimGenerator类继承了ExamGenerator抽象类,基于基础题目的随机生成,实现了小学题目的生成功能。代码具备继承抽象类、生成随机题目和文件操作等特点。

public class PrimGenerator extends ExamGenerator {
  
  /**
   * @param numOfProblem 生成题目数量
   * @param name         用户名
   */
  public static void generateRandomMathProblem(int numOfProblem, String name) {
    FileOperator fileOperator = new FileOperator();
    BasicProblem basicProblem = new BasicProblem();
    ExamCheck examCheck = new ExamCheck();
    //文件路径
    String path = "exam/" + name;
    //没有该用户的路径,则创建
    fileOperator.createFolder(path);
    StringBuilder test = new StringBuilder();
    for (int i = 1; i <= numOfProblem; i++) {
      String tempProblem = i + ".\t" + basicProblem.basicProblemGenerate(2) + " = ?";
      if (examCheck.check(tempProblem, path)) {
        test.append(tempProblem).append("\n \n");
      } else {
        i--;
      }
    }
    // 输出生成的数学题目
    fileOperator.saveToFile(
        path + "/" + new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()) + ".txt",
        test.toString());
  }
  
  //小学题生成
  public void generate(String name, int numOfProblem) {
    generateRandomMathProblem(numOfProblem, name);
  }
}

优点
(1)继承抽象类:PrimGenerator类继承了ExamGenerator抽象类,实现了generate()方法,符合面向对象的设计原则,提供了一种可扩展和复用的方式。
(2)生成随机题目:generateRandomMathProblem()方法使用BasicProblem类生成随机的数学题目,并通过ExamCheck类进行查重,确保生成的题目不重复。

缺点:
灵活性:generateRandomMathProblem()方法中的生成题目的逻辑是固定的,无法根据具体需求进行定制。可能需要通过进一步的参数设置或调整来增加灵活性。

代码总结

优点:
(1)可读性高:代码结构清晰,变量和函数命名规范,易于阅读和理解。
(2)高度可扩展性:通过抽象类、接口等方式,可以方便地衍生出不同难度的题目类,满足不同级别的需求。对于整个项目具有非常强的拓展性。
(3)可维护性强:采用了模块化设计,各个模块之间职责清晰,修改一个功能不会影响其他部分,便于后续维护和扩展。
(4)性能良好:代码逻辑合理,算法经过优化,运行效率较高。
(5)代码规范:变量以及方法命名采用了驼峰命名法,并且在每个类以及方法前都有相应的javadoc,能够清晰地表示其含义,易于阅读和理解。
缺点:
(1)错误处理不完善:代码中可能存在一些潜在的错误,但没有进行充分的错误处理和异常捕获。
(2)循环输入不够健壮:在循环等待用户正确输入选项时,没有添加退出机制,可能导致无限循环。

附:功能测试

(1)用户登录界面


输入选项1进行使用

输入错误,则会出现相关提示,并且支持再次输入。

输入正确则进入功能选择页面:

(2)功能选择页面


出现相关的功能提示,接下来根据提示分别测试对应功能。

(3)出题功能测试


选择0之后,会出现输入生成题目数量的提示,此时如果输入数量不再提示范围内,或者输入的数量不满足格式,则会有相应的错误提示处理。

如果输入正确,则有相对的题目文件生成并且给出相应的提示信息



发现此时生成的题目类型以及格式正确,并且无重复题目出现,对于其它类型的题目同理进行了相应的测试,发现均正确,此处省去图片。

(4)难度切换测试

将功能选择输入1,进入难度切换功能页面

当输入错误时,会出相对应的错误提示信息,并且可以再次输入

输入正确后,给出对应的修改成功提示并回到功能选择页面,可以选择继续出题或者切换难度或者直接退出。

(5)退出功能测试

将功能选择输入-1,退出登录并且出现最初的登陆界面。

至此功能测试完毕。

感谢阅读!