软件工程-作业3
结对项目:自动生成四则运算题
| 这个作业属于哪个课程 | 班级链接 |
|---|---|
| 这个作业要求在哪里 | 作业链接 |
| 这个作业的目标 | 团队协作与沟通、代码规范与项目管理 |
合作人员:
- 张逸壕 3123004163
- 韩佳鑫 3123004142
GitHub 项目地址:automatically-generated-arithmetic
一、PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 15 | 10 |
| Estimate | 估计这个任务需要多少时间 | 20 | 20 |
| Development | 开发 | 180 | 200 |
| Analysis | 需求分析 (包括学习新技术) | 20 | 15 |
| Design Spec | 生成设计文档 | 20 | 10 |
| Design Review | 设计复审 | 10 | 5 |
| Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 10 | 5 |
| Design | 具体设计 | 10 | 5 |
| Coding | 具体编码 | 60 | 80 |
| Code Review | 代码复审 | 30 | 30 |
| Test | 测试(自我测试,修改代码,提交修改) | 30 | 20 |
| Reporting | 报告 | 30 | 45 |
| Test Repor | 测试报告 | 30 | 45 |
| Size Measurement | 计算工作量 | 20 | 10 |
| Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 10 | 20 |
| ·合计 | 495 | 520 |
二、计算模块设计
1. 接口设计与实现
类结构
ArithmeticGeneratorApplication: 程序入口,处理命令行参数GlobalExceptionHandler:异常处理器FileUtils: 文件读写工具类AnswerEvaluator: 答案生成器QuestionGenerator: 题目生成器Fraction:分数处理类
ArithmeticGeneratorApplication`->`FileUtils`->`QuestionGenerator`->`AnswerEvaluator&Fraction`->`FileUtils(写)`->`end`
算法关键:解析&生成&计算四则运算表达式
三、性能改进
通过合理的数据结构和算法,提高生成题目的效率
1.使用HashSet快速检查重复:
/**
* 生成指定数量和范围的数学题目
* @param numQuestions 题目数量
* @param range 运算范围
* @return {@code List<String> } 题目列表
* @author zyh
* @date 2025/03/16
*/
public static List<String> generateQuestions(int numQuestions, int range) {
// 用于存储不重复的题目 这里用hashset检查快
Set<String> uniqueQuestions = new HashSet<>();
// 存储生成的题目
List<String> questions = new ArrayList<>();
// 随机数生成器
Random rand = new Random();
// 生成题目
while (uniqueQuestions.size() < numQuestions) {
// 生成随机表达式
String question = generateRandomExpression(range, rand);
// 归一化表达式,避免重复
String normalized = normalizeExpression(question);
// 确保唯一性 检查uniqueQuestions是否存在该表达式
if (!uniqueQuestions.contains(normalized)) {
// 不存在则添加到哈希set便于后续快速筛查
uniqueQuestions.add(normalized);
// 添加到列表
questions.add(question + " =");
}
}
// 返回
return questions;
}
2.递归下降解析器和反射调用实现高效计算
// 解析表达式 (递归下降解析器的入口)
private static Result parseExpression(List<Token> tokens, int index) {
return parseAdditionSubtraction(tokens, index);
}
// 解析加减法 (处理优先级)
private static Result parseAdditionSubtraction(List<Token> tokens, int index) {
Result result = parseMultiplicationDivision(tokens, index);
Object currentValue = result.value;
index = result.nextIndex;
while (index < tokens.size()) {
Token token = tokens.get(index);
// 如果是加号或减号
if (token.type == Token.Type.OPERATOR && (token.value.equals("+") || token.value.equals("-"))) {
// 解析乘除法
Result nextResult = parseMultiplicationDivision(tokens, index + 1);
Object nextValue = nextResult.value;
index = nextResult.nextIndex;
// 调用对应的方法进行计算
currentValue = invokeMethod(currentValue, nextValue, token.value);
} else {
// 不是加减运算符,退出循环
break;
}
}
return new Result(currentValue, index);
}
// 解析乘除法 (处理优先级)
private static Result parseMultiplicationDivision(List<Token> tokens, int index) {
// 解析基本单元 (数字、括号内的表达式)
Result result = parsePrimary(tokens, index);
Object currentValue = result.value;
index = result.nextIndex;
while (index < tokens.size()) {
Token token = tokens.get(index);
// 如果是乘号或除号
if (token.type == Token.Type.OPERATOR && (token.value.equals("*") || token.value.equals("/"))) {
// 解析基本单元
Result nextResult = parsePrimary(tokens, index + 1);
Object nextValue = nextResult.value;
index = nextResult.nextIndex;
// 调用对应的方法进行计算
currentValue = invokeMethod(currentValue, nextValue, token.value);
} else {
// 不是乘除运算符,退出循环
break;
}
}
return new Result(currentValue, index);
}
// 解析基本单元 (数字、括号内的表达式)
private static Result parsePrimary(List<Token> tokens, int index) {
if (index >= tokens.size()) {
throw new IllegalArgumentException("Unexpected end of expression");
}
Token token = tokens.get(index);
// 如果是数字
if (token.type == Token.Type.NUMBER) {
// 统一转换为 Fraction 对象
Object value = new Fraction(token.value);
return new Result(value, index + 1);
// 如果是左括号
} else if (token.type == Token.Type.LPAREN) {
// 遇到左括号,递归解析括号内的表达式
int closingParenIndex = findClosingParen(tokens, index);
//如果没找到
if(closingParenIndex == -1){
throw new IllegalArgumentException("Mismatched parentheses");
}
// 递归调用, 注意这里要传入index + 1
Result result = parseExpression(tokens, index + 1);
// 跳过右括号
return new Result(result.value, closingParenIndex + 1);
} else {
throw new IllegalArgumentException("Unexpected token: " + token.value);
}
}
// 查找与给定左括号匹配的右括号的索引
private static int findClosingParen(List<Token> tokens, int openParenIndex) {
int parenCount = 1;
for (int i = openParenIndex + 1; i < tokens.size(); i++) {
Token token = tokens.get(i);
// 如果是左括号
if (token.type == Token.Type.LPAREN) {
parenCount++;
// 如果是右括号
} else if (token.type == Token.Type.RPAREN) {
parenCount--;
// 找到匹配的右括号
if (parenCount == 0) {
return i;
}
}
}
// 没有找到匹配的右括号
return -1;
}
3.Intellij Profiler分析:

四、单元测试
1.单元测试代码
@SpringBootTest
class ArithmeticGeneratorApplicationTests {
/**
* 确保除法生成合理分数
* @author zyh
* @date 2025/03/18
*/
@Test
public void testDivisionReasonableFraction() {
Random rand = new Random();
String expression = QuestionGenerator.generateRandomExpression(10, rand);
assertFalse(expression.contains("÷") && isSmaller(expression.split(" ")[0], expression.split(" ")[2]));
}
/**
* 生成多个不重复的题目
* @author zyh
* @date 2025/03/18
*/
@Test
public void testGenerateMultipleUniqueQuestions() {
List<String> questions = QuestionGenerator.generateQuestions(5, 10);
assertEquals(5, questions.size());
Set<String> uniqueQuestions = new HashSet<>(questions);
assertEquals(5, uniqueQuestions.size()); // 确保题目不重复
}
/**
* 操作数生成
* @author zyh
* @date 2025/03/18
*/
@Test
public void testGenerateOperand() {
Random rand = new Random();
String operand = QuestionGenerator.generateOperand(10, rand);
assertTrue(operand.matches("\\d+") || operand.matches("\\d+/\\d+"));
}
/**
* 表达式归一化
* @author zyh
* @date 2025/03/18
*/
@Test
public void testNormalizeExpression() {
String expression = "3 + 5";
String normalized = QuestionGenerator.normalizeExpression(expression);
assertEquals("3 + 5", normalized);
}
/**
* 确保减法不产生负数
* @author zyh
* @date 2025/03/18
*/
@Test
public void testSubtractionNoNegativeResult() {
Random rand = new Random();
String expression = QuestionGenerator.generateRandomExpression(10, rand);
assertFalse(expression.contains("-") && isSmaller(expression.split(" ")[0], expression.split(" ")[2]));
}
/**
* AnswerEvaluator测试计算答案
*/
@Test
public void testAnswerEvaluator() {
List<String> questions = QuestionGenerator.generateQuestions(5, 10);
List<String> answers = AnswerEvaluator.evaluateQuestions(questions);
assertEquals(5, answers.size());
}
/**
* 测试AnswerEvaluator评分
*/
@Test
public void testAnswerEvaluatorGrade() {
String result = AnswerEvaluator.gradeAnswers("Exercises.txt", "Answers.txt");
assertNotNull(result);
}
/**
* 测试Fraction类
*/
@Test
public void testFraction() {
assertEquals(new Fraction(1, 2), new Fraction("1/2"));
assertNotEquals(new Fraction(1, 2), new Fraction("1"));
}
/**
* 测试Fraction类的计算
*/
@Test
public void testFractionCalculation() {
assertEquals(new Fraction(1, 2), new Fraction("1/4").add(new Fraction("1/4")));
assertEquals(new Fraction(1, 2), new Fraction("1/2").subtract(new Fraction("0/2")));
}
/**
* 测试Fraction类的转化真分数
*/
@Test
public void testFractionToProper() {
assertEquals("1'1/2", new Fraction("3/2").toMixedNumberString());
assertNotEquals("1'1/2", new Fraction("4/2").toMixedNumberString());
}
}
2.覆盖率

五、异常处理
1.异常处理器
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* @param exception 异常
* @author zyh
* @date 2025/03/18
*/
@ExceptionHandler(value = Exception.class)
public void allException(Exception exception) {
// 返回错误结果
System.out.println(exception.getMessage());
}
}
2.异常设计代码
@SpringBootTest
public class ExceptionTest {
/**
* 范围参数不合法
* @author zyh
* @date 2025/03/18
*/
@Test
public void testInvalidRangeParameter() {
String[] args = {"-n", "10", "-r", "0"};
ArithmeticGeneratorApplication.main(args);
// 检查控制台输出是否包含错误信息
ByteArrayOutputStream errContent = new ByteArrayOutputStream();
System.setErr(new PrintStream(errContent));
ArithmeticGeneratorApplication.main(args);
// 验证错误信息
String expectedErrorMessage = "处理命令行参数时出错: bound must be positive";
assertTrue(errContent.toString().contains(expectedErrorMessage));
}
/**
* 文件不存在
* @author zyh
* @date 2025/03/18
*/
@Test
public void testFileNotExist() {
String[] args = {"-e", "NotExist.txt", "-a", "NotExist.txt"};
ByteArrayOutputStream errContent = new ByteArrayOutputStream();
System.setErr(new PrintStream(errContent)); // 重定向标准错误流
// 执行程序
ArithmeticGeneratorApplication.main(args);
// 验证错误信息
String expectedErrorMessage = "Error reading files: NotExist.txt (系统找不到指定的文件。)";
assertTrue(errContent.toString().contains(expectedErrorMessage));
}
/**
* 参数缺失
* @author zyh
* @date 2025/03/18
*/
@Test
public void testMissingRequiredParameter() {
String[] args = {"-n", "10"};
ArithmeticGeneratorApplication.main(args);
// 检查控制台输出是否包含帮助信息
// 可以通过重定向 System.out 来捕获输出
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
System.setOut(new PrintStream(outContent));
ArithmeticGeneratorApplication.main(args);
assertTrue(outContent.toString().contains("数学生成器"));
assertTrue(outContent.toString().contains("生成的题目数量"));
assertTrue(outContent.toString().contains("生成题目的数字范围"));
}
}
3.设计目标
1.testInvalidRangeParameter
-
目标:
- 范围参数不合法时抛出异常
- 打印错误信息,便于定位问题
-
场景:范围参数不合法
2. testFileNotExist
-
目标:
- 文件不存在时抛出异常,避免程序终止
- 打印错误信息,便于定位问题
-
场景:文件不存在
3. testMissingRequiredParameter
-
目标:
- 参数不足时立即抛出异常,阻止后续无效操作
- 提示正确命令格式,提升用户体验。
-
场景:参数缺失

浙公网安备 33010602011771号