个人项目代码分析
目录导航
1 前言
2 个人项目需求
3 队友个人项目分析
3.1 运行结果展示
3.2 代码逻辑
3.2.1 小学表达式生成
3.2.2 初高中表达式生成
3.2.3 判断重复
3.2.4 生成试卷
3.2.5 控制台界面
3.3 代码规范
3.4 整体设计
4 优缺点分析
4.1 优点
4.2 缺点
1 前言
历时一周的个人项目结束了,个人项目难度并不是很大,但是要注意编码规范。确定基本思路后初代版本我其实一天就写完了,但这一版本中存在很多问题,例如没有事先定义抽象类,各种方法都放在实体类里,甚至当时只有两个实体类(Paper类和User类),Paper类中定义了一堆静态方法,在User类中直接调用Paper类的一个静态方法来实现功能,虽然达到了方法拆分的目的,但是思维仅仅停留在了面向过程的层面上,这样的设计会使得代码维护变得极其困难。后面又重新设计了整个项目,定义了抽象类,增加类的数量,降低类与类之间的耦合程度,并对一些算法进行了优化,最终也是做出了一个符合要求的项目。个人项目结束后就轮到结对编程项目了,在结对编程开始后要先分析一下队友的个人项目代码,下面我将从代码逻辑、代码规范与整体设计上进行分析,并给出其代码的优缺点。
本文是对辜玫致同学个人项目的分析与评价。
2 个人项目需求
本次个人项目的需求如下:
用户:
小学、初中和高中数学老师。
功能:
1、命令行输入用户名和密码,两者之间用空格隔开(程序预设小学、初中和高中各三个账号,具体见附表),如果用户名和密码都正确,将根据账户类型显示“当前选择为XX出题”,XX为小学、初中和高中三个选项中的一个。否则提示“请输入正确的用户名、密码”,重新输入用户名、密码;
2、登录后,系统提示“准备生成XX数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录):”,XX为小学、初中和高中三个选项中的一个,用户输入所需出的卷子的题目数量,系统默认将根据账号类型进行出题。每道题目的操作数在1-5个之间,操作数取值范围为1-100;
3、题目数量的有效输入范围是“10-30”(含10,30,或-1退出登录),程序根据输入的题目数量生成符合小学、初中和高中难度的题目的卷子(具体要求见附表)。同一个老师的卷子中的题目不能与以前的已生成的卷子中的题目重复(以指定文件夹下存在的文件为准,见5);
4、在登录状态下,如果用户需要切换类型选项,命令行输入“切换为XX”,XX为小学、初中和高中三个选项中的一个,输入项不符合要求时,程序控制台提示“请输入小学、初中和高中三个选项中的一个”;输入正确后,显示“”系统提示“准备生成XX数学题目,请输入生成题目数量”,用户输入所需出的卷子的题目数量,系统新设置的类型进行出题;
5、生成的题目将以“年-月-日-时-分-秒.txt”的形式保存,每个账号一个文件夹。每道题目有题号,每题之间空一行;

3 队友个人项目分析
3.1 运行结果展示
登录界面:

有提示,且可选择登录或退出系统
输入单个参数:

程序不会卡死,而是会提示请输入正确的用户名密码,说明对这一方面注意了判断。
输入错误的用户名密码:

程序发现没有用户名和密码与之对应,于是登录不成功,功能正常。
输入正确的用户名密码:

正常登录,且有提示
登录后随便输入文字:

提示请按要求输入,然后,用户可以继续输入,若输入了正确的信息,仍可完成相应的功能,不会受到之前错误输入的影响。
登录后输入切换为xx,但xx不在选项内:

提示请输入小学、初中和高中三个选项中的一个,同样,用户可以继续输入,程序不会受到之前错误输入的影响。
多次输错:

程序仍然不会受到影响,若输入了正确内容,则程序会给出正确的相应
登录后输入数字,但不在范围内:

提示不在限制范围内,同样之后可以正常输入。
登录后输入-1:

成功退出当前用户,并且可以重新登录。
登录后输入正确的数字:

成功生成题目,检查后确实存在“李四”这一文件夹,且文件夹下却有一份txt文件,以“年-月-日-时-分-秒.txt”的形式命名。
该文件的内容如下(这里仅展示部分,一个共有30道题,题目数量没有问题):

格式符合要求,难度符合要求
测试能否连续出题:

每次登录可以多次出题
登录后输入切换为xx,xx在选项之内:

切换后,相应的提示文字也发生了变化
此时,再输入生成题目数量,查看文件:


生成的确实是符合要求的高中题目
再尝试小学题目,生成的文件如下:

符合小学题目要求
最后,尝试退出系统:

成功退出了系统
综上所述,程序功能正常,生成题目符合要求,控制台给出的提示也很到位,满足了需求内容。
3.2 代码逻辑
3.2.1 小学表达式生成
表达式生成的方法在MathPaper.java文件中,这个文件存在一个继承自抽象类Paper的实体类MathPaper,该实体类即包含了表达式生成的方法。他的表达式生成是通过对字符串进行操作来完成的。
首先定义了一个生成小学数学题目的方法generatePrimaryQuestion:
1 static String generatePrimaryQuestion(String[] symbol, Random random) { // 生成小学的数学题目 2 String question; 3 while (true) { 4 question = ""; 5 int operand = 0;//当前总的操作数的个数 6 int operand1 = 0; // 表当前括号之间的操作数的个数 7 int brackets = random.nextInt(3) + 1;//括号对个数,取值为1-3 8 int sub = 0;//左括号与右括号数目的差值 9 while (true) { 10 if (random.nextInt(2) == 1 && brackets > 0) {//有1/2的概率先加入左括号 11 question = question + "("; 12 sub++; 13 brackets--;//括号对个数要减1 14 operand1 = 0;//当前括号之间的操作数的个数为0 15 } 16 int num1 = random.nextInt(100) + 1;//0-100的数 17 question = question + num1; 18 operand1++;//当前括号中的操作数个数加1 19 operand++;//式子总的操作数个数加1 20 if (random.nextInt(2) == 1 && sub > 0 && operand1 > 1) {//当左右括号数差值大于0且操作数多于1时,有1/2概率加入右括号 21 question = question + ")"; 22 sub--; 23 } 24 if (brackets == 0 && sub == 0 && operand > 1) {//当括号都用完时,如果操作数个数大于1则生成可以结束 25 break; 26 } 27 question += symbol[random.nextInt(4)];//随机加入符号 28 } 29 if (operand <= 5) { //除去操作数大于5式子 30 break; 31 } 32 } 33 return question; 34 }
利用Java的Random类产生随机数实现随机生成功能。可以看到他使用了两个while(true)循环,外层循环当且仅当生成的生成的表达式满足操作数限制(不超过5个)时退出,内层循环则根据事先随机选择的括号对个数来生成表达式,逻辑是每次循环都有一定概率添加左括号,并记录下左右括号的差值,若差值大于零且操作数大于1就有一定概率添加右括号,这样做是为了避免出现括号内仅包含一个数值,即形如:(15)这样的表达式出现。同时,每一次循环都会随机生成一个操作数。当生成的左括号达到括号对数量要求后,不再生成左括号,继续生成操作数和右括号直到满足左右括号差值为0且操作数数量大于1为止。这时退出内层循环,进入外层循环判断生成的表达式是否符合要求,符合则退出,返回一个成功生成的合法表达式,否则再重复内层循环的过程,直到生成出一个合法表达式为止。
3.2.2 初高中表达式生成
生成初中表达式的方法为generateSeniorQuestion,生成高中表达式的方法为generateHighQuestion。
1 static String generateSeniorQuestion(String[] symbol, Random random) { // 生成初中的数学题目 2 int square = random.nextInt(2) + 1;//表示平方或者开方的个数,至少为1 3 String question = generatePrimaryQuestion(symbol, random); // 先生成一个只有加减乘除和括号的式子 4 String[] numbers = question.split("[+\\-*/]"); // 再把操作数提取出来 5 while (square > 0) { 6 int num = random.nextInt(numbers.length); //先随机拿一个操作数 7 String string = numbers[num]; 8 if (random.nextInt(2) == 1) { // 加入开方 9 numbers[num] = symbol[5] + numbers[num]; 10 } else { // 加入平方 11 numbers[num] = numbers[num] + symbol[4]; 12 } 13 square--; 14 question = question.replace(string, numbers[num]); // 再替换掉原来的操作数 15 } 16 return question; 17 } 18 19 static String generateHighQuestion(String[] symbol, Random random) { // 生成高中的数学题目 20 int trigon = random.nextInt(2) + 1;//表示三角函数个数,至少为1 21 String question = generateSeniorQuestion(symbol, random); // 先生成一个初中的式子 22 String[] numbers = question.split("[+\\-*/]"); // 再把操作数提取出来 23 while (trigon > 0) { 24 int num = random.nextInt(numbers.length); //先随机拿一个操作数 25 String string = numbers[num]; 26 numbers[num] = symbol[6 + random.nextInt(3)] + numbers[num]; // 加入三角函数符号 27 trigon--; 28 question = question.replace(string, numbers[num]); // 再替换掉原来的操作数 29 } 30 return question; 31 } 32 }
这两个方法的逻辑是一样的,都是先随机选择要生成的新操作符个数,然后在前一级题目的基础上,提取出操作数,并对该操作数加入新的操作符,然后再替换掉原来的操作数。
3.2.3 判断重复
生成试卷前首先要做的工作是去重。老师给的重复的定义是:当只有两个操作数时,形如1+2和2+1这类仅交换操作数顺序的题目算作重复,其余情况则当已生成试卷的题目中有与即将生成的题目相同,则算重复。
1 Boolean checkRepeat(String question) { // 检查题目是否重复,如果重复则返回1 2 for (MathPaper mathPaper : mathPapers) { 3 for (String string : mathPaper.questions) { 4 if (string.contains(question)) //如果被以前题目的字符串包含了就认为重复 5 return true; 6 String[] numbers = question.split("[+\\-*/]"); 7 if (numbers.length == 2) { // 检查当只有两个操作数的时候是否满足交换律重复 8 String newQuestion = numbers[1] + question.charAt(numbers[0].length()) + numbers[0]; // 交换两个操作数的顺序 9 if (string.contains(newQuestion)) 10 return true; 11 } 12 } 13 } 14 return false; 15 }
mathPapers的定义如下:
ArrayList<MathPaper> mathPapers = new ArrayList<>();
当用户登录后,其文件夹下的试卷将通过loadPapersFromFile方法加载进这个列表中:
1 void loadPapersFromFile() { 2 mathPapers.clear(); 3 String folderPath = Client.paperFolder + File.separator + getAccount(); // 指定文件夹路径 4 List<String[]> stringArraysList = new ArrayList<>(); // 创建一个列表来存储多个字符串数组 5 File folder = new File(folderPath); 6 File[] files = folder.listFiles(); 7 if (files != null) { 8 for (File file : files) { 9 if (file.isFile() && file.getName().endsWith(".txt")) { // 检查文件是否为文本文件 10 String[] stringArray = Client.readStringArrayFromFile(file); 11 if (stringArray != null) { // 如果成功读取字符串数组,将其添加到列表中 12 stringArraysList.add(stringArray); 13 } 14 } 15 } 16 } 17 // 输出读取到的多个字符串数组内容 18 for (String[] stringArray : stringArraysList) { 19 MathPaper MathPaper = new MathPaper(stringArray.length); 20 Collections.addAll(MathPaper.questions, stringArray); 21 mathPapers.add(MathPaper); 22 } 23 } 24 }
操作数大于2时直接判断,操作数等于2时分割字符串,判断交换后是否相等即可。但是这里有错误,首先他在分割时也会分割形如2/1和1/2这类表达式,并判断出操作数交换后相等,所以会认为他们重复,但是这两个表达式明显不同。所以在判断时,应对除法加以考虑。其次他在判断中使用的是contain方法,即被以前题目的字符串包含了就判断重复,这显然不合理。我们假设原文件存在一个表达式:1+2*3,现在生成了一个表达式1+2,则会被判断为重复,但这两个表达式明显不同。综上所述,这个判断重复的代码逻辑存在很大问题。
3.2.4 生成试卷
生成试卷的方法在MathTeacher.java文件中被定义,方法名为generatePaper。
1 void generatePaper(int num) { 2 MathPaper mathPaper = new MathPaper(num); 3 String[] symbol = new String[]{"+", "-", "*", "/", "^2", "√", "sin", "cos", "tan"}; 4 Random random = new Random(); 5 for (int i = 1; i <= num; i++) { 6 String question; 7 if (getType().equals("小学")) { 8 question = MathPaper.generatePrimaryQuestion(symbol, random); 9 } else if (getType().equals("初中")) { 10 question = MathPaper.generateSeniorQuestion(symbol, random); 11 } else { 12 question = MathPaper.generateHighQuestion(symbol, random); 13 } 14 if (question.charAt(0) == '(' && question.charAt(question.length() - 1) == ')') { // 把被括号包围的式子给去掉括号 15 String newQuestion = question.substring(1, question.length() - 1); 16 if (newQuestion.indexOf("(") <= newQuestion.indexOf(")")) { // 这是应该去掉括号的情况 17 question = newQuestion; 18 } 19 } 20 if (checkRepeat(question)) { 21 i--; 22 continue; 23 } 24 String question1 = i + ". " + question + " =\n"; 25 mathPaper.questions.add(question1); 26 } 27 mathPapers.add(mathPaper); 28 writeMathPaperToFile(mathPaper); 29 System.out.println("题目生成完毕\n"); 30 }
这段代码的主要逻辑就是根据用户的类别及输入的生成题目的数量调用对应的生成题目方法并生成指定数量的题目,在每生成一道题目后进行查重,重复则重新生成,直到满足生成数量,最后调用writeMathPaperToFile方法将题目写入文件。同时,在生成的过程中把被括号包围的式子去掉括号,避免出现形如(1+2+3)这类的表达式。
3.2.5 控制台界面
控制台界面的代码在Client.java文件中,这里仅列出几个关键的方法。
首先是登录界面:
1 static User login() { 2 Scanner scanner = new Scanner(System.in); 3 System.out.println("请输入账号和密码,两者之间用空格隔开"); 4 String input = scanner.nextLine(); 5 String[] strings = input.split("\\s+"); 6 if (strings.length != 2) 7 return null; 8 String account = strings[0]; 9 String password = strings[1]; 10 for (User user : users) { 11 if (user.getAccount().equals(account) && user.getPassword().equals(password)) {//存在用户满足该用户名和密码 12 System.out.println("登录成功!\n"); 13 System.out.println("当前选择为" + user.getType() + "出题\n"); 14 return user; 15 } 16 } 17 return null; 18 }
若查找到则返回对应的user对象,否则返回null。
然后是处理用户输入的方法:
1 static void doJob(User user) { 2 Scanner scanner = new Scanner(System.in); 3 MathTeacher mathTeacher = (MathTeacher) user; 4 mathTeacher.loadPapersFromFile(); // 加载试卷 5 while (true) { 6 System.out.println("准备生成" + user.getType() + "数学题目,请输入生成题目数量(题目数量限制为10-30,如果输入-1将退出当前用户,则重新登录):"); 7 System.out.println("或者您也可以输入“切换为XX”,XX的选择为小学,初中或高中"); 8 String num = scanner.nextLine(); 9 if (num.contains("切换为")) { 10 if (num.equals("切换为小学")) { 11 user.setType("小学"); 12 } else if (num.equals("切换为初中")) { 13 user.setType("初中"); 14 } else if (num.equals("切换为高中")) { 15 user.setType("高中"); 16 } else { 17 System.out.println("请输入小学、初中和高中三个选项中的一个\n"); 18 } 19 } else if (num.equals("-1")) { 20 System.out.println("退出当前用户成功\n"); 21 break; 22 } else if (num.matches("\\d+")) { 23 int num1 = Integer.parseInt(num); 24 if (num1 < 10 || num1 > 30) { 25 System.out.println("不在限制的输入范围内,请重新输入\n"); 26 continue; 27 } 28 mathTeacher.generatePaper(num1); 29 } else { 30 System.out.println("请按要求输入"); 31 } 32 } 33 }
首先加载试卷,为了之后去重用,接下来进入while(true)循环,不断读取用户输入,读取到的字符串若含有“切换为”则进一步判断要切换的身份,并给登录者赋予新的type。num.matches("\\d+")的含义是判断输入是否有且只有数字,若是则表明用户输入的是希望生成的题目数量,则进一步判断数量是否在范围内,若在则调用generatePaper方法生成符合难度、对应数量题目的试卷。若用户随意输入了一段内容,则会进入else判断,提示请按要求输入。另外,用户若输入了-1,则会跳出while循环,相当于退出了当前用户并重新登录。
最后是main函数:
1 public static void main(String[] args) { 2 Scanner scanner = new Scanner(System.in); 3 loadUsersFromFile(); // 把用户列表加载进来 4 System.out.println("**************************欢迎来到中小学数学卷子自动生成系统*********************************\n"); 5 while (true) { 6 System.out.println("登录请按0,退出系统请按1"); 7 String flag = scanner.nextLine(); 8 if (flag.matches("\\d+")) { 9 if (Integer.parseInt(flag) == 0) { 10 User user = login();//查找当前用户 11 if (user == null) { 12 System.out.println("请输入正确的用户名、密码"); 13 continue; 14 } 15 doJob(user); 16 } else if (Integer.parseInt(flag) == 1) { 17 System.out.println("退出成功\n"); 18 break; 19 } else { 20 System.out.println("请输入0到1的数字\n"); 21 } 22 } else { 23 System.out.println("请输入0到1的数字\n"); 24 } 25 } 26 saveUsersToFile(); 27 }
main函数也是使用了一个while(true)循环,主要处理用户刚进入界面时的选择,用户输入0则登录,输入1则退出系统。在用户输入0后,调用login函数查找是否存在和输入的账号密码对应的user对象,若返回为null则说明不存在,提示用户输入正确的用户名和密码。若用户输入了其他内容,则会提示用户输入0到1的数字。
3.3 代码规范
根据Google Java编程规范,他的代码在几个重要的编程规范上做的很好:
1.垂直空白。他在类内连续的成员之间都有一个空行(连续两个字段之间的空行是可选的,并不是必要的),且package语句、import语句和class之间每个部分都有一个空行,很好地满足了编程规范的要求。
2.import语句没有使用通配符(*),符合编程规范。
3.水平空白。在分隔任何保留字与紧随其后的左括号前,分隔任何保留字与紧随其后的右大括号后、任何左大括号前、任何二元或三元运算符的两侧、在 , : ; 及右括号后、类型与变量之间均有空格,符合编程规范。
4.命名符合规范,包名均小写,类名以 UpperCamelCase 风格编写,方法名都以 lowerCamelCase 风格编写,其他非常量字段名、参数名以及局部变量名也都符合编程规范。
5.源文件结构合理,一个源文件(.java)中只有一个顶级类。
6.代码满足左大括号前不换行,左大括号后换行,右大括号前换行,如果右大括号是一个语句、函数体或类的终止,则右大括号后换行; 否则不换行。(例如,如果右大括号后面是else或逗号,则不换行),符合编程规范。
但是,我也发现了他不规范的一处:
如果在一条语句后做注释,则双斜杠(//)两边都要空格。而他的代码中有些注释不符合这个规范。(见3.2.1贴出的代码)
3.4 整体设计
整体设计并不复杂,他对于每一个实体类,总是先定义了一个抽象类,然后让这个实体类继承这个抽象类,实体类中定义了各种方法来实现功能。但是,他的整体设计存在一些问题,这将在后面分析缺点时详细阐述。
4 优缺点分析
4.1 优点
1.代码风格符合编程规范(具体优点见3.3),代码清晰易懂,且注释也很清晰。
2.经过反复测试,代码很好地实现了个人项目的需求,在启动程序后,程序会先提示输入0登录,输入1退出,提供了退出程序的功能。登录时,程序可以准确判断账号密码是否正确、输入是否合法,若仅输入了一个参数,程序不会卡死,而是会提示“请输入正确的账号密码”。在登录成功后,程序开始处理用户输入,且能通过用户输入准确作出反应,例如用户输入“切换为xx”时,若xx为“小学、初中、高中”中的一个,则程序会执行身份切换,若不是,则程序会提示“请输入小学、初中、高中”三个选项中的一个,若用户输入-1,则会退出当前用户重新登录,若用户仅输入除-1外的数字,则程序会判定用户想要生成题目,进而继续判断输入的数字是否在范围内。若用户随便输入,则程序会提示“输入不合法”。生成试卷后,试卷会以txt文件形式保存在用户对应文件夹中,且命名规范,题目也符合要求。总之,程序很好地实现了需求中的功能,考虑到了很多特殊情况,非常全面,生成的表达式也符合需求,试卷格式也完全正确。
3.代码中的每个方法行数都不超过40行,将一些较复杂的功能进行了拆分,形成了多个简单的功能,最后再综合在一起,这样便于排查错误与维护,且增强了可读性。
4.2 缺点
主要是设计结构不太合理。抽象类像是强行添加上去的,没有起到实质的作用。例如,以下是Paper抽象类的代码:
1 package papergeneratesystem; 2 3 import java.util.ArrayList; 4 5 abstract class Paper { 6 int num; // 题目数量 7 ArrayList<String> questions = new ArrayList<>(); 8 }
然后,仅有MathPaper类继承了它,看起来这个抽象类似乎是没有什么实质作用,因为对这些成员变量的定义完全可以直接放在MathPaper类中。而且我在排查完所有代码后,没有找到Paper出现的痕迹,所有地方都是直接使用的MathPaper类,没有体现多态的思想。换句话说,缺点就是整体设计中面向对象的设计理念比较薄弱,没有很好地降低类与类之间的耦合程度,抽象类像是为了设计而设计的。
事实上,抽象类的目的在于可以更细致化的表明哪些是不同的,哪些是相同的,抽象类可以默认实现某些方法, 就不需要对一些方法重复实现相同的功能,实现代码复用。也可以定义抽象方法交给不同的子类去实现。子类可以重写抽象类的方法,以实现多态。
另外一个小缺点就是在功能实现的效率方面,在3.2.1中我介绍了他小学表达式生成的逻辑,使用了两层永真循环,在众多随机生成的表达式中强行选出符合要求的表达式,将效率交给运气(毕竟如果运气比较差的话,可能生成了很多表达式都不符合要求,效率就会很慢),有点太暴力了。对于这个小规模的需求还好,并不会太明显,但若规模增大,这种算法效率上的劣势就会暴露无遗了。最好是人为对随机行为进行一些约束,而不是等随机行为结束后再进行筛选。
最后有一个小建议,这个不算是缺点,对这个个人项目需求的实现也可采用表达式树的形式实现,即可以构建一棵表达式二叉树,这个树的根节点均为运算符,叶节点均为操作数,中序遍历并适当加括号就可以构建出完整的表达式,同时利用这个二叉树也可以很方便地完成求值,以实现给出试卷题目的同时也能给出这份试卷的答案。
浙公网安备 33010602011771号