- 评价人:雷茜
- 代码编写者:彭子芹
一、前言
本次博客是对“中小学数学试卷自动生成程序”也就是我们的个人项目的互评,我和我的互评对象使用的编程语言都是java,虽然我们都是按照Google编码规范编写的代码,但是我们的编程习惯和风格有很大的区别,我们对整个项目的入手思路和分类实现很不一样,这也让我看到了自己的一些不足之处,让我对于项目程序的设计和撰写方面有了不少的收获。
二、功能测试
-
测试环境
IntelliJ IDEA 2022.2.2 -
测试过程
错误的账户密码不能登录且会重新回到登录界面,good!
输入正确的账户密码,成功登录,即使以一种***钻的方式输入也ok!
能够识别出账户的类型,默认准备出该类型的题目,在输入不在10-30范围内的数时会提示“输入有误”,并让我们重新输入,直到输入正确的指令
输入0可以切换生成题目类型,再输入需要切换的类型即可,这里也可以判断出输入的类型是否是小初高三个选项中的一个,如果不是也能正确识别并让用户重新输入直到输入正确的选项才切换成功(虽然这里没有通过“切换为XX”来进行切换,但是体验感也非常好)。
任何时候输入-1都可以退出登录。
题目的存储是通过相对路径存储的,每一个用户的题目都存在一个文件夹下,很规范美观。
题目文档的命名也是年-月-日-时-分-秒.txt很符合项目要求。
总体测试下来,用户体验感很好,提示很规范易懂,界面也很整洁,每一步操作都会有提示告诉用户操作是否规范以及是否成功,而且项目中也有自己的小设计,比如那个用0来切换,以及设置了一个回车按键的函数,让每一次胡扯进入下一步,防止出现输入过多回车导致后面的输入紊乱。
三、代码分析
- 前面的代码都是基于一个用户的视角来进行的测试(看不到源码),着重于功能是否完善和体验感是否良好,而接下来我将以一名软件工程专业的学生的角度来分析和评价我的互评对象的项目,主要分析一下她的整个项目结构和代码。
- 整体结构
首先观看一下她的代码生成的UML类图,不难发现有一个User类和一个UsedQuestion类是完全独立出来了的,没有与其他类交互,既没有继承也没有依赖,这是一个不太好的处理。但是我们也可以看到她把功能分的很细,程序的可读性很强,且每个类和每个方法之间的耦合度比较低,这是有利于我们后期对于这个程序的维护的。
接着,来看看她的项目结构,她对于每一种服务都有专门的软件包存储,对于不同的功能有不同的类来存储,比如dao中存储的就是涉及到数据库连接与操作的类,generate里存的就是生成题目的方法,其中包含了一个抽象类和三个继承后的类,这是很清晰易懂且利于维护的。再看她的依赖结构矩阵,主要是dao数据库方面的类依赖项比较多,其他的依赖性并没有很强。但是这个module类感觉就有点多余了,正如前面所说User和UsedQuestion与其他类没有交互和依赖关系。
``
点击查看代码
package dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
/**
* 这是拥有关于对question表操作的dao层.
* */
public class QuestionDao {
private Connection conn;
public QuestionDao(Connection conn) {
this.conn = conn;
}
/**
* 这是查找question表中的问题的函数,主要用于查重.
* */
public int searchQuestion(String question, String account) throws Exception {
String sql = "select * from used_question where teacher_account = ? and question = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, account);
pstmt.setString(2, question);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) { //下一行有数据返回1,没有则返回0
return 1;
}
return 0;
}
/**
*这个是往查重的question表中添加使用过问题的函数.
*/
public void addQuestion(String account, String question) throws Exception {
String sql = "insert into used_question(teacher_account,question) values(?,?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, account);
pstmt.setString(2, question);
pstmt.executeUpdate();
}
}
这是UsedQuestion类的全部代码,但是实际上最后彭子芹是通过数据库来查重的,具体做法就是把每一道题目和账户名都插入数据库,再把题目和账户名那两列设置为不可重复,这样在生成题目插入数据库时数据库就可以检测到同一账户名下生成的重复的题目,在这一过程中根本就没有用到这个UsedQuestion类。
-
编码标准
本次个人项目要求所有人需要遵循Google编码规范,很遗憾,在这一步检查的时候她的代码没有完全满足这个要求,有两处不规范。
-
精简和高复用度
这次的个人项目算是一个比较小的项目,但以后我们如果参与更大的项目,那么代码的重复和高耦合性都将影响到代码的运行效率和后期维护,除此之外,一些不规范的编码习惯也会给我们的程序带来一些“隐患”,所以我们要从小项目开始提高自己代码的精简和复用,减少程度代码隐藏错误和缺陷。这里我使用了PMD插件来检查代码的重复和代码隐藏错误和缺陷。
从上面PMD检测出的结果来说,我们可以看到她的代码中有不少“隐患”,比如说 useStringbufferforStringappand规则警告,说明她的代码中有一些使用‘+’的方式来拼接字符串,如果需要频繁修改字符串(生成题目时会频繁修改字符串)最好使用Stringbuffer类(能提高代码运行效率);再比如CloseResourse警告,我们可以看到下面的dao软件包中的UseDao类的代码中每一次进行完对数据库的操作后都没有及时调用close()来关闭连接,没有及时释放资源,以及前面的QuestionDao类的代码也是如此。
CloseResourse
package dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
/**
* 这是拥有关于操作user表的dao层.
*/
public class UserDao {
private Connection conn;
public UserDao(Connection conn) {
this.conn = conn;
}
/**
* 查找用户生成试卷数量的函数.
*/
public int searchUserPaperNum(String account) throws Exception { //没问题
String sql = "select paper_num from user where account = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, account);
ResultSet rs = pstmt.executeQuery();
rs.next(); //在调用getString之前要使用,此时的光标在结果集之上
String result = rs.getString(1);
if (result != null) {
return Integer.parseInt(result.trim());
}
return 0;
}
/**
* 用户每生成一张试卷,他的试卷数目会更新,使用此函数更新.
*/
public void addUserPaperNum(int paperNum, String account) throws Exception { //没问题
String sql = "update user set paper_num = ? where account = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, paperNum);
pstmt.setString(2, account);
pstmt.execute();
}
/**
* 通过dao层对用户的账户密码的检验.
*/
public boolean checkUserPwd(String account, String password) throws Exception {
String sql = "select * from user where account = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, account);
ResultSet rs = pstmt.executeQuery();
if (!rs.next()) {
return false;
} //不存在返回数据
String rightAccount = rs.getString("account");
String rightPwd = rs.getString("password");
if (account.equals(rightAccount) && password.equals(rightPwd)) { //字符串判断相等要用equal 不知道什么原因
return true;
}
return false;
}
/**
* 返回用户类型.
*/
public String getUserType(String account) throws Exception {
String sql = "select type from user where account = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, account);
ResultSet rs = pstmt.executeQuery();
if (!rs.next()) {
return null;
}
return rs.getString("type");
}
}
还有ReturnEmptyCollectionThanNull警告,在上面三个生成题目的函数中一旦捕获到异常都是返回null,最好还是抛出异常而不是返回null,不然有可能会出现空指针异常的问题。
ReturnEmptyCollectionThanNull
/**
* 生成小学问题.
*/
public static String[] primary(Connection conn, int questionNumber, String account) {
QuestionDao questionDao = new QuestionDao(conn);
Primary primary = new Primary();
int number = 0;
String[] questions = new String[questionNumber];
while (number < questionNumber) {
try {
String question = primary.question();
int resultNum = questionDao.searchQuestion(question, account);
if (resultNum != 0) {
//未执行
continue;
} else {
questionDao.addQuestion(account, question);
questions[number] = question;
number++;
}
} catch (Exception e) {
return null;
}
}
return questions;
}
/**
* 生成中学问题.
*/
public static String[] middle(Connection conn, int questionNumber, String account) {
QuestionDao questionDao = new QuestionDao(conn);
Middle middle = new Middle();
int number = 0;
String[] questions = new String[questionNumber];
while (number < questionNumber) {
try {
String question = middle.question();
int resultNum = questionDao.searchQuestion(question, account); //现在的问题是要完善此查重函数
if (resultNum != 0) {
//未执行
continue;
} else {
questionDao.addQuestion(account, question);
questions[number] = question;
number++;
}
} catch (Exception e) {
return null;
}
}
return questions;
}
/**
* 生成高中问题.
*/
public static String[] senior(Connection conn, int questionNumber, String account) {
QuestionDao questionDao = new QuestionDao(conn);
Senior senior = new Senior();
int number = 0;
String[] questions = new String[questionNumber];
while (number < questionNumber) {
try {
String question = senior.question();
int resultNum = questionDao.searchQuestion(question, account); //现在的问题是要完善此查重函数
if (resultNum != 0) {
//未执行
continue;
} else {
questionDao.addQuestion(account, question);
questions[number] = question;
number++;
}
} catch (Exception e) {
return null;
}
}
return questions;
}
- 代码复杂度
对于阅读代码的人来说,越简单的代码越好理解和维护,对于程序来说,代码复杂度越低的效率自然也越高,所以在分析代码时也要考虑到代码复杂度的问题,如果发现代码复杂度过高,可以想办法优化一下,提高代码的可读性和程序的效率。这里我使用Metrics来分析了她的代码复杂度。
我们可以看到所有的方法的ev(G)值都很低,这说明代码的程序非结构化程度的比较低,修改起来不那么容易出bug,代码维护较为容易;所有方法的iv(G)也比较低,说明与其他模块之间的耦合度不算高,利于隔离和复用;所有方法中除了Senior.question()方法以外v(G)都比较低,圈复杂度数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数,圈复杂度较高说明程序代码可能质量低且难于测试和维护,所以这里来要分析一下她这个圈复杂度高的的Senior.question()方法。
Senior.question()方法代码
public String question() { //超过40行了,删一下空行以及将break与语句放在同一行就好了
Random rand = new Random();
String []signs = { "+", "-", "*", "/", "=" };
String []operation = null;
String display = ""; //题目
int operationNum = 0;
operationNum = rand.nextInt(5) + 1; //操作数1~5
operation = new String[operationNum];
for (int i = 0; i < operationNum; i++) {
operation[i] = String.valueOf(rand.nextInt(100) + 1);
}
boolean []flag = new boolean[operationNum];
int specialSign = rand.nextInt(operationNum) + 1; //1~操作数个特殊符号
for (int i = 0; i < specialSign; i++) {
int index = rand.nextInt(operationNum);
if (i == 0) {
int cases = rand.nextInt(3);
switch (cases) {
case 0: operation[index] = "sin" + operation[index];
break;
case 1: operation[index] = "cos" + operation[index];
break;
case 2: operation[index] = "tan" + operation[index];
break;
default: break;
}
} else {
if (!flag[index]) {
int cases = rand.nextInt(5);
switch (cases) {
case 0: operation[index] = "sin" + operation[index];
break;
case 1: operation[index] = "cos" + operation[index];
break;
case 2: operation[index] = "tan" + operation[index];
break;
case 3: operation[index] = operation[index] + "²";
break;
case 4: operation[index] = "√" + operation[index];
break;
default: break;
}
}
}
flag[index] = true;
}
if (operationNum > 3 && rand.nextBoolean()) {
int length = rand.nextInt(operationNum - 2) + 1;
int a = rand.nextInt(operationNum - length);
int b = a + length;
operation[a] = "(" + operation[a];
operation[b] = operation[b] + ")";
}
for (int i = 0; i < operationNum - 1; i++) {
display = display + operation[i] + signs[rand.nextInt(4)];
}
display = display + operation[operationNum - 1] + signs[4];
return display;
}
- 代码