结对项目
| 这个作业属于哪个课程 | 软件工程 |
|---|---|
| 这个作业要求在哪里 | 结对项目 |
| 这个作业的目标 | 团队协作与沟通、代码规范与项目管理 |
| 项目成员 | 郑邦洲3123004767 、林佳俊3123004748 |
Github仓库
PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | ||
| · Estimate | 估计这个任务需要多少时间 | 600 | 645 |
| 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 | 10 |
| · Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 30 | 35 |
| Total | 合计 | 600 | 645 |
模块设计
| 类名 | 职责描述 |
|---|---|
Main |
解析命令行参数,选择生成模式或校验模式 |
ExpressionGenerator |
递归生成表达式树,确保数值范围和运算合法性,避免重复题目 |
Fraction |
封装分数运算(加减乘除),处理带分数、约分和负数逻辑 |
ExerciseChecker |
解析题目文件,计算正确答案并与用户答案对比 |
FileUtils |
统一处理文件读写操作 |

效能分析


关键代码展示
分数类 Fraction.java
public class Fraction {
private int numerator; // 分子
private int denominator; // 分母
// 构造方法:自动约分并处理负数
public Fraction(int numerator, int denominator) {
int gcd = MathUtils.gcd(numerator, denominator);
this.numerator = numerator / gcd;
this.denominator = denominator / gcd;
if (this.denominator < 0) { // 确保分母为正
this.numerator *= -1;
this.denominator *= -1;
}
}
// 加法运算
public Fraction add(Fraction other) {
int num = this.numerator * other.denominator + other.numerator * this.denominator;
int den = this.denominator * other.denominator;
return new Fraction(num, den);
}
// 输出格式化:带分数或真分数
@Override
public String toString() {
if (denominator == 1) return String.valueOf(numerator);
int whole = numerator / denominator;
int num = Math.abs(numerator % denominator);
return whole != 0 ? whole + "'" + num + "/" + denominator : numerator + "/" + denominator;
}
}
2. 表达式生成器 ExpressionGenerator.java
public class ExpressionGenerator {
private Node buildNode(int operators) {
if (operators == 0) {
return new NumberNode(generateNumber()); // 生成叶子节点(数值)
}
char op = randomOperator(); // 随机选择运算符
Node left = buildNode(leftOps);
Node right = buildNode(rightOps);
// 交换左右子树保证 + 和 × 不重复
if (op == '+' || op == '×') {
if (left.value().compareTo(right.value()) > 0) {
swap(left, right);
}
}
// 计算并验证结果合法性
Fraction value = calculate(op, left.value(), right.value());
return new OperatorNode(op, left, right, value);
}
private interface Node {
Fraction value();
String toString();
}
}
3. 答案校验器 ExerciseChecker.java
public class ExerciseChecker {
public static void check(String exerciseFile, String answerFile) {
List<String> exercises = FileUtils.readLines(exerciseFile);
List<String> userAnswers = FileUtils.readLines(answerFile);
for (int i = 0; i < exercises.size(); i++) {
String expr = exercises.get(i).replace(" =", "");
Fraction correct = evaluate(expr); // 计算正确答案
Fraction userAns = new Fraction(userAnswers.get(i));
if (correct.equals(userAns)) {
correctIndices.add(i + 1);
} else {
wrongIndices.add(i + 1);
}
}
}
// 使用逆波兰表达式求值
private static Fraction evaluate(String expr) {
List<String> tokens = Tokenizer.tokenize(expr);
List<String> postfix = InfixToPostfix.convert(tokens);
return PostfixEvaluator.evaluate(postfix);
}
}
关键设计思路
- 分数统一化
- 所有数值(自然数、真分数)均用
Fraction类处理,避免混合运算错误。 - 自动约分保证结果唯一性,例如
4/8存储为1/2。
- 所有数值(自然数、真分数)均用
- 表达式树生成
- 递归构建二叉树结构,确保运算符数量不超过3个。
- 通过交换左右子树避免
a + b和b + a的重复。
- 答案校验优化
- 使用逆波兰表达式算法保证计算顺序正确。
- 分离
Tokenizer、InfixToPostfix和PostfixEvaluator实现模块化。
- 文件操作封装
FileUtils统一处理编码和异常,避免重复代码
单元测试
import static org.junit.Assert.*;
import java.util.*;
import org.junit.jupiter.api.*;
import org.mockito.*;
import org.mockito.MockitoAnnotations;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
// 测试类定义
public class ExpressionGeneratorTest {
@Mock
private Random random;
@InjectMocks
private ExpressionGenerator expressionGenerator;
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
}
/**
* 测试正常场景:生成多个表达式
* 预期结果:返回指定数量的表达式,每个表达式都是唯一的
*/
@Test
@DisplayName("测试正常场景:生成多个表达式")
public void testGenerate_NormalScenario() {
// 模拟输入参数
int count = 5;
// 模拟生成表达式的方法
when(random.nextInt(3)).thenReturn(1, 2, 1, 2, 1);
when(expressionGenerator.generateExpression(1)).thenReturn(new Node("1 + 1", 2), new Node("2 + 2", 4));
when(expressionGenerator.generateExpression(2)).thenReturn(new Node("1 + 2 + 3", 6), new Node("2 + 3 + 4", 9));
// 调用被测方法
List<String> result = expressionGenerator.generate(count);
// 验证结果
assertEquals(5, result.size());
assertTrue(result.contains("1 + 1 = "));
assertTrue(result.contains("2 + 2 = "));
assertTrue(result.contains("1 + 2 + 3 = "));
assertTrue(result.contains("2 + 3 + 4 = "));
// 假设生成的表达式是唯一的
assertEquals(5, new HashSet<>(result).size());
}
/**
* 测试边界条件:生成0个表达式
* 预期结果:返回空的List
*/
@Test
@DisplayName("测试边界条件:生成0个表达式")
public void testGenerate_ZeroCount() {
// 模拟输入参数
int count = 0;
// 调用被测方法
List<String> result = expressionGenerator.generate(count);
// 验证结果
assertTrue(result.isEmpty());
}
/**
* 测试边界条件:生成1个表达式
* 预期结果:返回包含1个表达式的List
*/
@Test
@DisplayName("测试边界条件:生成1个表达式")
public void testGenerate_SingleExpression() {
// 模拟输入参数
int count = 1;
// 模拟生成表达式的方法
when(random.nextInt(3)).thenReturn(1);
when(expressionGenerator.generateExpression(1)).thenReturn(new Node("1 + 1", 2));
// 调用被测方法
List<String> result = expressionGenerator.generate(count);
// 验证结果
assertEquals(1, result.size());
assertTrue(result.contains("1 + 1 = "));
}
/**
* 测试异常处理:生成表达式时返回null
* 预期结果:跳过null表达式,继续生成其他表达式
*/
@Test
@DisplayName("测试异常处理:生成表达式时返回null")
public void testGenerate_NullExpression() {
// 模拟输入参数
int count = 3;
// 模拟生成表达式的方法
when(random.nextInt(3)).thenReturn(1, 2, 1);
when(expressionGenerator.generateExpression(1)).thenReturn(new Node("1 + 1", 2), null);
when(expressionGenerator.generateExpression(2)).thenReturn(new Node("1 + 2 + 3", 6));
// 调用被测方法
List<String> result = expressionGenerator.generate(count);
// 验证结果
assertEquals(2, result.size());
assertTrue(result.contains("1 + 1 = "));
assertTrue(result.contains("1 + 2 + 3 = "));
}
/**
* 测试异常处理:生成表达式时抛出异常
* 预期结果:捕获异常,继续生成其他表达式
*/
@Test
@DisplayName("测试异常处理:生成表达式时抛出异常")
public void testGenerate_Exception() {
// 模拟输入参数
int count = 3;
// 模拟生成表达式的方法
when(random.nextInt(3)).thenReturn(1, 2, 1);
when(expressionGenerator.generateExpression(1)).thenReturn(new Node("1 + 1", 2), mock(Node.class));
when(expressionGenerator.generateExpression(2)).thenThrow(new RuntimeException("Generate expression error"));
// 调用被测方法
List<String> result = expressionGenerator.generate(count);
// 验证结果
assertEquals(1, result.size());
assertTrue(result.contains("1 + 1 = "));
}
/**
* 测试异常处理:生成表达式时重复表达式
* 预期结果:跳过重复的表达式,继续生成其他表达式
*/
@Test
@DisplayName("测试异常处理:生成表达式时重复表达式")
public void testGenerate_DuplicateExpression() {
// 模拟输入参数
int count = 3;
// 模拟生成表达式的方法
when(random.nextInt(3)).thenReturn(1, 1, 2);
when(expressionGenerator.generateExpression(1)).thenReturn(new Node("1 + 1", 2), new Node("1 + 1", 2));
when(expressionGenerator.generateExpression(2)).thenReturn(new Node("1 + 2 + 3", 6));
// 调用被测方法
List<String> result = expressionGenerator.generate(count);
// 验证结果
assertEquals(2, result.size());
assertTrue(result.contains("1 + 1 = "));
assertTrue(result.contains("1 + 2 + 3 = "));
}
}
测试数据
-n 15 -r 10

结对小结
在本次结对编程项目中,我们通过紧密合作,成功完成了项目的各个阶段。郑邦洲在代码实现和性能优化方面表现出色,而林佳俊则在测试和文档编写方面提供了极大的帮助。我们发现结对编程能够有效提高代码质量,并且在遇到问题时能够更快地找到解决方案。
经验总结
- 优点: 结对编程能够有效减少代码中的错误,并且在设计阶段能够提供更多的思路。
- 教训: 在项目初期,我们低估了性能优化的难度,导致在后期花费了较多时间进行性能改进。

浙公网安备 33010602011771号