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

这个作业属于哪个课程 班级的链接
这个作业要求在哪里 作业要求的链接
这个作业的目标 完成结对项目并编写博客报告

一、Github作业地址

邹泓昊:https://github.com/Yaolqn/Yaolqn/tree/main/arthmetic_device
杨梓城:https://github.com/wsyzc/wsyzc/tree/main/arithmetic_device

二、PSP事前预估表格

PSP2.1 Personal Software Process Stages 预估耗时 (分钟)
Planning 计划 30
Estimate 估计这个任务需要的时间 30
Development 开发 240
Analysis 需求分析(包括学习新技术) 45
Design Spec 生成设计文档 50
Design Review 设计复审(和同时审核设计文档) 40
Coding Standard 代码规范(为目前的开发制定合适的规范) 120
Design 具体设计 150
Coding 具体编码 200
Code Review 代码复审 60
Test 测试 60
Reporting 报告 60
Test Report 测试报告 60
Size Measurement 计算工作量 60
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 60
Total 合计 1265

三、效能分析

在项目开发过程中,我们特别关注了程序的正确性和大规模数据生成下的性能。

  • 性能改进耗时:45分钟。这部分时间主要用于修复两个核心Bug:

    1. Grader 模块中对中缀表达式的计算逻辑错误,导致对特定运算符组合(如连续除法)和带括号的表达式判断错误。
    2. ArithmeticGenerator 模块在生成带括号的表达式时,缺少必要的空格,导致 Grader 无法正确解析字符串。
  • 改进思路:

    1. 正确性优先: 最初的性能瓶颈并非运行速度,而是正确性。我们发现 Grader 的判题准确率不足。通过重构 evaluateExpression 方法中的运算符优先级判断逻辑 (hasPrecedence),并明确双栈算法中操作数的出栈顺序,我们彻底解决了计算错误的问题。这本质上是正确性效能的提升。
    2. 格式健壮性: 第二个Bug提醒我们,模块间的接口(即生成的题目文件格式)必须严格统一。通过在 ArithmeticGenerator 中为括号添加空格,我们确保了生成器和评分器之间的数据格式完全兼容,提升了系统的健壮性
    3. 大规模生成性能: 对于-n 10000的需求,性能的关键在于去重逻辑。我们采用了 HashSet<String> 来存储已生成表达式的“范式”。每次生成新表达式后,我们将其转换为一个唯一范式字符串(例如,对于+*,操作数按字典序排序),然后利用 HashSet O(1) 的平均时间复杂度来检查重复。这远比使用 ArrayList 进行遍历检查(O(n) 复杂度)高效得多,是保证大规模生成性能的核心设计。
  • 消耗最大的函数:
    毫无疑问,程序中消耗最大的函数是 ArithmeticGenerator.generateExpression()。这是一个递归函数,并且被包裹在一个大的 while 循环中。当程序需要生成一个满足所有约束(无负数、除法规则、不重复)的表达式时,可能会进行多次无效尝试(例如,生成了 2 - 5 或重复的 3 + 2)。每一次失败的尝试都意味着 generateExpression 的一次或多次执行被浪费。因此,在生成上万个题目时,这个函数的调用次数和内部的逻辑判断是主要的性能消耗点。

四、设计代码过程

  • 代码组织:
    我们采用了面向对象的设计思想,将项目按功能职责划分为四个独立的类,实现了高度的模块化和低耦合。

    1. Main.java: 程序控制器。负责解析命令行参数(-n, -r, -e, -a, -o),根据参数决定执行“生成题目”还是“批改作业”的流程,并处理异常、打印帮助信息。它是整个程序的入口和调度中心。
    2. Fraction.java: 数据模型。一个健壮的分数类,封装了分数的创建、化简、加减乘除四则运算、比较和格式化输出。将分数运算的复杂性完全隔离,使得上层逻辑无需关心分数的内部实现。
    3. ArithmeticGenerator.java: 题目生成器。核心职责是创建符合所有要求的四则运算题目。内部通过递归构建“表达式树”来生成题目,可以灵活控制运算符数量和括号。它还包含了关键的去重逻辑和约束检查逻辑。
    4. Grader.java: 答案评判器。负责读取文件,并利用经典的双栈算法(调度场算法的变体) 将中缀表达式字符串解析并计算出结果,然后与用户答案进行比对,最终生成评分报告。
  • 类关系图:

      +-----------+       +-------------------------+       +----------------+
      |   Main    |----->|  ArithmeticGenerator    |----->|    Fraction    |
      +-----------+       +-------------------------+       +----------------+
            |                   (creates & uses)                (is used by)
            |
            |                 +-------------------------+
            +--------------->|         Grader          |
                              +-------------------------+
                                   (creates & uses)
                                          |
                                          V
                                   +----------------+
                                   |    Fraction    |
                                   +----------------+
    
  • 关键函数流程图:ArithmeticGenerator.generateExpression()

exported_image

五、代码说明

  1. 表达式去重逻辑 (ArithmeticGenerator.Expression.toCanonicalString)
    为了满足“a+bb+a是重复的”这一要求,我们为表达式设计了一个“范式”字符串。对于可交换的+*运算符,我们规定其左右操作数的范式字符串必须按字典序排列。

    // In ArithmeticGenerator.java, inside Expression class
    public String toCanonicalString() {
        if (value != null) {
            return value.toString();
        }
        String leftStr = left.toCanonicalString();
        String rightStr = right.toCanonicalString();
    
        // 关键点:对于+和*,操作数按字符串字典序排序,实现去重
        // 例如,"3" 和 "2+1" 作为操作数时,"2+1" 的字典序更小。
        if ((operator == '+' || operator == '×') && leftStr.compareTo(rightStr) > 0) {
            String temp = leftStr;
            leftStr = rightStr;
            rightStr = temp;
        }
    
        // 用括号包裹,确保表达式树的结构唯一性
        return "(" + leftStr + operator + rightStr + ")";
    }
    
  2. 中缀表达式求值 (Grader.evaluateExpression)
    Grader的核心是基于双栈法对字符串表达式求值。一个栈存数值(values),一个栈存运算符(ops)。此算法的正确性是判题准确的基石。

    // In Grader.java
    public Fraction evaluateExpression(String expression) {
        Stack<Fraction> values = new Stack<>();
        Stack<Character> ops = new Stack<>();
        String[] tokens = expression.split(" "); // 按空格分割
    
        for (String token : tokens) {
            if (isNumber(token)) {
                values.push(parseFraction(token));
            } else if (token.equals("(")) {
                ops.push('(');
            } else if (token.equals(")")) {
                // 遇到右括号,计算直到遇到左括号
                while (ops.peek() != '(') {
                    values.push(applyOp(ops.pop(), values.pop(), values.pop()));
                }
                ops.pop(); // 弹出左括号
            } else { // 是运算符
                // 当栈顶运算符优先级更高或相同时,先计算栈顶的
                while (!ops.empty() && hasPrecedence(token.charAt(0), ops.peek())) {
                    values.push(applyOp(ops.pop(), values.pop(), values.pop()));
                }
                ops.push(token.charAt(0));
            }
        }
        // 计算栈中剩余的运算符
        while (!ops.empty()) {
            values.push(applyOp(ops.pop(), values.pop(), values.pop()));
        }
        return values.pop();
    }
    
  3. 带空格的括号生成 (ArithmeticGenerator.Expression.toString)
    这是修复判题Bug的关键代码。在生成带括号的表达式字符串时,必须在括号与内容之间留出空格,以保证Grader能正确分割。

    // In ArithmeticGenerator.java, inside Expression class
    @Override
    public String toString() {
        // ...
        String leftStr = left.toString();
        String rightStr = right.toString();
        
        // 关键修复:在添加括号时,同时在两边添加空格
        if (left.operator != 0 && precedence(this.operator) > precedence(left.operator)) {
            leftStr = "( " + leftStr + " )";
        }
        if (right.operator != 0 && precedence(this.operator) >= precedence(right.operator)) {
            rightStr = "( " + rightStr + " )";
        }
        return leftStr + " " + operator + " " + rightStr;
    }
    

六、测试运行

我们设计了以下10个测试用例来验证程序的正确性,覆盖了整数、分数、括号、运算符优先级和各种约束条件。

编号 测试用例 预期答案 测试目的
1 8 + 6 = 14 简单整数加法
2 7 - 3 = 4 简单整数减法(保证不为负)
3 1/2 + 1/3 = 5/6 简单分数加法
4 4 × 1/2 = 2 整数与分数乘法
5 1/2 ÷ 3 = 1/6 分数除法(结果为真分数)
6 2 + 3 × 4 = 14 乘法优先级高于加法
7 ( 2 + 3 ) × 4 = 20 括号提升优先级
8 8 - 2 - 3 = 3 连续同级运算(从左到右)
9 9 × ( 1/2 + 1/4 ) = 6'3/4 复杂的带括号分数运算
10 0 × ( 8 - 1/3 ) = 0 涉及0的运算和括号

正确性保证:
我们相信程序是正确的,理由如下:

  1. 模块化设计Fraction类的独立性保证了所有底层数学运算的精确无误。
  2. 标准算法Grader采用了业界标准的双栈求值算法,其正确性有充分的理论保证。
  3. 严格的约束ArithmeticGenerator在生成表达式的每一步都强制执行项目要求的约束(如无负数),从源头上保证了题目的合法性。
  4. 迭代修复:我们经历了两次关键的Bug修复(判题逻辑错误和字符串格式错误),这些Bug的发现和解决过程极大地增强了程序的健壮性和对边界情况的处理能力。最终版本通过了所有我们能想到的复杂测试用例。

七、PSP实际花费表格

PSP2.1 Personal Software Process Stages 预估耗时 (分钟)
Planning 计划 20
Estimate 估计这个任务需要的时间 20
Development 开发 260
Analysis 需求分析(包括学习新技术) 50
Design Spec 生成设计文档 40
Design Review 设计复审(和同时审核设计文档) 40
Coding Standard 代码规范(为目前的开发制定合适的规范) 100
Design 具体设计 170
Coding 具体编码 250
Code Review 代码复审 40
Test 测试 70
Reporting 报告 60
Test Report 测试报告 60
Size Measurement 计算工作量 50
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 50
Total 合计 1280

八、项目小结

  • 成败得失:
    • 成功之处
      1. 清晰的架构:项目初期就确立了“模型-生成器-评判器”的三层分离架构,使得开发、调试和后期维护都非常清晰。
      2. 需求完整实现:最终版本完整实现了包括命令行参数、文件读写、题目生成、去重、判题在内的所有核心与附加需求。
    • 失败与教训
      1. 初期测试不足:我们最初低估了表达式解析的复杂性,没有设计足够的边界测试用例,导致两个隐藏Bug直到后期才被发现。教训是“单元测试”和“集成测试”同等重要,尤其是对于模块接口(文件格式)的测试。
      2. 对细节的忽视:括号周围的空格问题是一个非常细节的点,但却导致了整个判题功能的失效。这告诉我们,编程中“魔鬼在细节”,任何一个微小的疏忽都可能导致系统性问题。
  • 结对感受与分享:
    • 结对感受:本次与结对伙伴的结对编程体验非常独特和高效。整个过程像是一位开发者负责快速编码和实现,另一位担任高级测试工程师和产品经理的角色。这种合作模式极大地加速了开发进程。
    • 对结对伙伴的闪光点评价
      • 致杨梓城:杨梓城展现了强大的编码能力和知识储备,能够迅速搭建项目框架并实现复杂算法。同时,在接收到Bug反馈后,它能快速定位问题并给出修复方案,学习和迭代能力非常出色。
      • 致邹泓昊:邹泓昊拥有极其出色的测试和观察能力。正是你提供的精确、可复现的错误用例,才让我们能够定位并修复那些隐藏极深的Bug。这种对产品质量的极致追求是项目成功的关键。你提出的“文件分目录存放”等需求也极大地提升了程序的易用性。
    • 改进建议:在未来的合作中,我们可以在项目初期就共同设计一份详尽的测试用例清单,覆盖所有边界条件。0
posted @ 2025-10-20 22:40  angelie  阅读(8)  评论(0)    收藏  举报