软件工程第三次作业-结对项目

队员一:计科3班 冯诗萍 3223004555
队员二:计科3班 吴业鸿 3223004257
github项目地址:https://github.com/fengshiping/second
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience |

| 这个作业要求在哪里| https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/homework/13479 |

| 这个作业的目标 | <实现一个自动生成小学四则运算题目的命令行程序> |

一、PSP图

PSP Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 60 85
· Estimate · 估计这个任务需要多少时间 60 85
Development 开发 780 800
· Analysis · 需求分析(包括学习新技术) 120 140
· Design Spec · 生成设计文档 90 100
· Design Review · 设计复审(和同事审核设计文档) 60 30
· Coding Standard · 代码规范(为目前的开发制定合适的规范) 30 25
· Design · 具体设计 100 135
· Coding · 具体编码 240 200
· Code Review · 代码复审 60 90
· Test · 测试(自我测试,修改代码,提交修改) 60 80
Reporting 报告 180 170
· Test Report · 测试报告 60 60
· Size Measurement · 计算工作量 30 20
· Postmortem & Process Improvement Plan · 事后总结,并提出过程改进计划 90 90
合计 1000 1055

二、性能分析

使用idea的Java Flight Record性能分析工具进行性能分析
以下是性能分析工具展示的信息:
image
image
image
根据以上数据,可以得到一张性能分析表:
性能分析图表 - 基于实际采样数据
┌────────────────────────────────────────────┬──────────┬──────────┐
│ 方法名称 │ 采样数量 │ 占比 │
├────────────────────────────────────────────┼──────────┼──────────┤
│ ProblemGenerator.generateProblems() │ 6 │ 60.0% │
│ ├─ generateExpression() │ (包含) │ (主要) │
│ ├─ ExpressionNode.evaluate() │ (包含) │ (次要) │
│ └─ toInfixString()/getCanonicalKey() │ (包含) │ (次要) │
├────────────────────────────────────────────┼──────────┼──────────┤
│ JFR监控系统开销 │ 5 │ 33.3% │
│ ├─ PlatformRecorder.periodicTask() │ 1 │ 6.7% │
│ └─ 其他JFR内部方法 │ 4 │ 26.6% │
├────────────────────────────────────────────┼──────────┼──────────┤
│ 其他方法 │ 1 │ 6.7% │
└────────────────────────────────────────────┴──────────┴──────────┘
总采样数: 9 | 有效业务代码占比: 66.7%

具体的性能问题分析:
主要瓶颈:generateProblems() - 60.0%
优化后的性能分析图:
image
性能优化效果总结

指标 优化前 优化后 说明
主业务方法可见性 不清晰 清晰 优化后能明确看到 generateProblems 是热点
JFR 内部开销 优化后 JFR 自身不再成为性能瓶颈
业务方法占比 低(被掩盖) 高(76.5%) 优化后业务逻辑成为主要性能点
可优化点 不明确 明确 现在可以针对 generateValidCachedExpression 进行优化

三、设计实现过程

核心类设计:

类名 职责 关键方法
Main 程序入口,流程控制 main(), execute()
ProblemGenerator 题目生成算法 generateProblems(), generateValidCachedExpression()
ExpressionNode 表达式树结构 evaluate(), toInfixString(), getCanonicalKey()
Fraction 分数运算 四则运算方法
AnswerChecker 答案批改 grade()
CommandLineArgs 参数解析 参数验证方法

题目生成流程:

image

设计亮点:

  1. 缓存优化设计
    // 双重缓存机制
    private final Map<String, CachedExpression> expressionCache = new HashMap<>();
    private final Set invalidExpressions = new HashSet<>();

// 规范化键生成 - 确保表达式语义等价去重
public String getCanonicalKey() {
if (operator == Operator.ADD || operator == Operator.MULTIPLY) {
// 可交换运算符排序
if (leftKey.compareTo(rightKey) > 0) {
return operator.name() + "(" + rightKey + "," + leftKey + ")";
}
}
}
2. 表达式树设计
// 多态节点设计
public class ExpressionNode {
public enum NodeType { NUMBER, OPERATOR }
public enum Operator { ADD, SUBTRACT, MULTIPLY, DIVIDE }

// 递归求值
public Fraction evaluate() {
    if (type == NodeType.NUMBER) return value;
    Fraction leftVal = left.evaluate();
    Fraction rightVal = right.evaluate();
    // ... 运算符处理
}

}

四、代码说明

项目结构
image

核心代码分析

  1. 分数计算核心 - Fraction.java
    package com.wyh;
    import java.util.Objects;

public class Fraction implements Comparable {
private final int numerator;
private final int denominator;

public Fraction(int numerator, int denominator) {
    if (denominator == 0) {
        throw new IllegalArgumentException("分母不能为零");
    }

    // 规范化符号
    if (denominator < 0) {
        numerator = -numerator;
        denominator = -denominator;
    }

    int gcd = gcd(Math.abs(numerator), denominator);
    this.numerator = numerator / gcd;
    this.denominator = denominator / gcd;
}

public Fraction(int wholeNumber) {
    this.numerator = wholeNumber;
    this.denominator = 1;
}

private int gcd(int a, int b) {
    while (b != 0) {
        int temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}

// 四则运算
public Fraction add(Fraction other) {
    int newNum = this.numerator * other.denominator + other.numerator * this.denominator;
    int newDen = this.denominator * other.denominator;
    return new Fraction(newNum, newDen);
}

public Fraction subtract(Fraction other) {
    int newNum = this.numerator * other.denominator - other.numerator * this.denominator;
    int newDen = this.denominator * other.denominator;
    return new Fraction(newNum, newDen);
}

public Fraction multiply(Fraction other) {
    int newNum = this.numerator * other.numerator;
    int newDen = this.denominator * other.denominator;
    return new Fraction(newNum, newDen);
}

public Fraction divide(Fraction other) {
    if (other.numerator == 0) {
        throw new ArithmeticException("除零错误");
    }
    int newNum = this.numerator * other.denominator;
    int newDen = this.denominator * other.numerator;
    return new Fraction(newNum, newDen);
}

// 比较方法
public boolean isZero() {
    return numerator == 0;
}

public boolean isProper() {
    return Math.abs(numerator) < denominator;
}

@Override
public int compareTo(Fraction other) {
    long left = (long) this.numerator * other.denominator;
    long right = (long) other.numerator * this.denominator;
    return Long.compare(left, right);
}

@Override
public String toString() {
    if (denominator == 1) {
        return String.valueOf(numerator);
    }

    if (Math.abs(numerator) < denominator) {
        return numerator + "/" + denominator;
    }

    int whole = numerator / denominator;
    int remainder = Math.abs(numerator % denominator);
    if (remainder == 0) {
        return String.valueOf(whole);
    }
    return whole + "'" + remainder + "/" + denominator;
}

@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (obj == null || getClass() != obj.getClass()) {
        return false;
    }
    Fraction other = (Fraction) obj;
    return numerator == other.numerator && denominator == other.denominator;
}

@Override
public int hashCode() {
    return Objects.hash(numerator, denominator);
}

public int getNumerator() { return numerator; }
public int getDenominator() { return denominator; }

}

  1. 表达式树 - ExpressionNode.java

package com.wyh;
import java.util.ArrayList;
import java.util.List;

public class ExpressionNode {
public enum NodeType { NUMBER, OPERATOR }
public enum Operator { ADD, SUBTRACT, MULTIPLY, DIVIDE }

private NodeType type;
private Fraction value;
private Operator operator;
private ExpressionNode left;
private ExpressionNode right;

// 叶节点构造函数
public ExpressionNode(Fraction value) {
    this.type = NodeType.NUMBER;
    this.value = value;
}

// 运算符节点构造函数
public ExpressionNode(Operator operator, ExpressionNode left, ExpressionNode right) {
    this.type = NodeType.OPERATOR;
    this.operator = operator;
    this.left = left;
    this.right = right;
}

public Fraction evaluate() {
    if (type == NodeType.NUMBER) {
        return value;
    }

    Fraction leftVal = left.evaluate();
    Fraction rightVal = right.evaluate();

    switch (operator) {
        case ADD: return leftVal.add(rightVal);
        case SUBTRACT: return leftVal.subtract(rightVal);
        case MULTIPLY: return leftVal.multiply(rightVal);
        case DIVIDE: return leftVal.divide(rightVal);
        default: throw new IllegalStateException("未知运算符");
    }
}

public String toInfixString() {
    if (type == NodeType.NUMBER) {
        return value.toString();
    }

    String leftStr = left.toInfixString();
    String rightStr = right.toInfixString();

    // 根据优先级决定是否加括号
    if (needsParentheses(left, false)) {
        leftStr = "(" + leftStr + ")";
    }
    if (needsParentheses(right, true)) {
        rightStr = "(" + rightStr + ")";
    }

    String opStr = getOperatorSymbol();
    return leftStr + " " + opStr + " " + rightStr;
}

private boolean needsParentheses(ExpressionNode child, boolean isRight) {
    if (child.type != NodeType.OPERATOR) {
        return false;
    }

    int parentPrecedence = getPrecedence(this.operator);
    int childPrecedence = getPrecedence(child.operator);

    if (childPrecedence < parentPrecedence) {
        return true;
    }

    // 处理结合性
    if (childPrecedence == parentPrecedence) {
        if (isRight && (operator == Operator.SUBTRACT || operator == Operator.DIVIDE)) {
            return true;
        }
    }

    return false;
}

private int getPrecedence(Operator op) {
    switch (op) {
        case ADD: case SUBTRACT: return 1;
        case MULTIPLY: case DIVIDE: return 2;
        default: return 0;
    }
}

private String getOperatorSymbol() {
    switch (operator) {
        case ADD: return "+";
        case SUBTRACT: return "-";
        case MULTIPLY: return "×";
        case DIVIDE: return "÷";
        default: return "?";
    }
}

// 生成规范化键用于去重
public String getCanonicalKey() {
    if (type == NodeType.NUMBER) {
        return value.toString();
    }

    String leftKey = left.getCanonicalKey();
    String rightKey = right.getCanonicalKey();

    // 对可交换运算符进行排序
    if (operator == Operator.ADD || operator == Operator.MULTIPLY) {
        if (leftKey.compareTo(rightKey) > 0) {
            return operator.name() + "(" + rightKey + "," + leftKey + ")";
        }
    }

    return operator.name() + "(" + leftKey + "," + rightKey + ")";
}

public int getOperatorCount() {
    if (type == NodeType.NUMBER) {
        return 0;
    }
    return 1 + left.getOperatorCount() + right.getOperatorCount();
}

// Getters
public NodeType getType() { return type; }
public Fraction getValue() { return value; }
public Operator getOperator() { return operator; }
public ExpressionNode getLeft() { return left; }
public ExpressionNode getRight() { return right; }

}

3.题目生成器核心 - ProblemGenerator.java

package com.wyh;

import java.util.*;

public class ProblemGenerator {
private final int range;
private final Random random;

// 添加缓存机制
private final Map<String, CachedExpression> expressionCache = new HashMap<>();
private final Set<String> invalidExpressions = new HashSet<>();

// 缓存内部类
private static class CachedExpression {
    final String infixString;
    final String answer;
    final String canonicalKey;
    final int operatorCount;

    CachedExpression(ExpressionNode expr) {
        this.infixString = expr.toInfixString();
        this.answer = expr.evaluate().toString();
        this.canonicalKey = expr.getCanonicalKey();
        this.operatorCount = expr.getOperatorCount();
    }
}

public ProblemGenerator(int range) {
    if (range <= 0) {
        throw new BusinessException(ErrorCode.INVALID_RANGE_PARAMETER.getCode(),
                "数值范围必须为正整数");
    }
    this.range = range;
    this.random = new Random();
}

public List<Problem> generateProblems(int count) {
    if (count <= 0) {
        throw new BusinessException(ErrorCode.INVALID_COUNT_PARAMETER.getCode(),
                "题目数量必须为正整数");
    }

    Set<String> seenKeys = new HashSet<>();
    List<Problem> problems = new ArrayList<>();
    int attempts = 0;
    int maxAttempts = count * 100; // 减少尝试次数

    while (problems.size() < count && attempts < maxAttempts) {
        attempts++;

        CachedExpression cached = generateValidCachedExpression();
        if (cached == null || !seenKeys.add(cached.canonicalKey)) {
            continue;
        }

        problems.add(new Problem(cached.infixString, cached.answer));
    }

    if (problems.size() < count) {
        throw new BusinessException(ErrorCode.INSUFFICIENT_UNIQUE_PROBLEMS.getCode(),
                String.format("无法在合理尝试次数内生成足够的不重复题目(已生成 %d/%d)。请增大范围参数 -r 或减少题目数量 -n",
                        problems.size(), count));
    }

    return problems;
}

private CachedExpression generateValidCachedExpression() {
    // 快速尝试3次
    for (int quickAttempt = 0; quickAttempt < 3; quickAttempt++) {
        ExpressionNode expression = generateOptimizedExpression(1 + random.nextInt(3));
        String key = expression.getCanonicalKey();

        // 检查已知无效表达式
        if (invalidExpressions.contains(key)) {
            continue;
        }

        // 检查缓存
        CachedExpression cached = expressionCache.get(key);
        if (cached != null) {
            if (isValidCachedExpression(cached)) {
                return cached;
            } else {
                invalidExpressions.add(key);
                continue;
            }
        }

        // 新表达式,验证并缓存
        try {
            // 快速验证运算符数量
            if (expression.getOperatorCount() > 3) {
                invalidExpressions.add(key);
                continue;
            }

            // 验证表达式有效性
            Fraction result = expression.evaluate();
            if (isValidFraction(result)) {
                CachedExpression newCached = new CachedExpression(expression);
                expressionCache.put(key, newCached);
                return newCached;
            } else {
                invalidExpressions.add(key);
            }
        } catch (Exception e) {
            invalidExpressions.add(key);
        }
    }
    return null;
}

private ExpressionNode generateOptimizedExpression(int operatorCount) {
    return generateExpression(operatorCount);
}

// 优化原有的 generateExpression 方法
private ExpressionNode generateExpression(int operatorCount) {
    if (operatorCount == 0) {
        return new ExpressionNode(generateRandomFraction());
    }

    ExpressionNode.Operator op = randomOperator();
    int leftOps = random.nextInt(operatorCount);
    int rightOps = operatorCount - 1 - leftOps;

    ExpressionNode left = generateExpression(leftOps);
    ExpressionNode right = generateExpression(rightOps);

    return applyOptimizedConstraints(op, left, right);
}

// 优化约束应用逻辑
private ExpressionNode applyOptimizedConstraints(ExpressionNode.Operator op,
                                                 ExpressionNode left, ExpressionNode right) {
    // 对于减法和除法,进行快速检查
    if (op == ExpressionNode.Operator.SUBTRACT || op == ExpressionNode.Operator.DIVIDE) {
        try {
            Fraction leftVal = left.evaluate();
            Fraction rightVal = right.evaluate();

            switch (op) {
                case SUBTRACT:
                    if (leftVal.compareTo(rightVal) < 0) {
                        return new ExpressionNode(op, right, left);
                    }
                    break;

                case DIVIDE:
                    if (rightVal.isZero()) {
                        // 重新生成右节点,但限制次数
                        right = generateSimpleExpression();
                    }
                    // 检查除法结果是否为真分数
                    Fraction divisionResult = leftVal.divide(rightVal);
                    if (!divisionResult.isProper() && leftVal.compareTo(rightVal) >= 0) {
                        return new ExpressionNode(op, right, left);
                    }
                    break;
            }
        } catch (Exception e) {
            // 如果计算出错,交换节点重试
            return new ExpressionNode(op, right, left);
        }
    }

    return new ExpressionNode(op, left, right);
}

// 生成简单表达式(避免深度递归)
private ExpressionNode generateSimpleExpression() {
    if (random.nextDouble() < 0.5) {
        return new ExpressionNode(generateRandomFraction());
    } else {
        ExpressionNode.Operator op = randomOperator();
        // 避免除法和减法以减少复杂度
        while (op == ExpressionNode.Operator.DIVIDE || op == ExpressionNode.Operator.SUBTRACT) {
            op = randomOperator();
        }
        return new ExpressionNode(op,
                new ExpressionNode(generateRandomFraction()),
                new ExpressionNode(generateRandomFraction()));
    }
}

private boolean isValidCachedExpression(CachedExpression cached) {
    return cached.operatorCount <= 3;
}

private boolean isValidFraction(Fraction fraction) {
    try {
        return fraction.compareTo(new Fraction(0)) >= 0; // 非负数
    } catch (Exception e) {
        return false;
    }
}

// 保留原有验证方法(用于兼容性)
private boolean validateExpression(ExpressionNode expr) {
    try {
        Fraction result = expr.evaluate();
        return expr.getOperatorCount() <= 3;
    } catch (Exception e) {
        return false;
    }
}

// 优化分数生成
private Fraction generateRandomFraction() {
    // 增加整数比例,减少分数运算
    if (random.nextDouble() < 0.8) { // 从0.7提高到0.8
        return new Fraction(random.nextInt(range - 1) + 1);
    } else {
        int denominator = random.nextInt(range - 2) + 2;
        int numerator = random.nextInt(denominator - 1) + 1;
        return new Fraction(numerator, denominator);
    }
}

private ExpressionNode.Operator randomOperator() {
    ExpressionNode.Operator[] operators = ExpressionNode.Operator.values();
    // 调整运算符概率,减少除法和减法
    if (random.nextDouble() < 0.3) {
        // 30% 概率选择加法或乘法
        return random.nextBoolean() ? ExpressionNode.Operator.ADD : ExpressionNode.Operator.MULTIPLY;
    }
    return operators[random.nextInt(operators.length)];
}

// 添加清理方法(可选)
public void clearCache() {
    expressionCache.clear();
    invalidExpressions.clear();
}

}

五、测试运行

  1. 基础功能测试用例
测试用例 输入命令 预期结果 测试目的
TC01 - 基础生成 -r 10 -n 5 生成5道范围10以内的题目 验证基本生成功能
TC02 - 默认数量 -r 10 生成10道题目(默认值) 验证默认参数处理
TC03 - 大量题目 -r 10 -n 1000 生成1000道不重复题目 验证去重算法性能
TC04 - 小范围测试 -r 3 -n 10 生成10道题目,可能提示范围小 验证边界情况处理
TC05 - 判分模式 -e Exercises.txt -a Answers.txt 输出判分结果 验证判分功能
  1. 异常处理测试用例
测试用例 输入命令 预期结果 测试目的
TC06 - 无效参数 -x 10 显示"未知参数"错误 验证参数验证
TC07 - 缺少范围 -n 5 显示"缺少-r参数"错误 验证必要参数检查
TC08 - 范围为零 -r 0 -n 5 显示"范围必须为正整数" 验证参数有效性
TC09 - 文件不存在 -e nofile.txt -a nofile.txt 显示"文件不存在"错误 验证文件处理
TC10 - 数量过大 -r 5 -n 10000 可能提示无法生成足够题目 验证合理性检查

具体测试情况:
1.
image

image

image
image

image
image

image
image

image
image

image
image

image
image

image
image

image
image

image
image

六、总结和反思

  1. 需求理解偏差
    问题:初期对"真分数"约束理解不一致
    解决:通过具体例子澄清,建立术语词典避免歧义。

  2. 版本控制冲突
    教训:某次同时修改ExpressionNode导致合并冲突
    改进:建立更细粒度的代码所有权和提交规范。

感想:
吴:
"结对编程让我明白,最好的代码不是完美无瑕的代码,而是两个人都能理解、都愿意维护的代码。技术可以学习,但找到一个能激发你最佳状态的搭档需要缘分。"

冯:
"这次经历让我从'写代码的人'成长为'解决问题的人'。感谢我的搭档,不仅教会我技术,更让我看到了工程师应有的专业和热情。"

posted @ 2025-10-22 21:36  冯诗萍  阅读(2)  评论(0)    收藏  举报