HNU软件工程导论个人项目代码评析

发布时间 2023-09-20 00:36:37作者: 葱爆刘马
项目作者:杜洋
博客作者:刘传煜
 
该工程大致可分为3部分:

  (1).MathBox、Math1、Math2、Math3为生成试题的字符串的类,其中MathBox为接口

  (2).User,Teacher为用户类,具有修改成员变量等基本函数。其中User为抽象类
 
  (3).TeacherSystem为出卷系统类。包含储存用户信息的Hashmap,交互界面以及登录、查重、创建试卷txt等功能。
 
程序整体执行逻辑如下:

  TeacherSystem 类的构造函数先将预置的用户信息插入 hashmap 。接下来在交互界面函数 interact() 中读取用户输入的 id 和 password ,传入 login() 方法查找 hashmap 进行比对,如果比对一致则修改 currentTeacher 类变量切换至登录状态.
 
  通过解析输入的命令,用户接下来可选择切换用户类型,输入出题数量或退出登录.
  
  若输入格式为"切换为小学/初中/高中",则直接调用用户类中 setType 函数修改.
  
  若输入为[10,30]的数字,则循环调用用户类型对应的Math类(Math1 or Math2 or Math3)中的generateQuestion() 函数,每次返回一条包含题目的字符串,并调用 distinguish() 函数查重比对。distinguish() 函数每次遍历用户对应文件里的试题,进行比对。
  
  若输入为"-1"则退出当前用户,重新登录.
 
项目结构如下:

 

 
代码运行效果:
 
登录功能:
缺陷是仅考虑输入不匹配的情况而未考虑用户不存在的情况导致异常.

 

出题功能:测试正常

 

切换难度功能:

同样的问题依然存在,如果输入的字符串越界就会出现异常.

 

 
具体代码实现如下:
 
  首先在主函数内仅初始化一个 TeacherSystem 类变量后调用其交互界面函数.
 public static void main(String[] args) {
        TeacherSystem sys = new TeacherSystem();
        sys.interact();
    }

 

  进入用户交互界面后,等待输入用户名及密码,接收到两个字符串后将其传入 login() 方法验证,如果验证通过就进行文字提示可以继续输入命令,否则就一直处于待登录状态,验证用户名以及密码.这里比较关键的处理就是对TeacherSystem成员变量-currentTeacher的灵活应用

  但是此处没有考虑缺省的输入状态,如果输入的字符串既非数字也非预先设定的"切换到XX"模式就会出现异常.

public void interact() {
        while(true) {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入用户名和密码");
            String str = scanner.nextLine();
            String[] tmp = str.split(" ");
            if(tmp.length != 2) {
                System.out.println("输入格式有误");
                continue;
            }
            String id = tmp[0];
            String pw = tmp[1];
            if((currentTeacher = login(id,pw)) != null) {
                System.out.println("登陆成功!");
            } else {
                continue;
            }/*登陆状态*/
            while(currentTeacher != null) {
                System.out.println("准备生成"+currentTeacher.getType()+"数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录):");
                String cmd = scanner.nextLine();
                if(cmd.length() == 5 && cmd.substring(0,3).equals("切换到")) {
                    modifyType(cmd.substring(3,5));
                    continue;
                } else {
                    int num = Integer.parseInt(cmd);
                    if(num == -1) {
                        currentTeacher = null;
                        break;
                    }
                    paperProduce(currentTeacher.getType(),num);
                }
            }
        }
    }

 

  成功登录后,继续解析输入的字符串,如果输入为"切换为小学/初中/高中",则调用modifyType()方法,根据输入的字符串调整用户类型.

public void modifyType(String newType) {
        if(newType.equals("小学") || newType.equals("初中") || newType.equals("高中")) {
            currentTeacher.setType(newType);
            System.out.println("修改成功");
        } else {
            System.out.println("请输入小学、初中和高中三个选项中的一个");
        }
    }

 

  如果解析字符串为合法数字则进入试卷生成阶段,首先根据currentTeacher确定题目难度,而题目难度由Math类的方法重载决定.

  而后进行文件处理逻辑:先保存当前路径并且定位到用户文件夹,而后创建相应格式的文件名,进而在对应文件夹位置创建以日期时间命名文件的文件.

  继而开始文件输入流的处理,通过相应的文件描述符开始输入,以一行为单位,每次输入一道题目,通过调用重载后的mathBox.questionGeneration()方法生成相应题目.

public void paperProduce(String type,int num) {
        if(num > 30 || num < 10) {
            System.out.println("3、题目数量的有效输入范围是“10-30”");
            return;
        }
        switchType(type);
        File f1 = new File("files\\"+currentTeacher.getId());
        if(!f1.exists()) {
            f1.mkdirs();
        }
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); // 日期格式化类
        Date date = new Date();
        String str = sdf.format(date);
        //System.out.print(str);
        File f = new File("files\\"+currentTeacher.getId()+"\\"+str+".txt");
        //System.out.println("file1的绝对路径:" + f.getAbsolutePath());
        try {
            f.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        } // 添加捕获异常
        try (
                FileWriter fw = new FileWriter(f);
                PrintWriter pw = new PrintWriter(fw);
                ) {
            for(int i = 1; i <= num; i++){
                String question = mathBox.questionGeneration();
                if(question.length() <4 || distinguish(question,f1,f) == true) {
                    i--;
                    continue;
                }
                pw.println(i + "." + question + "=");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

 

  为了满足查重要求,在mathBox.questionGeneration()方法返回生成的字符串题目后,还需要传入 distinguish() 方法进行处理.

  最外层循环用于遍历当前用户文件夹下的所有试卷,而后引入java自带的两个封装类FileReader类和BufferedReader类,前者用于读取打开的整个文件,后者每次取读入的文件的一行入buffer中,这里有助于性能上的提高,将其于生成的题目比较查看是否一致.

 public boolean distinguish(String question,File f1,File currFile) {
        File[] listFiles = f1.listFiles();
        for(File f : listFiles){
            if(f == currFile) continue;
            try (
                    FileReader fileReader = new FileReader(f);
                    BufferedReader br = new BufferedReader(fileReader);
            ) {
                while(true) {
                    String line = br.readLine();
                    if(null == line) { // 结束
                        break;
                    }
                    String[] lineArray = line.split("\\.");               
                    String questionExisted = lineArray[1].substring(0, lineArray[1].length() - 1); // 去除'='
                    if(questionExisted.equals(question)) {
                        System.out.println("出题重复" + question);
                        return true;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

 

  switchType() 方法扮演了类似mathBox工厂的角色,通过对象的多态性返回不同选择后被初始化的Math类对象

public void switchType(String type) {
        switch (type) {
            case "小学":
                mathBox = new Math1();
                break;
            case "初中":
                mathBox = new Math2();
                break;
            case "高中":
                mathBox = new Math3();
                break;
            default:
        }
    }

 

  不同难度的题目生成通过重载 MathBox 类的 questionGeneration() 方法完成.

public interface MathBox {
    public String questionGeneration();
}

 

  以生成小学题目为例:Math1实现接口MathBox的questionGeneration()方法

  @Override
    public String questionGeneration() {
        sb.setLength(0);
        Arrays.fill(openBraces,"");
        Arrays.fill(closeBraces,"");
        Random random = new Random();
        int num = random.nextInt(4)+2; // 4 [0,3] [2,5]小学题最少两个操作数 用宏写不太直观但便于后期修改
        for(int i = 0; i < num; i++) {
            opNum[i] = random.nextInt(100)+1; // 0,99 1,100
        }
        for(int j = 0; j < num-1; j++) {
            int tmp = random.nextInt(4); // 0,3
            operation[j] = operetionArray[tmp];
        }
        if(random.nextInt(2) == 1) { //加括号 0or1
            int rand1 = random.nextInt(num-1); // [0,num-2]
            int rand2 = random.nextInt(num-1-rand1)+rand1; // [rand1,3]
            openBraces[rand1] = "(";
            closeBraces[rand2] = ")";
        }
        sb.append(openBraces[0]); //左括号
        sb.append(opNum[0]); // 操作数
        for(int i = 0; i < num-2; i++) {
            sb.append(operation[i]); // 运算符
            sb.append(openBraces[i+1]); //左括号
            sb.append(opNum[i+1]); // 操作数
            sb.append(closeBraces[i]);//右括号
        }
        sb.append(operation[num-2]);
        sb.append(opNum[num-1]);
        sb.append(closeBraces[num-2]);
        return sb.toString();
    }

  同样的,通过实现该接口,使得生成不同难度(初中/高中)题目的功能实现得到了简化

 

程序优点分析:

 

  1.可拓展性强,用户使用抽象类,出题类使用接口,遵循设计上的依赖倒置原则,方便了后期拓展用户功能.

  2.产生卷子的路径不存在时,可以生成不存在的文件夹,使代码移植性高,而不需要下载整个程序.

  3.代码逻辑清晰,模块化设计良好,一个方法实现一个功能,代码逻辑性,可读性强。

  4.考虑到了异常处理,尽管仅是简单的打印异常信息,但对代码的健壮性是有帮助的。

  5.代码规范性高,使用一致的命名规范,以提高代码的可读性。确保类、方法和变量都具有描述性的名称,但是import类应避免使用通配符导入。

  6.文件操作是耗时的操作,尤其是在大量数据的情况下。考虑使用缓冲流来优化文件读写,以提高性能.

 

程序优化建议:

 

  1.程序在满足设计模式的单一职责原则上有待改进,一个类应该只有一个原因来引起它的变化。目前,TeacherSystem 类承担了太多的职责,包括用户交互、教师管理、题目生成等。并且在类的组织上,应该进一步考虑将不同的类放置在单独的源文件中,以提高代码的可维护性。每个类应该位于一个独立的源文件中.

  2.在程序执行逻辑中,直接用子类初始化MathBox显的较为臃肿,可以考虑设计模式中的工厂模式,以提高代码设计质量.

  3.关于异常处理,应考虑在设计中明确处理异常情况,而不仅仅是在代码中添加异常捕获。这可以提高代码的可预测性和可维护性,尤其在接收字符串输入方面就有欠考虑,未设置缺省的输入而可能导致异常.

  4.有些代码块在多个地方重复出现,例如文件路径的构建和创建文件的过程。可以将这些重复代码块提取成单独的方法以提高代码的重用性和可维护性。

  5.在文件描述符的管理上,在文件操作后,应该始终确保资源(如文件句柄)得到适当的关闭,以避免资源泄漏。

  6.在功能实现上,可以提示出试卷的保存路径,提升用户体验.

 

总结:

  综合来看,这段代码在实现基本功能上表现不错,但还有改进的空间,仍可以进行更高层次的抽象处理以降低功能之间的耦合,以提高代码质量,特别是在代码结构和可维护性、异常处理、安全性和性能方面。通过进一步的优化和重构,可以提高代码的质量和可维护性,使其更适用于实际应用。