HNU个人项目评测—中小学数学试卷自动生成程序

一.简介

本博客是针对结对编程队友苟怀炜同学的个人项目代码所写的分析与总结,代码使用语言为Java,与本人项目所用编程语言一致。为了更好的实现结对项目功能,在评价苟怀炜同学的代码时,我会学习他的优点,同时针对不足的地方提出自己的拙见。

二.项目要求

1、命令行输入用户名和密码,两者之间用空格隔开(程序预设小学、初中和高中各三个账号,具体见附表),如果用户名和密码都正确,将根据账户类型显示“当前选择为XX出题”,XX为小学、初中和高中三个选项中的一个。否则提示“请输入正确的用户名、密码”,重新输入用户名、密码;

2、登录后,系统提示“准备生成XX数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录):”,XX为小学、初中和高中三个选项中的一个,用户输入所需出的卷子的题目数量,系统默认将根据账号类型进行出题。每道题目的操作数在1-5个之间,操作数取值范围为1-100;

3、题目数量的有效输入范围是“10-30”(含10,30,或-1退出登录),程序根据输入的题目数量生成符合小学、初中和高中难度的题目的卷子(具体要求见附表)。同一个老师的卷子中的题目不能与以前的已生成的卷子中的题目重复(以指定文件夹下存在的文件为准,见5);

4、在登录状态下,如果用户需要切换类型选项,命令行输入“切换为XX”,XX为小学、初中和高中三个选项中的一个,输入项不符合要求时,程序控制台提示“请输入小学、初中和高中三个选项中的一个”;输入正确后,显示“”系统提示“准备生成XX数学题目,请输入生成题目数量”,用户输入所需出的卷子的题目数量,系统新设置的类型进行出题;

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


表1——账号-密码表

账户类型 账户 密码
小学 张三1 123
小学 张三2 123
小学 张三3 123
初中 李四1 123
初中 李四2 123
初中 李四3 123
高中 王五1 123
高中 王五2 123
高中 王五3 123

表2——小学、初中、高中题目难度要求表

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

三.测试与分析

1.黑盒测试

  1. 登录功能

    分析:界面设计良好,登录前后均有充分的文字提示信息,提高用户体验感,同时针对前置空格和后置空格分别做了处理,值得学习!



    分析:能够正确退出,且退出后能够重新登录。


  2. 出题功能

    分析:提示清晰,能够连续生成题目。





分析:小学初中高中三种类型的难度的题目均出题正确,同一个老师出的题存放在同一个文件夹中,且每个题目间有空行,看起来简洁舒适。同时文件命名符合要求规范。



3. 切换难度功能

分析:能够正确切换难度,在切换难度时,有对错误输入进行处理,每一步之间均有充足的提示信息。

2.白盒测试

项目结构
该项目一共有6个Java文件,分别Main,User,Problem,PrimaryProblem,MidProblem,HighProblem。其中Problem为抽象类,PrimaryProblem,MidProblem,HighProblem分别是小学,初中,高中难度下Problem的子类。充分体验了面向对象,继承的思想。

下面对每个Java文件代码进行分析:

  • Main.Java
init();
    while (true) {
      System.out.println("***********欢迎使用中小学数学卷子自动生成程序***********");
      User user = login();
      menu(user);
    }

分析:对代码进行封装,init()方法进行用户的初始化,login()方法用来实现用户的登录功能,menu()方法用来实现用户登录成功后的功能选择和处理。

public static User login() {
    Scanner scan = new Scanner(System.in);
    System.out.println("请输入用户名和密码,两者之间用空格隔开:");
    String temp = scan.nextLine();
    String[] info = temp.split("\\s+");
    if (info.length == 2) {
      for (User user : users) {
        if (user.check(info[0], info[1])) {
          System.out.println("登陆成功\n当前选择为" + user.getType() + "出题");
          return new User(info[0], info[1], user.getType());
        }
      }
    }

    System.out.println("用户名或密码错误,请重新输入!\n");
    return login();
  }

分析:①使用\\s+来对后置空格的数量进行处理,值得学习②递归调用login()方法,看似没有问题,但是当恶意登录错误的时候会存在爆栈导致程序崩溃,这个是需要注意的地方!
个人建议:将递归调用修改为使用一个boolean变量进行登录成功与否的判断。

public static void menu(User user) {
    while (true) {
      System.out.println("准备生成" + user.getType()
          + "数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录)(切换类型格式:切换为xx):");
      Scanner scan = new Scanner(System.in);
      String s = scan.nextLine().trim();
      if (s.contains("切换")) {
        user.changeType(s);

      } else {
        int i;
        try {
          i = Integer.parseInt(s); // 转化字符类型数字
          if (i == -1) {
            break;
          } else if (i >= 10 && i <= 30) {
            System.out.println("正在生成" + user.getType() + "数学题目");
            user.generateQuestions(i);

          } else {
            System.out.println("题目数量的有效输入范围是10-30,请重新输入");
          }
        } catch (NumberFormatException e) { //不是数字
          System.out.println("请按要求输入\n");
        }
      }
    }
  }

分析:用try catch方法来巧妙解决根据输入类型(数字或字符串)跳转到不同处理方法的难题,值得学习。

  • User.Java
private String name;
private String password;
private String count_type; //小学 中学 高中
private Problem paper;
public User(String name, String password, String count_type);
public boolean check(String name, String password);
public void generateQuestions(int numQuestions);
public Set<String> loadPreviousQuestions(String f);
void changeType(String s);

分析:将name,password,count_type,paper均设为私有属性,提高数据的安全性。但需要注意的是,count_type并不符合Google Java规范,同时paper属性是Problem类型,有点意义不明。
check()方法用来检查账号密码是否正确,generateQuestions()方法用来生成题目,loadPreviousQuestions()方法用来获取已有题目,changeType()方法用来更改试卷类型。
使用Set来保存已有题目,使得后续查重时时间复杂度大大降低,值得学习。

public User(String name, String password, String count_type) {
    this.name = name;
    this.password = password;
    this.count_type = count_type;
    if (count_type.equals("小学")) {
      paper = new PrimaryProblem();
    } else if (count_type.equals("初中")) {
      paper = new MidProblem();
    } else {
      paper = new HighProblem();
    }
  }

分析:根据不同的类型来生成不同的对象,是很好的工厂方法模式的运用,值得学习!

try (PrintWriter writer = new PrintWriter(folder + File.separator + filename)) {
      for (int i = 1; i <= numQuestions; i++) {
        String question = paper.create();
        // 检查题目是否重复
        while (previousQuestions.contains(question)) {
          question = paper.create();
        }
        writer.println("题目" + i + ":");
        writer.println(question);
        writer.println();
        previousQuestions.add(question); // 将新生成的题目添加到Set中
      }
    } catch (Exception e) {
      System.out.println("无法保存题目文件。");
    }
    System.out.println("题目生成成功\n题目保存路径为" + folder + "\n");
  }

分析:generateQuestions()方法用来生成试卷(ps:为啥不是getPaper(),含义有些不明T T),首先新创建一个question对象,然后通过while循环检查是否存在于previousQuestions中,
如果存在即重复调用create()方法再次创建一道新题目,直到新题目和以前的题目不重复为止。接着写入writer中,最后将题目加入set中防止一套试卷内出现相同题目。逻辑简洁清晰。

void changeType(String s) {
    if (s.equals("切换为小学")) {
      paper = new PrimaryProblem();
      count_type = "小学";
      System.out.println("当前选择为" + getType() + "出题");
    } else if (s.equals("切换为初中")) {
      paper = new MidProblem();
      count_type = "初中";
      System.out.println("当前选择为" + getType() + "出题");
    } else if (s.equals("切换为高中")) {
      paper = new HighProblem();
      count_type = "高中";
      System.out.println("当前选择为" + getType() + "出题");
    } else {
      System.out.println("请输入:切换为xx(xx为小学,初中,高中其中一个)");
    }
  }

分析:在切换类型的时候生成一个新的对象,但是后续又对原先的paper属性count_type进行修改,属于是画蛇添足。

  • Problem.Java
public abstract class Problem {
  abstract String create(); // 生成题目
  abstract void addOperator(String[] question); // 添加运算符
  public void addBrackets(String[] s); // 添加括号
}

分析:在抽象类中声明了两个抽象方法,子类继承时根据自身特性进行重载,有利于代码的复用和拓展,保证代码规范性。

public void addBrackets(String[] s) { // 添加括号
    int size = s.length;
    if (size > 2) {
      Random random = new Random();
      int begin = random.nextInt(size - 1);
      int end = random.nextInt(size - 1 - begin) + begin + 1;
      if (!s[begin].contains(")") && !s[end].contains("(") && end - begin != size - 1 && !(
          s[begin].contains("(") && s[end].contains(")"))) {
        s[begin] = "(" + s[begin];
        s[end] = s[end] + ")";
      }
    }
  }

分析:使用5个条件同时成立与否来进行添加括号与否的判断,逻辑上过于复杂,同时只能添加一对括号,可以针对上述情况进行优化调整。

  • PrimaryProblem.Java
public class PrimaryProblem extends Problem {

  @Override
  String create() {

    Random random = new Random();
    int size = random.nextInt(4) + 2; // 2到5个操作数
    String[] question = new String[size];
    for (int i = 0; i < size; i++) {
      question[i] = String.valueOf(random.nextInt(100) + 1);
    }
    addOperator(question);

    StringBuilder result = new StringBuilder();
    for (String s : question) {
      result.append(s);
    }
    result.append("=");
    return result.toString();
  }

  @Override
  void addOperator(String[] question) {
    Random random = new Random();
    int size = question.length;
    if (size > 2 && random.nextBoolean()) { //操作数大于二则考虑加括号
      addBrackets(question);
    }
    String[] operator = {"+", "-", "*", "/"};
    for (int i = 0; i < size - 1; i++) { // 添加符号 + - * /
      question[i] = question[i] + operator[random.nextInt(4)];
    }
  }

分析:使用String数组和随机数来进行出题,考虑到了小学题目必须要有两个操作数的情况,是一种出题的好方法,值得学习。

  • MidProblem.Java
    整体和PrimaryProblem并无太大差别,只是加入了对"√"和"²"的处理

  • HighProblem.Java

switch (op) {
        case 0:
          question[i] = "sin" + question[i];
          flag = true;
          break;
        case 1:
          question[i] = "cos" + question[i];
          flag = true;
          break;
        case 2:
          question[i] = "tan" + question[i];
          flag = true;
          break;
        case 3:
          if (!question[i].contains("√")) {
            question[i] = "√" + question[i];
          }
          break;
        case 4:
          if (!question[i].contains("²")) {
            question[i] = question[i] + "²";
          }
          break;
        default:
          addBrackets(question);
      }
    }
    if (!flag) {
      int op = random.nextInt(3);
      if (op == 0) {
        question[size - 1] = "sin" + question[size - 1];
      } else if (op == 1) {
        question[size - 1] = "cos" + question[size - 1];
      } else {
        question[size - 1] = "tan" + question[size - 1];
      }
    }

分析:在addOperator()方法中,重复地进行添加三角函数的操作,导致方法行数为41行违反了要求中方法行数不超过40行的要求T T
在后续询问中发现是由于ghw同学粗心大意修改方法后忘记删除,导致白白被扣掉10分,可怜他三秒QAQ
ghw同学的惨痛经历告诉我们,在最后的时候一定不要随意修改代码,要保证有充足的时间来检查!!!

四.总结

合理性

1.使用继承和抽象类:代码使用了继承和抽象类,通过 Problem 抽象类定义了数学问题生成的基本模板,然后通过 PrimaryProblem、MidProblem 和 HighProblem 具体子类实现了不同难度级别的问题生成逻辑。这种设计使得代码更具可扩展性,可以轻松添加新的数学问题类型。
2.随机性:使用 java.util.Random 来引入随机性,增加了生成问题的多样性。这对于生成多个随机问题是合理的,因为不同的问题可以具有不同的随机因素。
3.封装性:类的成员变量都被适当地封装,方法和变量的访问修饰符使用得当,符合面向对象编程的封装原则。
4.代码重用:通过将问题生成的通用逻辑放在 Problem 类中,不同难度级别的问题生成子类可以重用通用的代码,这是一个良好的设计实践。

不合理性

1.递归登录:在 Main 类中的 login() 方法中,当用户名或密码错误时,使用递归来重新尝试登录。这可能导致栈溢出错误,如果多次登录失败,递归的深度将会增加。更好的做法是使用循环来处理错误的登录尝试,以避免栈溢出。
2.代码注释:整个项目缺少缺少注释来解释其目的和关键部分的逻辑:如对属性的注释、对方法作用的注释,方法中复杂逻辑的解释,导致开始阅读时存在一定的困难。注释对于理解代码的重要性不可忽视。

ghw同学的项目充分使用了面向对象的思想,将很多功能进行了封装。功能模块划分清晰。程序中文字提示清晰,对用户体验良好。只是细节方面需要多加注意,比如属性名,类名,方法名等是否满足Google Java的规范,是否需要添加必要的注释来增加代码的可读性等

五.一点小小展望

在和ghw同学讨论的过程中,我们都发现彼此存在的一些问题,通过对问题的思考和解决,也让我们收获良多!
希望在后续的结对编程中,我们两个能吸取个人项目中的各种经验教训,对代码进行进一步的优化,做出更加合理优秀的程序xx

posted @ 2023-09-19 17:08  z00z  阅读(660)  评论(0编辑  收藏  举报