软件工程-作业3

结对项目:自动生成四则运算题

这个作业属于哪个课程 班级链接
这个作业要求在哪里 作业链接
这个作业的目标 团队协作与沟通、代码规范与项目管理

合作人员:

  • 张逸壕 3123004163
  • 韩佳鑫 3123004142

GitHub 项目地址automatically-generated-arithmetic


一、PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 15 10
Estimate 估计这个任务需要多少时间 20 20
Development 开发 180 200
Analysis 需求分析 (包括学习新技术) 20 15
Design Spec 生成设计文档 20 10
Design Review 设计复审 10 5
Coding Standard 代码规范 (为目前的开发制定合适的规范) 10 5
Design 具体设计 10 5
Coding 具体编码 60 80
Code Review 代码复审 30 30
Test 测试(自我测试,修改代码,提交修改) 30 20
Reporting 报告 30 45
Test Repor 测试报告 30 45
Size Measurement 计算工作量 20 10
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 10 20
·合计 495 520

二、计算模块设计

1. 接口设计与实现

类结构

  • ArithmeticGeneratorApplication: 程序入口,处理命令行参数
  • GlobalExceptionHandler:异常处理器
  • FileUtils: 文件读写工具类
  • AnswerEvaluator: 答案生成器
  • QuestionGenerator: 题目生成器
  • Fraction:分数处理类
ArithmeticGeneratorApplication`->`FileUtils`->`QuestionGenerator`->`AnswerEvaluator&Fraction`->`FileUtils(写)`->`end`

算法关键:解析&生成&计算四则运算表达式

三、性能改进

通过合理的数据结构和算法,提高生成题目的效率

1.使用HashSet快速检查重复:

/**
     * 生成指定数量和范围的数学题目
     * @param numQuestions 题目数量
     * @param range        运算范围
     * @return {@code List<String> } 题目列表
     * @author zyh
     * @date 2025/03/16
     */
    public static List<String> generateQuestions(int numQuestions, int range) {
        // 用于存储不重复的题目 这里用hashset检查快
        Set<String> uniqueQuestions = new HashSet<>();
        // 存储生成的题目
        List<String> questions = new ArrayList<>();
        // 随机数生成器
        Random rand = new Random();
        // 生成题目
        while (uniqueQuestions.size() < numQuestions) {
            // 生成随机表达式
            String question = generateRandomExpression(range, rand);
            // 归一化表达式,避免重复
            String normalized = normalizeExpression(question);
            // 确保唯一性 检查uniqueQuestions是否存在该表达式
            if (!uniqueQuestions.contains(normalized)) {
                // 不存在则添加到哈希set便于后续快速筛查
                uniqueQuestions.add(normalized);
                // 添加到列表
                questions.add(question + " =");
            }
        }
        // 返回
        return questions;
    }

2.递归下降解析器和反射调用实现高效计算

 // 解析表达式 (递归下降解析器的入口)
    private static Result parseExpression(List<Token> tokens, int index) {
        return parseAdditionSubtraction(tokens, index);
    }

    // 解析加减法 (处理优先级)
    private static Result parseAdditionSubtraction(List<Token> tokens, int index) {
        Result result = parseMultiplicationDivision(tokens, index);
        Object currentValue = result.value;
        index = result.nextIndex;

        while (index < tokens.size()) {
            Token token = tokens.get(index);
            // 如果是加号或减号
            if (token.type == Token.Type.OPERATOR && (token.value.equals("+") || token.value.equals("-"))) {
                // 解析乘除法
                Result nextResult = parseMultiplicationDivision(tokens, index + 1);
                Object nextValue = nextResult.value;
                index = nextResult.nextIndex;

                // 调用对应的方法进行计算
                currentValue = invokeMethod(currentValue, nextValue, token.value);
            } else {
                // 不是加减运算符,退出循环
                break;
            }
        }
        return new Result(currentValue, index);
    }


    // 解析乘除法 (处理优先级)
    private static Result parseMultiplicationDivision(List<Token> tokens, int index) {
        // 解析基本单元 (数字、括号内的表达式)
        Result result = parsePrimary(tokens, index);
        Object currentValue = result.value;
        index = result.nextIndex;

        while (index < tokens.size()) {
            Token token = tokens.get(index);
            // 如果是乘号或除号
            if (token.type == Token.Type.OPERATOR && (token.value.equals("*") || token.value.equals("/"))) {
                // 解析基本单元
                Result nextResult = parsePrimary(tokens, index + 1);
                Object nextValue = nextResult.value;
                index = nextResult.nextIndex;

                // 调用对应的方法进行计算
                currentValue = invokeMethod(currentValue, nextValue, token.value);
            } else {
                // 不是乘除运算符,退出循环
                break;
            }
        }
        return new Result(currentValue, index);
    }

    // 解析基本单元 (数字、括号内的表达式)
    private static Result parsePrimary(List<Token> tokens, int index) {
        if (index >= tokens.size()) {
            throw new IllegalArgumentException("Unexpected end of expression");
        }

        Token token = tokens.get(index);
        // 如果是数字
        if (token.type == Token.Type.NUMBER) {
            // 统一转换为 Fraction 对象
            Object value = new Fraction(token.value);
            return new Result(value, index + 1);

            // 如果是左括号
        } else if (token.type == Token.Type.LPAREN) {
            // 遇到左括号,递归解析括号内的表达式

            int closingParenIndex = findClosingParen(tokens, index);
            //如果没找到
            if(closingParenIndex == -1){
                throw  new IllegalArgumentException("Mismatched parentheses");
            }
            // 递归调用, 注意这里要传入index + 1
            Result result = parseExpression(tokens, index + 1);

            // 跳过右括号
            return new Result(result.value, closingParenIndex + 1);

        } else {
            throw new IllegalArgumentException("Unexpected token: " + token.value);
        }
    }

    // 查找与给定左括号匹配的右括号的索引
    private static int findClosingParen(List<Token> tokens, int openParenIndex) {
        int parenCount = 1;
        for (int i = openParenIndex + 1; i < tokens.size(); i++) {
            Token token = tokens.get(i);
            // 如果是左括号
            if (token.type == Token.Type.LPAREN) {
                parenCount++;
                // 如果是右括号
            } else if (token.type == Token.Type.RPAREN) {
                parenCount--;
                // 找到匹配的右括号
                if (parenCount == 0) {
                    return i;
                }
            }
        }
        // 没有找到匹配的右括号
        return -1;
    }

3.Intellij Profiler分析:

img

四、单元测试

1.单元测试代码

@SpringBootTest
class ArithmeticGeneratorApplicationTests {

    /**
     * 确保除法生成合理分数
     * @author zyh
     * @date 2025/03/18
     */
    @Test
    public void testDivisionReasonableFraction() {
        Random rand = new Random();
        String expression = QuestionGenerator.generateRandomExpression(10, rand);
        assertFalse(expression.contains("÷") && isSmaller(expression.split(" ")[0], expression.split(" ")[2]));
    }

    /**
     * 生成多个不重复的题目
     * @author zyh
     * @date 2025/03/18
     */
    @Test
    public void testGenerateMultipleUniqueQuestions() {
        List<String> questions = QuestionGenerator.generateQuestions(5, 10);
        assertEquals(5, questions.size());
        Set<String> uniqueQuestions = new HashSet<>(questions);
        assertEquals(5, uniqueQuestions.size()); // 确保题目不重复
    }

    /**
     * 操作数生成
     * @author zyh
     * @date 2025/03/18
     */
    @Test
    public void testGenerateOperand() {
        Random rand = new Random();
        String operand = QuestionGenerator.generateOperand(10, rand);
        assertTrue(operand.matches("\\d+") || operand.matches("\\d+/\\d+"));
    }

    /**
     * 表达式归一化
     * @author zyh
     * @date 2025/03/18
     */
    @Test
    public void testNormalizeExpression() {
        String expression = "3 + 5";
        String normalized = QuestionGenerator.normalizeExpression(expression);
        assertEquals("3 + 5", normalized);
    }

    /**
     * 确保减法不产生负数
     * @author zyh
     * @date 2025/03/18
     */
    @Test
    public void testSubtractionNoNegativeResult() {
        Random rand = new Random();
        String expression = QuestionGenerator.generateRandomExpression(10, rand);
        assertFalse(expression.contains("-") && isSmaller(expression.split(" ")[0], expression.split(" ")[2]));
    }

    /**
     * AnswerEvaluator测试计算答案
     */
    @Test
    public void testAnswerEvaluator() {
        List<String> questions = QuestionGenerator.generateQuestions(5, 10);
        List<String> answers = AnswerEvaluator.evaluateQuestions(questions);
        assertEquals(5, answers.size());
    }

    /**
     * 测试AnswerEvaluator评分
     */
    @Test
    public void testAnswerEvaluatorGrade() {
        String result = AnswerEvaluator.gradeAnswers("Exercises.txt", "Answers.txt");
        assertNotNull(result);
    }

    /**
     * 测试Fraction类
     */
    @Test
    public void testFraction() {
        assertEquals(new Fraction(1, 2), new Fraction("1/2"));
        assertNotEquals(new Fraction(1, 2), new Fraction("1"));
    }

    /**
     * 测试Fraction类的计算
     */
    @Test
    public void testFractionCalculation() {
        assertEquals(new Fraction(1, 2), new Fraction("1/4").add(new Fraction("1/4")));
        assertEquals(new Fraction(1, 2), new Fraction("1/2").subtract(new Fraction("0/2")));
    }

    /**
     * 测试Fraction类的转化真分数
     */
    @Test
    public void testFractionToProper() {
        assertEquals("1'1/2", new Fraction("3/2").toMixedNumberString());
        assertNotEquals("1'1/2", new Fraction("4/2").toMixedNumberString());
    }
}

2.覆盖率

img

五、异常处理

1.异常处理器

@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * @param exception 异常
     * @author zyh
     * @date 2025/03/18
     */
    @ExceptionHandler(value = Exception.class)
    public void allException(Exception exception) {
        // 返回错误结果
         System.out.println(exception.getMessage());
    }
}

2.异常设计代码

@SpringBootTest
public class ExceptionTest {

    /**
     * 范围参数不合法
     * @author zyh
     * @date 2025/03/18
     */
    @Test
    public void testInvalidRangeParameter() {
        String[] args = {"-n", "10", "-r", "0"};
        ArithmeticGeneratorApplication.main(args);

        // 检查控制台输出是否包含错误信息
        ByteArrayOutputStream errContent = new ByteArrayOutputStream();
        System.setErr(new PrintStream(errContent));

        ArithmeticGeneratorApplication.main(args);
        // 验证错误信息
        String expectedErrorMessage = "处理命令行参数时出错: bound must be positive";
        assertTrue(errContent.toString().contains(expectedErrorMessage));
    }

    /**
     * 文件不存在
     * @author zyh
     * @date 2025/03/18
     */
    @Test
    public void testFileNotExist() {
        String[] args = {"-e", "NotExist.txt", "-a", "NotExist.txt"};
        ByteArrayOutputStream errContent = new ByteArrayOutputStream();
        System.setErr(new PrintStream(errContent)); // 重定向标准错误流

        // 执行程序
        ArithmeticGeneratorApplication.main(args);

        // 验证错误信息
        String expectedErrorMessage = "Error reading files: NotExist.txt (系统找不到指定的文件。)";
        assertTrue(errContent.toString().contains(expectedErrorMessage));
    }

    /**
     * 参数缺失
     * @author zyh
     * @date 2025/03/18
     */
    @Test
    public void testMissingRequiredParameter() {
        String[] args = {"-n", "10"};
        ArithmeticGeneratorApplication.main(args);

        // 检查控制台输出是否包含帮助信息
        // 可以通过重定向 System.out 来捕获输出
        ByteArrayOutputStream outContent = new ByteArrayOutputStream();
        System.setOut(new PrintStream(outContent));

        ArithmeticGeneratorApplication.main(args);

        assertTrue(outContent.toString().contains("数学生成器"));
        assertTrue(outContent.toString().contains("生成的题目数量"));
        assertTrue(outContent.toString().contains("生成题目的数字范围"));
    }
}

3.设计目标

1.testInvalidRangeParameter

  • 目标

    1. 范围参数不合法时抛出异常
    2. 打印错误信息,便于定位问题
  • 场景:范围参数不合法


2. testFileNotExist

  • 目标

    1. 文件不存在时抛出异常,避免程序终止
    2. 打印错误信息,便于定位问题
  • 场景:文件不存在


3. testMissingRequiredParameter

  • 目标

    1. 参数不足时立即抛出异常,阻止后续无效操作
    2. 提示正确命令格式,提升用户体验。
  • 场景:参数缺失

posted @ 2025-03-18 13:28  十曜  阅读(27)  评论(0)    收藏  举报