四则运算器
软件工程作业
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 软件工程 |
这个作业要求在哪里 | 作业要求 |
开发人员 | 张伟聪 3123004247 吴钊鑫 3123004244 |
github链接
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | 估计这个任务需要多少时间 | 40 | 45 |
Development | 开发 | ||
· Analysis | 需求分析 (包括学习新技术) | 125 | 135 |
· Design Spec | 生成设计文档 | 65 | 60 |
· Design Review | 设计复审 | 20 | 25 |
· Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 25 | 30 |
· Design | 具体设计 | 65 | 70 |
· Coding | 具体编码 | 75 | 80 |
· Code Review | 代码复审 | 25 | 30 |
· Test | 测试(自我测试,修改代码,提交修改) | 95 | 110 |
Reporting | 报告 | ||
· Test Report | 测试报告 | 50 | 55 |
· Size Measurement | 计算工作量 | 15 | 12 |
· Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 30 | 38 |
Total | 合计 | 600 | 650 |
项目分包
softwork [SoftwareProject]
├── out#运行jar包
│ ├── artifacts
│ │ ├── SoftwareProject_jar
│ │ │ ├── SoftwareProject.jar
├── src#项目源文件
│ ├── main
│ │ ├── java
│ │ │ ├── com.zhang
│ │ │ │ ├── generator#生成器
│ │ │ │ │ ├── ExpressionGenerator.java#表达式生成器
│ │ │ │ │ ├── ProblemGenerator.java#问题生成器
│ │ │ │ ├── grader#评分器
│ │ │ │ │ ├── Grader.java#评分类
│ │ │ │ ├── model#数据模型
│ │ │ │ │ ├── Expression.java#表达式类
│ │ │ │ │ ├── Fraction.java#分数类
│ │ │ │ │ ├── Operator.java#运算符类
│ │ │ │ │ ├── Problem.java#问题类
│ │ │ │ ├── utils#工具类
│ │ │ │ │ ├── CommandLineParser.java#命令行解析器
│ │ │ │ │ ├── ExpressionParser.java#表达式解析器
│ │ │ │ │ ├── RPNEvaluator.java#后缀表达式求值器
│ │ │ │ |—— Main.java#主类
│ ├── resources#资源
├── test#测试
│ ├── java
│ │ ├── GeneratorTest.java#生成器测试
│ │ ├── ProblemGeneratorTest.java#问题生成器测试
项目设计流程与模块分析
1. 项目结构概览
该项目的结构分为四大部分:
out(运行目录)
包含项目生成的 .jar 文件,主要用于项目的打包和运行。
src(源代码目录)
存放项目的所有源代码文件,按照功能分为几个包:
- generator(生成器包):包括
ProblemGenerator
和ExpressionGenerator
,负责生成运算表达式和题目。 - grader(评分器包):包含
Grader
类,负责对生成的问题进行评分或检查答案。 - model(数据模型包):包括
Expression
类、Fraction
类、Operator
类和Problem
类,主要用于定义数据结构。 - utils(工具包):包括
CommandLineParser
类、ExpressionParser
类、RPNEvaluator
类等工具类,提供项目所需的各种功能支持。 - test(测试目录):存放测试代码,主要是对生成器和问题生成器的功能进行单元测试。
2. 各个模块与类的功能分析
生成器模块(generator):
- ExpressionGenerator.java:该类负责生成数学表达式。通常,生成的表达式可能包含加、减、乘、除等基本运算符,还可能涉及分数等更复杂的数学元素。
- ProblemGenerator.java:负责生成具体的题目。这些题目可以是简单的四则运算,也可以是带有括号、分数等元素的复杂题目。
评分器模块(grader):
- Grader.java:该类负责根据给定的答案和生成的问题进行评分。它可能会评估问题的答案是否正确,或者根据不同的评分标准给出不同的分数。
数据模型模块(model):
- Expression.java:该类表示一个数学表达式,包含表达式的组成部分(如操作数、运算符等)。它可能会提供方法来解析和计算表达式。
- Fraction.java:用于表示分数,支持分数的基本运算,如加减乘除等。
- Operator.java:表示运算符。可以扩展为支持更多类型的运算符(如加减乘除、取余等)。
- Problem.java:表示一个题目,包含题目的表达式、答案以及可能的附加信息(如难度级别等)。
工具类模块(utils):
- CommandLineParser.java:负责解析命令行输入,允许用户通过命令行传入参数,例如生成题目的数量、类型等。
- ExpressionParser.java:用于解析数学表达式,将用户输入的字符串转化为表达式对象,方便后续计算。
- RPNEvaluator.java:用于计算后缀表达式的值。支持栈式算法来解析后缀表达式并返回结果。
- Main.java:程序的主类,负责启动项目的核心功能。
3. 项目核心流程
- 启动程序:
Main.java
类作为入口,负责初始化项目,处理命令行参数,启动表达式生成和问题生成。 - 生成表达式:通过
ExpressionGenerator
类生成表达式,可能需要使用ExpressionParser
来解析表达式中的运算符和操作数。 - 生成问题:
ProblemGenerator
使用表达式生成器生成具体问题,并将其包装成Problem
对象,供后续使用。 - 评分与反馈:生成的问题可以交给
Grader
类进行评分,判断答案是否正确并提供反馈。 - 命令行交互:用户通过命令行输入参数,
CommandLineParser
解析这些输入,动态调整问题的生成方式。
4. 测试与质量保证
项目包含了两个主要的测试类:
- GeneratorTest.java:测试
ExpressionGenerator
和ProblemGenerator
的功能,确保生成的表达式和问题符合预期。 - ProblemGeneratorTest.java:专门测试问题生成器的准确性,验证生成的问题是否符合设计要求。
优化改进
程序的主要计算密集型任务主要集中在以下几个方面:
表达式生成(ExpressionGenerator)
四则运算计算(RPNEvaluator)
问题评分(Grader)
命令行参数解析(CommandLineParser)
1. 表达式生成 (ExpressionGenerator
)
package com.zhang.generator;
import com.zhang.model.Operator;
import java.util.Random;
public class ExpressionGenerator {
private static final Random random = new Random();
public static String generateExpression(int maxValue) {
int num1 = random.nextInt(maxValue) + 1;
int num2 = random.nextInt(maxValue) + 1;
char operator = Operator.getRandomOperator();
return num1 + " " + operator + " " + num2;
}
}
📌 代码解析:
- 生成两个随机数(
num1
和num2
)。 - 通过
Operator.getRandomOperator()
随机选取运算符(+、-、×、÷)。 - 拼接成数学表达式(如
3 + 5
)。
⚡ 性能优化:
-
避免重复调用
Random
:
- 直接预生成一组随机数,减少
nextInt()
的调用次数:
java复制编辑private static final int[] RANDOM_NUMBERS = new Random().ints(10000, 1, 100).toArray(); private int getRandomNumber() { return RANDOM_NUMBERS[random.nextInt(RANDOM_NUMBERS.length)]; }
- 直接预生成一组随机数,减少
-
支持更复杂的表达式
:
- 通过递归生成嵌套运算,如
(3 + 5) × (2 - 1)
。
- 通过递归生成嵌套运算,如
🔹 2. 逆波兰表达式求值 (RPNEvaluator
)
package com.zhang.utils;
import java.util.Stack;
public class RPNEvaluator {
public static int evaluate(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for (String token : tokens) {
switch (token) {
case "+": stack.push(stack.pop() + stack.pop()); break;
case "-":
int b = stack.pop(), a = stack.pop();
stack.push(a - b); break;
case "*": stack.push(stack.pop() * stack.pop()); break;
case "/":
int divisor = stack.pop(), dividend = stack.pop();
stack.push(dividend / divisor); break;
default: stack.push(Integer.parseInt(token));
}
}
return stack.pop();
}
}
📌 代码解析:
- 遍历后缀表达式(RPN)。
- 使用栈(Stack)进行计算:
- 遇到数字,压入栈中。
- 遇到运算符,弹出两个数字计算结果,并压回栈中。
⚡ 性能优化:
-
减少
Stack
对象的创建:
- 改用数组模拟栈,减少
push/pop
的方法调用开销:
java复制编辑public static int evaluate(String[] tokens) { int[] stack = new int[tokens.length]; int top = -1; for (String token : tokens) { switch (token) { case "+": stack[top - 1] += stack[top]; top--; break; case "-": stack[top - 1] -= stack[top]; top--; break; case "*": stack[top - 1] *= stack[top]; top--; break; case "/": stack[top - 1] /= stack[top]; top--; break; default: stack[++top] = Integer.parseInt(token); } } return stack[0]; }
- 改用数组模拟栈,减少
-
优化除法运算
:
- 避免除以零(提前检测除数是否为
0
)。
- 避免除以零(提前检测除数是否为
🔹 3. 评分系统 (Grader
)
java复制编辑package com.zhang.grader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.HashMap;
public class Grader {
public void grade(String exerciseFile, String answerFile) throws Exception {
List<String> exercises = Files.readAllLines(Paths.get(exerciseFile));
List<String> answers = Files.readAllLines(Paths.get(answerFile));
int correct = 0;
for (int i = 0; i < exercises.size(); i++) {
if (exercises.get(i).equals(answers.get(i))) {
correct++;
}
}
System.out.println("正确率: " + (correct * 100.0 / exercises.size()) + "%");
}
}
📌 代码解析:
- 逐行读取题目和答案文件。
- 遍历比对,计算正确率。
测试代码
测试类如何验证程序正确性
import com.zhang.model.Expression;
import com.zhang.model.Fraction;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class GeneratorTest {
private ExpressionGenerator generator;
@BeforeEach
public void setUp() {
// 在每个测试前初始化一个 ExpressionGenerator,范围设为 10
generator = new ExpressionGenerator(10);
}
@Test
public void testGenerateExpressionNotNull() {
// 测试生成表达式是否非空
Expression expr = generator.generateExpression(2);
*assertNotNull*(expr, "生成的表达式不应为空");
}
@Test
public void testExpressionEvaluation() {
// 测试表达式是否可以正确求值
Expression expr = generator.generateExpression(2);
Fraction result = expr.evaluate();
*assertNotNull*(result, "表达式求值结果不应为空");
*assertFalse*(result.isNegative(), "表达式结果不应为负数");
}
@Test
public void testMaxOperatorsLimit() {
// 测试最大运算符数量限制
Expression expr = generator.generateExpression(1);
int operatorCount = countOperators(expr);
*assertTrue*(operatorCount <= 1, "运算符数量应不超过指定最大值");
}
@Test
public void testNoNegativeIntermediates() {
// 测试表达式中间结果没有负数
Expression expr = generator.generateExpression(2);
*assertFalse*(hasNegativeIntermediates(expr), "表达式中间结果不应包含负数");
}
@Test
public void testDivisionProperResult() {
// 测试除法结果是真分数或整数
Expression expr = generator.generateExpression(2);
*assertFalse*(hasImproperDivision(expr), "除法结果应为真分数或整数");
}
// 辅助方法:计算表达式中的运算符数量
private int countOperators(Expression expr) {
if (expr.isLeaf()) {
return 0;
}
return 1 + countOperators(expr.getLeft()) + countOperators(expr.getRight());
}
// 辅助方法:检查是否有负数中间结果
private boolean hasNegativeIntermediates(Expression expr) {
return generator.hasNegativeIntermediates(expr); // 复用原类方法
}
// 辅助方法:检查是否有非真分数的除法结果
private boolean hasImproperDivision(Expression expr) {
return generator.hasDivisionWithImproperResult(expr); // 复用原类方法
}
}
import com.zhang.generator.ProblemGenerator;
import com.zhang.model.Expression;
import com.zhang.model.Fraction;
import com.zhang.model.Problem;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class ProblemGeneratorTest {
private ProblemGenerator generator;
@BeforeEach
public void setUp() {
// 在每个测试前初始化 ProblemGenerator,范围设为 10
generator = new ProblemGenerator(10);
}
@Test
public void testGenerateUniqueProblemNotNull() {
// 测试生成单个唯一题目是否成功
Problem problem = generator.generateUniqueProblem();
*assertNotNull*(problem, "生成的题目不应为空");
*assertNotNull*(problem.getExpression(), "题目表达式不应为空");
*assertNotNull*(problem.getAnswer(), "题目答案不应为空");
}
@Test
public void testGenerateProblemsNoDuplicates() throws IOException {
// 生成 5 个题目并检查是否有重复
generator.generateProblems(5);
// 读取生成的 Exercises.txt 文件
List<String> exercises = Files.*readAllLines*(Path.*of*("Exercises.txt"));
*assertEquals*(exercises.size(), exercises.stream().distinct().count(), "不应有重复题目");
}
@Test
public void testGenerateProblemsWithValidExpressions() throws IOException {
// 生成 3 个题目并验证每个表达式都包含运算符
generator.generateProblems(3);
// 读取 Exercises.txt 文件
List<String> exercises = Files.*readAllLines*(Path.*of*("Exercises.txt"));
for (String line : exercises) {
String expression = line.split("=")[0].trim().substring(3); // 提取表达式部分,去掉编号
*assertTrue*(expression.contains("+") || expression.contains("-") ||
expression.contains("×") || expression.contains("÷"),
"表达式应至少包含一个运算符: " + expression);
}
}
@Test
public void testGenerateProblemsCountMatchesRequest() throws IOException {
// 请求生成 5 个题目,验证生成数量是否正确
int requestedCount = 5;
generator.generateProblems(requestedCount);
// 读取文件并检查题目数量
List<String> exercises = Files.*readAllLines*(Path.*of*("Exercises.txt"));
*assertEquals*(requestedCount, exercises.size(), "生成题目数量应与请求一致");
}
}
1. ProblemGeneratorTest 测试类
这个测试类验证了题目生成的核心逻辑:
testGenerateUniqueProblemNotNull
验证单个题目生成不为空,包括表达式和答案。这确保程序能正常生成题目,符合“四则运算题目:e =”的基本要求。
testGenerateProblemsNoDuplicates
检查生成的多道题目无重复。通过读取 Exercises.txt 文件并比较行数与去重后的行数,确保题目唯一性,满足“程序一次运行生成的题目不能重复”的需求。
testGenerateProblemsWithValidExpressions
验证生成的表达式包含运算符(+、−、×、÷),符合“算术表达式”的定义,且题目格式正确。
testGenerateProblemsCountMatchesRequest
确保生成题目数量与请求数量一致,验证 -n 参数的功能,即“使用 -n 参数控制生成题目的个数”。
2. GeneratorTest 测试类
这个测试类深入验证了表达式生成的细节:
testGenerateExpressionNotNull
确保生成的表达式非空,奠定题目生成的基础。
testExpressionEvaluation
验证表达式可以正确求值,且结果非负,满足“计算过程不能产生负数”的要求。
testMaxOperatorsLimit
检查运算符数量不超过指定上限(测试中为1),支持“每道题目中出现的运算符个数不超过3个”的需求。
testNoNegativeIntermediates
确保表达式中间结果无负数,严格符合“算术表达式中如果存在形如 e1 − e2 的子表达式,那么 e1 ≥ e2”。
testDivisionProperResult
验证除法结果是真分数或整数,满足“生成的题目中如果存在形如 e1 ÷ e2 的子表达式,那么其结果应是真分数”。
题目生成与数量控制
testGenerateProblemsCountMatchesRequest
证明程序能按 -n
参数生成指定数量的题目,支持“一万道题目的生成”。
数值范围控制
测试中的 ProblemGenerator(10)
和 ExpressionGenerator(10)
初始化表明程序接受范围参数(如 -r 10
),生成小于10的自然数和真分数,符合“使用 -r 参数控制题目中数值的范围”。
表达式合法性
testGenerateProblemsWithValidExpressions
和 testGenerateExpressionNotNull
确保生成的表达式符合定义(包含运算符、自然数或真分数),满足“算术表达式:e = n | e1 + e2 | ...”。
无负数约束
testExpressionEvaluation
和 testNoNegativeIntermediates
验证了计算过程和结果无负数,满足“计算过程不能产生负数”及“e1 ≥ e2”的要求。
除法结果为真分数
testDivisionProperResult
确保除法结果是真分数或整数,符合需求。
运算符数量限制
testMaxOperatorsLimit
验证运算符数量可控,确保不超过3个。
题目唯一性
testGenerateProblemsNoDuplicates
证明题目无重复,满足“任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目”。
文件输出
测试中读取 Exercises.txt
文件,间接验证了题目存储功能,符合“生成的题目存入 Exercises.txt 文件”的要求。虽然测试未直接验证 Answers.txt
,但逻辑上与题目生成一致
总结
运行截图
在本次项目中,我们采用结对编程的方式进行开发,充分发挥了各自的优势,提升了开发效率。
张伟聪的分工:负责表达式生成、运算符解析等逻辑实现,优化了题目生成算法,提高了随机性和可读性。
吴钊鑫的分工:负责命令行解析、文件读写、评分模块,确保了输入输出的正确性,并优化了评分性能。
结对感受:
我们在合作中学到了如何更高效地沟通和协作,遇到问题时能够相互讨论、优化思路。张伟聪的代码结构清晰,逻辑严谨,帮助改进了代码质量;吴钊鑫善于优化性能,使程序运行更加高效。通过这次合作,我们不仅提升了编程能力,也积累了团队协作经验。