四则运算器

软件工程作业

项目 内容
这个作业属于哪个课程 软件工程
这个作业要求在哪里 作业要求
开发人员 张伟聪 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(生成器包):包括 ProblemGeneratorExpressionGenerator,负责生成运算表达式和题目。
  • 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:测试 ExpressionGeneratorProblemGenerator 的功能,确保生成的表达式和问题符合预期。
  • 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;
    }
}

📌 代码解析

  • 生成两个随机数num1num2)。
  • 通过 Operator.getRandomOperator() 随机选取运算符(+、-、×、÷)。
  • 拼接成数学表达式(如 3 + 5)。

⚡ 性能优化

  1. 避免重复调用 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)];
    }
    
  2. 支持更复杂的表达式

    • 通过递归生成嵌套运算,如 (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)进行计算:
    • 遇到数字,压入栈中。
    • 遇到运算符,弹出两个数字计算结果,并压回栈中。

⚡ 性能优化

  1. 减少 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];
    }
    
  2. 优化除法运算

    • 避免除以零(提前检测除数是否为 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 参数控制题目中数值的范围”。

表达式合法性

testGenerateProblemsWithValidExpressionstestGenerateExpressionNotNull 确保生成的表达式符合定义(包含运算符、自然数或真分数),满足“算术表达式:e = n | e1 + e2 | ...”。

无负数约束

testExpressionEvaluationtestNoNegativeIntermediates 验证了计算过程和结果无负数,满足“计算过程不能产生负数”及“e1 ≥ e2”的要求。

除法结果为真分数

testDivisionProperResult 确保除法结果是真分数或整数,符合需求。

运算符数量限制

testMaxOperatorsLimit 验证运算符数量可控,确保不超过3个。

题目唯一性

testGenerateProblemsNoDuplicates 证明题目无重复,满足“任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目”。

文件输出

测试中读取 Exercises.txt 文件,间接验证了题目存储功能,符合“生成的题目存入 Exercises.txt 文件”的要求。虽然测试未直接验证 Answers.txt,但逻辑上与题目生成一致


总结

运行截图

在本次项目中,我们采用结对编程的方式进行开发,充分发挥了各自的优势,提升了开发效率。

张伟聪的分工:负责表达式生成、运算符解析等逻辑实现,优化了题目生成算法,提高了随机性和可读性。
吴钊鑫的分工:负责命令行解析、文件读写、评分模块,确保了输入输出的正确性,并优化了评分性能。

结对感受:
我们在合作中学到了如何更高效地沟通和协作,遇到问题时能够相互讨论、优化思路。张伟聪的代码结构清晰,逻辑严谨,帮助改进了代码质量;吴钊鑫善于优化性能,使程序运行更加高效。通过这次合作,我们不仅提升了编程能力,也积累了团队协作经验。

posted @ 2025-03-18 11:57  ALESK  阅读(37)  评论(0)    收藏  举报