结对项目-自动生成小学四则运算题目命令行程序
(一)
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/homework/13479 |
这个作业的目标 | <结对编程项目,实现一个自动生成小学四则运算题目的命令行程序> |
github仓库(master分支) | https://github.com/hypocodeemia/hypocodeemia |
github仓库具体地址 | https://github.com/hypocodeemia/hypocodeemia/tree/master/math_exercise |
成员姓名 | 成员学号 |
---|---|
林嘉俊 | 3123004446 |
王梓涵 | 3123002706 |
(二)PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 45 |
Estimate | 估计这个任务需要多少时间 | 20 | 30 |
Development | 开发 | 650 | 700 |
Analysis | 需求分析 (包括学习新技术) | 60 | 80 |
Design Spec | 生成设计文档 | 40 | 50 |
Design Review | 设计复审 | 20 | 25 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 15 | 20 |
Design | 具体设计 | 60 | 70 |
Coding | 具体编码 | 180 | 200 |
Code Review | 代码复审 | 30 | 35 |
Test | 测试(自我测试,修改代码,提交修改) | 75 | 90 |
Reporting | 报告 | 90 | 110 |
Test Repor | 测试报告 | 30 | 40 |
Size Measurement | 计算工作量 | 15 | 20 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 45 | 50 |
合计 | 880 | 995 |
(三)效能分析
操作类型 | 总耗时(total time/cpu time) |
---|---|
生成10000道题 | 180ms/180ms |
![]() |
|
---------- | --------------------------------------------------------------------- |
检查答案对错 | 84ms/84ms |
![]() |
①生成10000道题
函数 | 耗时占比 | parent |
---|---|---|
ExerciseServiceImpl.generateExercises | 92.85% | 总 |
CommandLineController.writeExercisesToFile | 7.14% | 总 |
---- | ---- | ---- |
ExerciseServiceImpl.generateAndValidateExercise | 100% | ExerciseServiceImpl.generateExercises |
---- | ---- | ---- |
ExerciseServiceImpl.generateSingleExercise | 76.92% | ExerciseServiceImpl.generateAndValidateExercise |
ValidationServiceImpl.isExpressionNonNegative | 23.07% | ExerciseServiceImpl.generateAndValidateExercise |
---- | ---- | ---- |
ExerciseServiceImpl.generateFourOperandExercise | 40% | ExerciseServiceImpl.generateSingleExercise |
ExerciseServiceImpl.generateThreeOperandExercise | 40% | ExerciseServiceImpl.generateSingleExercise |
ExerciseServiceImpl.generateTwoOperandExercise | 20% | ExerciseServiceImpl.generateSingleExercise |
关键发现:
- 分数除法运算是最大的性能瓶颈
- 四操作数题目生成比两操作数题目生成更耗时
- 文件写入操作占据时间比例较小
②检查10000题答案对错
函数 | 耗时占比 | parent |
---|---|---|
ExerciseServiceImpl.checkAnswers | 71.42% | 总 |
CommandLineController.readLinesFromFile | 14.28% | 总 |
CommandLineController.writeGradeToFile | 14.28% | 总 |
---- | ---- | ---- |
ExpressionEvaluator.evaluate | 60% | ExerciseServiceImpl.checkAnswers |
FractionUtil.parseFraction | 20% | ExerciseServiceImpl.checkAnswers |
ExpressionEvaluator.processOperation | 33.33% | ExpressionEvaluator.evaluate |
关键发现: |
- 计算数学表达式的结果是答案检查过程的主要性能瓶颈
- 表达式求值占据了检查逻辑的大部分时间
- 分数解析也有一定的性能开销
③改进思路
整体来看,程序性能表现合格,能够处理大规模题目生成和检查任务。
改进思路:
- 优化核心算法:重点改进分数运算和表达式求值算法
- 减少IO操作:优化文件读写性能
- 缓存优化:对常用计算结果进行缓存
- 并行处理:对可并行操作进行优化
具体改进措施:
-
分数运算优化
优化了Fraction.divide方法的实现,减少中间对象的创建
改进了最大公约数计算算法,使用更高效的欧几里得算法实现 -
表达式求值优化
优化了ExpressionEvaluator的栈操作,减少不必要的对象创建
改进了运算符优先级处理逻辑 -
文件IO优化
使用更大的缓冲区进行文件读写
优化了文件编码处理逻辑
(四)设计实现过程
(1)项目结构
点击查看代码
math_exercise/
├── src/
│ └── main/
│ │ └── java/
│ │ └── com/linjiajun/math_exercise/
│ │ ├── MathExerciseApplication.java # Spring Boot主应用类
│ │ ├── controller/
│ │ │ └── CommandLineController.java # 命令行控制器
│ │ ├── service/
│ │ │ ├── ExerciseService.java # 题目生成服务接口
│ │ │ ├── ValidationService.java # 验证服务接口
│ │ │ ├── impl/
│ │ │ ├── ExerciseServiceImpl.java # 题目生成服务实现类
│ │ │ └── ValidationServiceImpl.java # 验证服务实现类
│ │ │
│ │ │
│ │ ├── bean/
│ │ │ ├── Exercise.java # 题目模型类
│ │ │ └── Fraction.java # 分数模型类
│ │ └── util/
│ │ ├── ExpressionEvaluator.java # 表达式求值器
│ │ └── FractionUtil.java # 分数工具类
│ └── test/
│ └── java/
│ └── com/linjiajun/math_exercise/
│ ├── MathExerciseApplicationTests.java # 测试类
├── target/
│ └── math_exercise-0.0.1-SNAPSHOT.jar # 打包生成的可执行JAR
├── Exercises.txt # 生成的题目文件
├── Answers.txt # 生成的答案文件
├── Grade.txt # 答案检查统计文件
└── pom.xml # Maven项目配置文件
(2)关键流程图
①题目生成
②检查答案对错
(3)简略调用关系
点击查看代码
用户命令行输入
↓
CommandLineController
↓
ExerciseServiceImpl ←→ ValidationServiceImpl
↓
ExpressionEvaluator ←→ FractionUtil
↓
Fraction + Exercise
(五)代码说明
(1)关键需求实现
①生成的题目中计算过程不能产生负数
点击查看代码
/**
* 验证整个表达式是否满足非负要求 - 核心验证逻辑
* 确保所有中间步骤和最终结果都不为负数
*/
public boolean isExpressionNonNegative(String expression) {
try {
String cleanExpression = expression.replace("=", "").replace(" ", "");
return evaluateAllSteps(cleanExpression);
} catch (Exception e) {
log.debug("表达式验证失败: {}", expression, e);
return false;
}
}
/**
* 评估表达式的所有计算步骤,确保中间步骤和最终结果都非负
*/
private boolean evaluateAllSteps(String expression) {
Stack<Fraction> numbers = new Stack<>();
Stack<String> operators = new Stack<>();
int i = 0;
while (i < expression.length()) {
char c = expression.charAt(i);
// 解析逻辑...
if (isOperator(c)) {
while (!operators.isEmpty() &&
precedence(operators.peek()) >= precedence(String.valueOf(c))) {
// 关键:每一步运算都检查结果非负
if (!processOperationWithCheck(numbers, operators)) {
return false;
}
}
operators.push(String.valueOf(c));
i++;
}
// ... 其他解析逻辑
}
// 处理剩余运算符
while (!operators.isEmpty()) {
if (!processOperationWithCheck(numbers, operators)) {
return false;
}
}
// 最终结果也应该是非负的
Fraction finalResult = numbers.pop();
return isNonNegative(finalResult);
}
/**
* 带检查的运算处理 - 核心减法验证
*/
private boolean processOperationWithCheck(Stack<Fraction> numbers, Stack<String> operators) {
String op = operators.pop();
Fraction right = numbers.pop();
Fraction left = numbers.pop();
Fraction result;
switch (op) {
case "+":
result = left.add(right);
break;
case "-":
result = left.subtract(right);
// 关键:检查减法结果是否为负
if (!isNonNegative(result)) {
return false; // 发现负数,立即返回失败
}
break;
case "×":
result = left.multiply(right);
break;
case "÷":
result = left.divide(right);
break;
default:
throw new IllegalArgumentException("未知运算符: " + op);
}
numbers.push(result);
return true;
}
实现思路:
- 逐步骤验证:在表达式求值的每一步都检查结果是否非负
- 立即失败:一旦发现负数结果立即终止验证
- 最终确认:确保最终答案也非负
- 全面覆盖:处理括号、运算符优先级等复杂情况
②生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数
点击查看代码
/**
* 验证除法运算是否合法 - 确保结果为真分数
*/
@Override
public boolean isValidDivision(Fraction left, Fraction right) {
// 1. 检查除数不为零
if (right.getNumerator() == 0 && right.getWhole() == 0) {
log.debug("除数为零");
return false;
}
// 2. 计算除法结果
Fraction result = left.divide(right);
// 3. 严格验证:结果必须是真分数
boolean isValid = result.isProperFraction();
if (!isValid) {
log.debug("除法结果不是真分数: {} ÷ {} = {}", left, right, result);
}
return isValid;
}
/**
* 真分数判断标准 - 严格定义
*/
public boolean isProperFraction() {
// 整数部分必须为0
if (whole != 0) {
return false;
}
// 分子绝对值必须小于分母
if (Math.abs(numerator) >= denominator) {
return false;
}
// 分母不能为1(否则就是整数)
if (denominator == 1) {
return false;
}
// 分子不能为0(否则就是0)
if (numerator == 0) {
return false;
}
return true;
}
- 定义:真分数必须满足四个条件:
- 整数部分为0
- 分子绝对值小于分母
- 分母不为1
- 分子不为0
- 计算验证:先计算结果再验证,确保准确性
- 立即拒绝:不满足条件立即拒绝该题目
③每道题目中出现的运算符个数不超过3个
点击查看代码
/**
* 生成单个题目,根据运算符数量选择不同的生成策略
* @param range 数值范围
* @param index 题目编号
* @return 生成的题目,如果生成失败返回null
*/
private Exercise generateSingleExercise(int range, int index) {
int operatorCount = random.nextInt(3) + 1;
try {
switch (operatorCount) {
case 1:
return generateTwoOperandExercise(range, index);
case 2:
return generateThreeOperandExercise(range, index);
case 3:
return generateFourOperandExercise(range, index);
default:
return null;
}
} catch (Exception e) {
log.debug("生成题目失败: {}", e.getMessage());
return null;
}
}
④程序一次运行生成的题目不能重复
点击查看代码
/**
* 获取规范化后的表达式
* 用于题目去重比较,移除空格并统一运算符表示
* @return 规范化后的表达式字符串
*/
public String getNormalizedExpression() {
return expression.replace(" ", "").replace("×", "*").replace("÷", "/");
}
@Override
public List<Exercise> generateExercises(int count, int range) {
Set<Exercise> exercises = new HashSet<>();
int attempts = 0;
// 增加最大尝试次数
int maxAttempts = count * 20;
while (exercises.size() < count && attempts < maxAttempts) {
Exercise exercise = generateAndValidateExercise(range, exercises.size() + 1);
if (exercise != null && !exercises.contains(exercise)) {
exercises.add(exercise);
}
attempts++;
}
if (exercises.size() < count) {
log.warn("只成功生成了 {} 道题目,目标数量为 {}", exercises.size(), count);
}
return new ArrayList<>(exercises);
}
(2)关键代码
①表达式求值核心算法
点击查看代码
/**
* 计算数学表达式的结果
* 使用操作数栈和运算符栈,按照运算符优先级进行计算
* @param expression 数学表达式字符串
* @return 计算结果(分数形式)
* @throws IllegalArgumentException 如果表达式格式错误或包含不支持的操作
*/
public static Fraction evaluate(String expression) {
// 移除空格和等号
expression = expression.replace(" ", "").replace("=", "");
Stack<Fraction> numbers = new Stack<>();
Stack<String> operators = new Stack<>();
int i = 0;
while (i < expression.length()) {
char c = expression.charAt(i);
if (c == '(') {
operators.push("(");
i++;
} else if (c == ')') {
while (!"(".equals(operators.peek())) {
processOperation(numbers, operators);
}
// 移除 "("
operators.pop();
i++;
} else if (isOperator(c)) {
while (!operators.isEmpty() && precedence(operators.peek()) >= precedence(String.valueOf(c))) {
processOperation(numbers, operators);
}
operators.push(String.valueOf(c));
i++;
} else {
// 解析数字或分数
StringBuilder sb = new StringBuilder();
while (i < expression.length() &&
(Character.isDigit(expression.charAt(i)) ||
expression.charAt(i) == '/' ||
expression.charAt(i) == '\'')) {
sb.append(expression.charAt(i));
i++;
}
numbers.push(parseFraction(sb.toString()));
}
}
while (!operators.isEmpty()) {
processOperation(numbers, operators);
}
return numbers.pop();
}
设计思路:
- 采用双栈算法
- 操作数栈存储分数对象,运算符栈存储运算符和括号
- 正确处理运算符优先级:乘除优先于加减
- 支持括号改变运算顺序
- 时间复杂度 O(n),空间复杂度 O(n)
②题目生成与验证
点击查看代码
/**
* 生成单个题目并确保符合所有约束条件
*/
private Exercise generateAndValidateExercise(int range, int index) {
int attempts = 0;
while (attempts < 50) {
Exercise exercise = generateSingleExercise(range, index);
if (exercise != null &&
validationService.isExpressionNonNegative(exercise.getExpression())) {
return exercise;
}
attempts++;
}
return null;
}
/**
* 验证运算的合法性 - 多层验证确保题目质量
*/
private boolean isOperationValid(Fraction left, Fraction right, String operator, String fullExpression) {
// 第一层:基础运算验证
boolean basicValid;
switch (operator) {
case "-":
basicValid = validationService.isValidSubtraction(left, right);
break;
case "÷":
basicValid = validationService.isValidDivision(left, right);
break;
default:
basicValid = true;
}
if (!basicValid) {
return false;
}
// 第二层:完整表达式验证
return validationService.isExpressionNonNegative(fullExpression);
}
设计思路:
- 采用多次尝试机制,确保生成符合条件的题目
- 多层验证链:基础验证 → 表达式验证 → 范围验证 → 结果验证
- 确保减法结果非负、除法结果为真分数
- 验证整个表达式的所有中间步骤
③分数运算
点击查看代码
/**
* 分数规范化 - 确保分数始终处于最简形式
*/
private void normalize() {
if (denominator == 0) {
throw new IllegalArgumentException("分母不能为零");
}
// 处理分母为负的情况
if (denominator < 0) {
numerator = -numerator;
denominator = -denominator;
}
// 约分:求分子分母的最大公约数
int gcd = gcd(Math.abs(numerator), denominator);
numerator /= gcd;
denominator /= gcd;
// 处理假分数:转换为带分数
if (Math.abs(numerator) >= denominator) {
whole += numerator / denominator;
numerator = Math.abs(numerator) % denominator;
}
// 如果分子为0,重置整数部分
if (numerator == 0) {
whole = 0;
}
}
/**
* 分数除法运算
*/
public Fraction divide(Fraction other) {
// 转换为假分数进行计算
int num1 = this.toImproperFraction().numerator;
int den1 = this.toImproperFraction().denominator;
int num2 = other.toImproperFraction().numerator;
int den2 = other.toImproperFraction().denominator;
// 分数除法:乘以倒数
int newNum = num1 * den2;
int newDen = den1 * num2;
return new Fraction(newNum, newDen);
}
设计思路:
- 规范化确保分数处于最简形式
- 支持真分数、带分数、自然数的统一处理
- 使用欧几里得算法计算最大公约数进行约分
- 四则运算都基于假分数进行计算,避免复杂逻辑
(六)测试运行
测试方法均在MathExerciseApplicationTests类中
测试编号 | 测试内容 | 结果 |
---|---|---|
1-testGenerateExercisesDirectly | 调用生成题目方法 | 正常运行,得到对应文件 |
2-testCheckAnswersDirectly | 调用检查答案方法 | 正常运行,得到对应文件 |
3-testParamLost | 测试缺失参数-r的情况 | 不会得到题目和答案文件,系统会打印程序使用帮助信息 |
4-testIllegalParam | 测试不合规参数的情况(-n的参数为负数) | 会提示"题目数量必须大于0,数值范围必须大于1" |
5-testWrongDirectly | 测试文件地址异常 | 会显示文件读取错误:xxx文件 |
6-testNoNegativeResults | 测试负数约束验证 | 正常运行 |
7-testDivisionResultsAreProperFractions | 除法结果真分数验证 | 正常运行 |
8-testOperatorCountLimit | 测试运算符个数限制 | 正常运行 |
9-testExerciseUniqueness | 测试题目去重功能 | 正常运行 |
10-testLargeScalePerformance | 测试大规模性能 | 正常运行 |
点击查看代码
@Slf4j
@SpringBootTest
class MathExerciseApplicationTests {
@Autowired
private CommandLineController commandLineController;
@Autowired
private ExerciseService exerciseService;
@Autowired
private ValidationService validationService;
@Test
void contextLoads() {
}
/**
* 等效于执行: java -jar math_exercise-0.0.1-SNAPSHOT.jar -n 10000 -r 10
* 直接调用Controller处理生成题目的逻辑
*/
@Test
public void testGenerateExercisesDirectly() {
log.info("开始直接调用生成题目方法...");
String[] args = {"-n", "10000", "-r", "10"};
commandLineController.processCommand(args);
log.info("生成题目方法调用完成");
}
/**
* 等效于执行: java -jar math_exercise-0.0.1-SNAPSHOT.jar -e Exercises.txt -a Answers.txt
* 直接调用Controller处理检查答案的逻辑
*/
@Test
public void testCheckAnswersDirectly() {
log.info("开始直接调用检查答案方法...");
String[] args = {"-e", "Exercises.txt", "-a", "Answers.txt"};
commandLineController.processCommand(args);
log.info("检查答案方法调用完成");
}
@Test
public void testParamLost() {
log.info("开始测试缺失参数-r的情况...");
String[] args = {"-n", "10000"};
commandLineController.processCommand(args);
}
@Test
public void testIllegalParam() {
log.info("开始测试不合规参数的情况...");
String[] args = {"-n", "-1", "-r", "10"};
commandLineController.processCommand(args);
}
@Test
public void testWrongDirectly() {
log.info("开始测试文件地址异常...");
String[] args = {"-e", "121.txt", "-a", "1424.txt"};
commandLineController.processCommand(args);
}
@Test
public void testNoNegativeResults() {
log.info("测试负数约束验证...");
List<Exercise> exercises = exerciseService.generateExercises(100, 10);
int negativeCount = 0;
int expressionViolationCount = 0;
for (Exercise exercise : exercises) {
// 验证所有题目答案都不为负数
if (exercise.getAnswer().getNumerator() < 0) {
negativeCount++;
log.error("发现负数答案: {} = {}", exercise.getExpression(), exercise.getAnswer());
}
// 验证表达式本身不包含会导致负数的运算
if (!validationService.isExpressionNonNegative(exercise.getExpression())) {
expressionViolationCount++;
log.error("表达式违反非负要求: {}", exercise.getExpression());
}
}
assertEquals("不应有负数答案", 0, negativeCount);
assertEquals("不应有违反非负要求的表达式", 0, expressionViolationCount);
log.info("✓ 成功验证100道题目均无负数结果");
}
@Test
public void testDivisionResultsAreProperFractions() {
log.info("测试除法结果真分数验证...");
List<Exercise> exercises = exerciseService.generateExercises(100, 10);
int divisionCount = 0;
int improperFractionCount = 0;
for (Exercise exercise : exercises) {
if (exercise.getExpression().contains("÷")) {
divisionCount++;
// 验证除法结果必须是真分数
if (!exercise.getAnswer().isProperFraction()) {
improperFractionCount++;
log.error("除法结果不是真分数: {} = {}",
exercise.getExpression(), exercise.getAnswer());
}
}
}
assertEquals("所有除法结果都应是真分数", 0, improperFractionCount);
log.info("✓ 验证{}道除法题目,结果均为真分数", divisionCount);
}
@Test
public void testOperatorCountLimit() {
log.info("测试运算符个数限制...");
List<Exercise> exercises = exerciseService.generateExercises(50, 10);
int violationCount = 0;
for (Exercise exercise : exercises) {
String expression = exercise.getExpression();
// 统计运算符数量
long operatorCount = expression.chars()
.filter(ch -> ch == '+' || ch == '-' || ch == '×' || ch == '÷')
.count();
// 验证运算符数量不超过3个
if (operatorCount > 3) {
violationCount++;
log.error("运算符个数超过3个: {}", expression);
}
}
assertEquals("所有题目运算符个数应不超过3个", 0, violationCount);
log.info("✓ 验证50道题目运算符个数均不超过3个");
}
@Test
public void testExerciseUniqueness() {
log.info("测试题目去重功能...");
List<Exercise> exercises = exerciseService.generateExercises(100, 10);
Set<String> normalizedExpressions = new HashSet<>();
int duplicateCount = 0;
for (Exercise exercise : exercises) {
String normalized = exercise.getNormalizedExpression();
if (normalizedExpressions.contains(normalized)) {
duplicateCount++;
log.error("发现重复题目: {}", exercise.getExpression());
}
normalizedExpressions.add(normalized);
}
assertEquals("不应有重复题目", 0, duplicateCount);
log.info("✓ 成功生成100道不重复题目");
}
@Test
public void testLargeScalePerformance() {
log.info("测试大规模性能...");
long startTime = System.currentTimeMillis();
List<Exercise> exercises = exerciseService.generateExercises(10000, 10);
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// 验证生成10000道题目在合理时间内完成
assertTrue("生成10000道题目应在5秒内完成,实际耗时: " + duration + "ms", duration < 5000);
assertEquals("应生成10000道题目", 10000, exercises.size());
log.info("✓ 成功生成10000道题目,耗时: {}ms", duration);
}
}
通过这些测试用例,我能够:
- 验证功能完整性:所有核心功能都经过测试
- 确保需求符合性:严格验证了所有数学规则约束
- 验证性能表现:确认程序能够高效处理大规模数据
- 检查错误处理:确保程序能够优雅处理各种错误情况
(七)项目小结
项目启示
这个项目不仅是一个技术实践,更是一次完整的软件工程体验。我们深刻体会到:
- 质量源于设计:良好的架构设计是项目成功的基础
- 测试保障质量:完善的测试体系是代码质量的保证
- 协作提升效率:有效的团队协作能产生1+1>2的效果
结对感受
开发者A:林嘉俊
在结对编程过程中,我学会了更好地沟通技术方案,通过代码审查发现了自己忽略的细节问题。合作伙伴对测试的重视让我发现了更多潜在的问题.
对对方的评价:
- 对细节的把握非常到位,发现了多个关键的业务逻辑问题
- 测试用例设计全面,覆盖了各种边界情况
建议:可以更早开始性能测试和相关优化
开发者B:王梓涵
这个项目让我对软件工程的全流程有了更完整的认识,从需求分析、架构设计到测试部署。特别是在验证逻辑设计和异常处理方面收获很大。
与合作伙伴的讨论激发了很多思路,比如表达式规范化去重等。代码复审环节帮助我们发现了很多潜在问题。
对对方的评价:
- 算法实现能力强
- 代码结构清晰,注释完整,便于理解和维护
建议:可以更多进行代码的逻辑测试,异常处理