软件工程第三次作业——结对作业

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Class12Grade23ComputerScience
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class12Grade23ComputerScience/homework/13470
这个作业的目标 完成结队项目:学习设计一个自动生成小学四则运算题目的命令行程序

一、个人信息

姓名 学号
何珊 3223004211
吴泓霏 3223004647

GitHub地址:https://github.com/Streflay/3223004211_-FourOperation

二、PSP表格

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

三、效能分析

微信图片_20251010124433_20_348

微信图片_20251010124433_21_348

四、设计实现过程

开发环境:IntelliJ IDEA Community Edition 2022.2.2
开发语言:JAVA

系统分为三层

入口层(Main):负责参数解析、调度各模块、结果输出。

功能层(Main、CheckAnswer、FourOperations、Fraction、IOUtils):实现文件 I/O、批改用户答案、自动生成题目、进行四则运算。

运维/测试层:单元测试(JUnit4)、性能分析(JProfiler/async-profiler 脚本)与代码质量工具(Checkstyle/PMD/SpotBugs)。

wechat_2025-10-10_173834_919

类与接口

Main:
职责:作为程序入口,解析命令行参数,执行生成题目或批改答案操作,处理程序运行流程及异常。
主要方法:

  • public static void main(String[] args)
    • 初始化扫描器、四则运算生成器 FourOperations、答案批改器 CheckAnswer,输出程序介绍与帮助信息;进入循环,获取用户输入命令,若为 exit 则退出程序,否则调用 executeCommand 执行命令,过程中捕获并处理各类异常,最后关闭扫描器。
  • private static void executeCommand(String commandLine, FourOperations generator, CheckAnswer checker) throws IOException
    • 拆分命令行参数,检查参数数量;根据命令首参数(-n、-r、-e),分别执行生成指定数量和范围的四则运算题目,或批改指定题目与答案文件的操作,若命令格式错误则抛出异常。
  • private static int parsePositiveInt(String s, String name)
    • 将字符串参数解析为正整数,若无法解析或数值非正,抛出包含具体错误信息的异常。
  • private static void printHelp()
    • 输出程序支持的命令及说明。

CheckAnswer:
职责:实现自动批改功能,读取题目文件、标准答案文件和用户答案文件,比对答案并生成批改结果文件。
接口:

  • public void check(String exerciseFile, String userAnswerFile) throws IOException
    • 读取题目文件、标准答案文件(Answers.txt)、用户答案文件内容;检查三者数量是否一致,不一致则抛出异常;遍历比对每道题答案,记录正确与错误题号;生成包含每题详情和总评的内容,写入 Grade.txt。

FourOperations:
职责:作为题目生成模块,负责生成指定数量和数值范围的四则运算题目,并生成对应的答案文件。
接口:

  • public void generateExercises(int count, int range) throws IOException
    • 校验题目数量和数值范围为正整数;在尝试次数上限内,循环生成候选表达式,检查是否重复,评估并验证表达式中间约束(减法中间结果非负、除法子表达式为真分数等),符合要求则记录;最后将题目和答案分别写入Exercises.txt和Answers.txt,若尝试超限则抛运行时异常。
  • private String generateCandidateExpression(int range)
    • 随机确定 1 - 3 个运算符数量,生成操作数和运算符组成的 tokens 列表;若运算符数≥2 且随机条件满足,随机插入一对包围至少一个运算符的括号;将 tokens 以空格分隔成候选表达式字符串返回。
  • private String randomOperand(int range)
    • 随机生成一个操作数:整数或分数(可能为带分数形式)
  • private void writeListToFile(String filename, List lines) throws IOException
    • 将 list 写入文件(每个元素一行)
  • private Fraction evaluateExpressionAndValidate(String expr)
    • 调用infixToPostfix转中缀为后缀表达式,计算时验证减法、除法等约束,符合则返回最终结果(Fraction类型),否则返回null。
  • private List infixToPostfix(String expr)
    • 用调度场算法,将中缀表达式(token 以空格分隔)转为后缀表达式列表。
  • private boolean isOperator(String t)
    • 判断字符串t是否为+、-、×、÷运算符。
  • private int precedence(String op)
    • 返回运算符优先级,+、-为 1,×、÷为 2,其他为 0。

Fraction:
职责:精确分数类,用于表示和处理分数运算,支持分数的解析、四则运算及相关属性判断,能按要求格式输出分数。
接口:

  • public static Fraction parse(String s)
    • 解析字符串为分数,支持整数、a/b 形式的普通分数、a’b/c 形式的带分数。
  • public Fraction add(Fraction o)
    • 加法
  • public Fraction sub(Fraction o)
    • 减法
  • public Fraction mul(Fraction o)
    • 乘法
  • public Fraction div(Fraction o)
    • 除法
  • public boolean isZero()
    • 判断是否为0
  • public boolean isNegative()
    • 判断是否为负数
  • public boolean isPositive()
    • 判断是否为正数
  • public boolean isProperFraction()
    • 判断是否为真分数
  • private void simplify()
    • 约分
  • private long gcd(long a, long b)
    • 计算最大公约数
  • public String toString()
    • 按要求格式输出分数,整数、普通分数或带分数形式。

IOUtils:
职责:文件操作工具类,提供文件的行读取和行写入功能。
接口:

  • public static List readLines(String filename) throws IOException
    • 读取文件每一行到 List
  • public static void writeLines(String filename, List lines) throws IOException
    • 将 List 写入文件,每行一条

五、代码说明

1、Fraction.java - 精确分数核心类(保障分数运算准确性)

分数是小学四则运算的核心,此类解决 “浮点数精度丢失” 问题,支持分数解析、运算与格式化输出。

点击查看代码
/**
 * Fraction - 精确分数类,使用 long 存储分子与分母
 * 支持:
 *  - 解析字符串(整数、a/b、带分数 a’b/c)
 *  - 加减乘除(返回化简后的 Fraction)
 *  - 判断是否为负、是否为零、是否为真分数(|num| < den)
 *  - toString 按题目要求输出:整数 / a/b / 带分数 a’b/c
 */
public class Fraction {
    private long num; // 分子
    private long den; // 分母,始终 >0

    public Fraction(long num, long den) {
        if (den == 0) throw new ArithmeticException("分母不能为 0");
        // 规范化:把符号放到分子上,分母始终 >0
        if (den < 0) {
            den = -den;
            num = -num;
        }
        this.num = num;
        this.den = den;
        simplify();
    }

    // 解析字符串:支持 "3", "3/5", "2’3/8" (注意这里的分隔符为 U+2019 或 ASCII apostrophe)
    public static Fraction parse(String s) {
        if (s == null) return null;
        s = s.trim();
        if (s.isEmpty()) return null;

        try {
            // 带分数检查(可能使用 ’ 或 ')
            if (s.contains("’") || s.contains("'")) {
                String[] parts = s.contains("’") ? s.split("’") : s.split("'");
                if (parts.length != 2) return null;
                long whole = Long.parseLong(parts[0]);
                String fracPart = parts[1];
                String[] fr = fracPart.split("/");
                if (fr.length != 2) return null;
                long a = Long.parseLong(fr[0]);
                long b = Long.parseLong(fr[1]);
                long numerator = Math.abs(whole) * b + a;
                numerator = whole >= 0 ? numerator : -numerator;
                return new Fraction(numerator, b);
            } else if (s.contains("/")) {
                String[] p = s.split("/");
                if (p.length != 2) return null;
                long a = Long.parseLong(p[0]);
                long b = Long.parseLong(p[1]);
                return new Fraction(a, b);
            } else {
                long v = Long.parseLong(s);
                return new Fraction(v, 1);
            }
        } catch (NumberFormatException ex) {
            return null;
        }
    }

    public Fraction add(Fraction o) {
        long n = this.num * o.den + o.num * this.den;
        long d = this.den * o.den;
        return new Fraction(n, d);
    }

    public Fraction sub(Fraction o) {
        long n = this.num * o.den - o.num * this.den;
        long d = this.den * o.den;
        return new Fraction(n, d);
    }

    public Fraction mul(Fraction o) {
        long n = this.num * o.num;
        long d = this.den * o.den;
        return new Fraction(n, d);
    }

    public Fraction div(Fraction o) {
        if (o.num == 0) throw new ArithmeticException("除以零");
        long n = this.num * o.den;
        long d = this.den * o.num;
        if (d < 0) { n = -n; d = -d; }
        return new Fraction(n, d);
    }

    public boolean isZero() {
        return this.num == 0;
    }

    public boolean isNegative() {
        return this.num < 0;
    }

    public boolean isPositive() {
        return this.num > 0;
    }

    public boolean isProperFraction() {
        return Math.abs(this.num) < this.den && this.num != 0;
    }

    // 约分
    private void simplify() {
        long g = gcd(Math.abs(num), den);
        if (g != 0) {
            num /= g;
            den /= g;
        }
    }

    private long gcd(long a, long b) {
        if (b == 0) return a;
        return gcd(b, a % b);
    }

    @Override
    public String toString() {
        if (den == 1) {
            return String.valueOf(num); // 整数
        } else {
            long absNum = Math.abs(num);
            if (absNum > den) {
                long whole = absNum / den;
                long rem = absNum % den;
                String sign = num < 0 ? "-" : "";
                return rem == 0 ? sign + whole : sign + whole + "’" + rem + "/" + den;
            } else {
                String sign = num < 0 ? "-" : "";
                return sign + absNum + "/" + den;
            }
        }
    }
}

2、 FourOperations.java - 题目生成核心类(按约束生成合规题目)

此类负责生成符合小学教学要求的题目,核心约束:减法中间结果非负、除法结果为真分数、题目不重复。

点击查看代码
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.*;

/**
 * FourOperations - 题目生成模块
 * 提供 public void generateExercises(int count, int range)
 * 会在当前目录写出 Exercises.txt 和 Answers.txt
 */
public class FourOperations {

    private static final String[] OPERATORS = {"+", "-", "×", "÷"};
    private final Random random = new Random();
    private final Set<String> generatedExpressions = new HashSet<>();

    public FourOperations() {
        // 无参构造,Main 中以 new FourOperations() 使用
    }

    /**
     * 由 Main 调用:生成 count 道题,数值范围为 [0, range)
     * 会生成 Exercises.txt 和 Answers.txt
     */
    public void generateExercises(int count, int range) throws IOException {
        if (count <= 0) throw new IllegalArgumentException("count 必须为正整数");
        if (range <= 0) throw new IllegalArgumentException("range 必须为正整数");

        List<String> exercises = new ArrayList<>(count);
        List<String> answers = new ArrayList<>(count);

        // 尝试次数上限:根据题目数量自动扩大
        int attemptsLimit = Math.max(10000, count * 20);
        int attempts = 0;

        while (exercises.size() < count) {
            if (++attempts > attemptsLimit) {
                throw new RuntimeException(
                        "生成题目失败:尝试次数过多,可能范围太小或约束过严。\n" +
                                "建议:增加数值范围 (-r 参数) 或减少题目数量 (-n 参数) 以生成足够题目。\n" +
                                "当前已生成题目数:" + exercises.size() + " / " + count +
                                "\n尝试次数上限:" + attemptsLimit
                );
            }

            String expr = generateCandidateExpression(range);
            if (expr == null) continue;

            // 检查重复(按字符串)
            if (generatedExpressions.contains(expr)) continue;

            // 评估表达式并检查约束
            Fraction result = evaluateExpressionAndValidate(expr);
            if (result == null) continue;

            generatedExpressions.add(expr);
            exercises.add(expr);
            answers.add(result.toString());
        }

        writeListToFile("Exercises.txt", exercises);
        writeListToFile("Answers.txt", answers);

        System.out.println("已生成 " + exercises.size() + " 道题目和答案文件。");
    }


    // 生成一个候选表达式(字符串,token 之间以空格分隔)
    private String generateCandidateExpression(int range) {
        int operatorCount = random.nextInt(3) + 1; // 1 ~ 3 个运算符
        List<String> tokens = new ArrayList<>();

        for (int i = 0; i < operatorCount + 1; i++) {
            tokens.add(randomOperand(range));
            if (i < operatorCount) {
                tokens.add(OPERATORS[random.nextInt(OPERATORS.length)]);
            }
        }

        // 随机插入一对括号(有 50% 概率),括号包围至少一个运算符
        if (operatorCount >= 2 && random.nextBoolean()) {
            int leftOpIdx = random.nextInt(operatorCount); // 0..operatorCount-1 表示左侧操作数索引
            int rightOpIdx = leftOpIdx + 1 + random.nextInt(operatorCount - leftOpIdx); // 至少覆盖一个运算符
            int leftTokenIndex = leftOpIdx * 2; // token 索引
            int rightTokenIndex = rightOpIdx * 2;
            // 插入 "(" 在 leftTokenIndex 处,插入 ")" 在 rightTokenIndex 后(要加偏移)
            tokens.add(leftTokenIndex, "(");
            tokens.add(rightTokenIndex + 2, ")"); // +2:因为上面插入 "(" 后索引右移 1
        }

        // 返回以空格分隔的表达式
        return String.join(" ", tokens);
    }

    // 随机生成一个操作数:整数或分数(可能为带分数形式)
    private String randomOperand(int range) {
        // 40% 生成整数,60% 生成分数/带分数(可调)
        boolean makeFraction = random.nextDouble() < 0.6;

        if (!makeFraction) {
            int val = random.nextInt(range); // 0 .. range-1
            return String.valueOf(val);
        } else {
            // 生成分母和分子、或带分数
            int denom = random.nextInt(Math.max(1, range - 1)) + 1; // 1..range-1 (至少1)
            int numer = random.nextInt(Math.max(1, denom)) + 1;    // 1..denom (暂时允许 >= denom 以产带分数)
            // 50% 可能做带分数
            if (numer >= denom && random.nextBoolean()) {
                int whole = numer / denom;
                int remain = numer % denom;
                if (remain == 0) return String.valueOf(whole);
                return whole + "’" + remain + "/" + denom; // 带分数格式 2’3/8
            } else {
                // 普通真分数或假分数都允许(后面会在校验时剔除不符合 ÷ 约束的项)
                // 但尽量使分子 < 分母 提高成为真分数的概率
                if (numer >= denom) {
                    // 使其更可能成为真分数
                    int n = random.nextInt(Math.max(1, denom - 1)) + 1; // 1..denom-1
                    return n + "/" + denom;
                } else {
                    return numer + "/" + denom;
                }
            }
        }
    }

    // 将 list 写入文件(每个元素一行)
    private void writeListToFile(String filename, List<String> lines) throws IOException {
        try (BufferedWriter bw = new BufferedWriter(new FileWriter(filename))) {
            for (String s : lines) {
                bw.write(s);
                bw.newLine();
            }
        }
    }

    /**
     * 评估表达式并验证中间约束:
     * - 在执行任何减法(a - b)时,不允许中间结果为负(即 a - b >= 0)。
     * - 在执行任何除法(a ÷ b)时,该子表达式的结果必须是“真分数”(0 < value < 1)。
     *
     * 若表达式满足,返回最终结果(Fraction);否则返回 null。
     */
    private Fraction evaluateExpressionAndValidate(String expr) {
        try {
            List<String> postfix = infixToPostfix(expr);
            // evaluate postfix while checking constraints
            Deque<Fraction> stack = new ArrayDeque<>();
            for (String token : postfix) {
                if (isOperator(token)) {
                    // pop b then a
                    if (stack.size() < 2) return null;
                    Fraction b = stack.pop();
                    Fraction a = stack.pop();
                    Fraction res;
                    switch (token) {
                        case "+":
                            res = a.add(b);
                            break;
                        case "-":
                            res = a.sub(b);
                            // 中间结果不能为负
                            if (res.isNegative()) return null;
                            break;
                        case "×":
                            res = a.mul(b);
                            break;
                        case "÷":
                            // 除法:分母不能为 0
                            if (b.isZero()) return null;
                            res = a.div(b);
                            // 要求该子表达式结果为真分数(大于0且小于1)
                            if (!(res.isPositive() && res.isProperFraction())) return null;
                            break;
                        default:
                            return null;
                    }
                    stack.push(res);
                } else {
                    // 操作数
                    Fraction f = Fraction.parse(token);
                    if (f == null) return null;
                    stack.push(f);
                }
            }
            if (stack.size() != 1) return null;
            Fraction finalRes = stack.pop();
            // 最终结果也要求非负(题目中通常不希望最终答案为负)
            if (finalRes.isNegative()) return null;
            return finalRes;
        } catch (Exception e) {
            // 任何解析/计算异常都视为无效表达式
            return null;
        }
    }

    // 中缀转后缀(shunting-yard),输入 token 间以空格分隔
    private List<String> infixToPostfix(String expr) {
        String[] tokens = expr.trim().split("\\s+");
        List<String> output = new ArrayList<>();
        Deque<String> ops = new ArrayDeque<>();
        for (String t : tokens) {
            if (t.equals("(")) {
                ops.push(t);
            } else if (t.equals(")")) {
                while (!ops.isEmpty() && !ops.peek().equals("(")) {
                    output.add(ops.pop());
                }
                if (!ops.isEmpty() && ops.peek().equals("(")) ops.pop();
            } else if (isOperator(t)) {
                while (!ops.isEmpty() && isOperator(ops.peek())
                        && precedence(ops.peek()) >= precedence(t)) {
                    output.add(ops.pop());
                }
                ops.push(t);
            } else {
                // 操作数
                output.add(t);
            }
        }
        while (!ops.isEmpty()) {
            output.add(ops.pop());
        }
        return output;
    }

    private boolean isOperator(String t) {
        return "+".equals(t) || "-".equals(t) || "×".equals(t) || "÷".equals(t);
    }

    private int precedence(String op) {
        if ("+".equals(op) || "-".equals(op)) return 1;
        if ("×".equals(op) || "÷".equals(op)) return 2;
        return 0;
    }
}

六、测试运行

运行结果:

微信图片_20251010124435_24_348

微信图片_20251010124435_25_348

微信图片_20251010124435_26_348

微信图片_20251010124436_27_348

微信图片_20251010124436_28_348

微信图片_20251010124436_29_348

测试覆盖率

微信图片_20251010124434_23_348

测试展示

微信图片_20251010124434_22_348

点击查看代码
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;

import java.io.IOException;
import java.nio.file.*;
import java.util.*;

import static org.junit.jupiter.api.Assertions.*;

/**
 * AllTests - 将对 Fraction, IOUtils, FourOperations, CheckAnswer 的测试放入同一文件。
 *
 * 运行前请确保你的项目能够引用 JUnit 5。
 */

public class AllTest {
    private String originalUserDir;

    @BeforeEach
    public void beforeEach(@TempDir Path tempDir) {
        // 保存原始工作目录以便恢复
        originalUserDir = System.getProperty("user.dir");
        // 每个测试会在自己的临时目录下运行(由 @TempDir 提供)
        System.setProperty("user.dir", tempDir.toAbsolutePath().toString());
    }

    @AfterEach
    public void afterEach() {
        // 恢复原始工作目录
        if (originalUserDir != null) {
            System.setProperty("user.dir", originalUserDir);
        }
    }

    @Test
    public void testFractionParsingAndArithmetic() {
        // 解析整数
        Fraction f1 = Fraction.parse("3");
        assertNotNull(f1);
        assertEquals("3", f1.toString());

        // 解析真分数
        Fraction f2 = Fraction.parse("3/5");
        assertNotNull(f2);
        assertEquals("3/5", f2.toString());

        // 解析带分数(两种可能的单引号)
        Fraction f3 = Fraction.parse("2’3/8");
        assertNotNull(f3);
        assertEquals("2’3/8", f3.toString());

        Fraction f3a = Fraction.parse("2'3/8");
        assertNotNull(f3a);
        assertEquals("2’3/8".replace("’", "’"), f3a.toString().replace("'", "’")); // 格式化差异容忍

        // 四则运算: 1/6 + 1/8 = 7/24
        Fraction a = Fraction.parse("1/6");
        Fraction b = Fraction.parse("1/8");
        Fraction sum = a.add(b);
        assertEquals("7/24", sum.toString());

        // 乘除: 3 * 2 = 6
        Fraction three = new Fraction(3, 1);
        Fraction two = new Fraction(2, 1);
        assertEquals("6", three.mul(two).toString());

        // 减法与负数判断
        Fraction sub = two.sub(three); // 2 - 3 = -1
        assertTrue(sub.isNegative());
        assertEquals("-1", sub.toString());
    }

    @Test
    public void testIOUtilsReadWriteFiles(@TempDir Path tempDir) throws IOException {
        // 在临时目录下写入并读取文件,IOUtils 使用相对路径,因此 user.dir 已设置为 tempDir
        List<String> toWrite = Arrays.asList("line1", "line2", "1/2 + 1/2 =");
        IOUtils.writeLines("io_test.txt", toWrite);

        List<String> readBack = IOUtils.readLines("io_test.txt");
        assertEquals(toWrite.size(), readBack.size());
        assertEquals(toWrite, readBack);

        // 再测试写空文件
        IOUtils.writeLines("empty.txt", Collections.emptyList());
        List<String> e = IOUtils.readLines("empty.txt");
        assertTrue(e.isEmpty());
    }

    @Test
    public void testFourOperationsGenerateExercisesAndAnswersFiles() throws Exception {
        FourOperations generator = new FourOperations();

        // 生成 5 道题,范围 20(足够多)
        generator.generateExercises(5, 20);

        // 检查生成的 Exercises.txt 与 Answers.txt 存在并行数正确
        List<String> exercises = IOUtils.readLines("Exercises.txt");
        List<String> answers = IOUtils.readLines("Answers.txt");

        assertNotNull(exercises);
        assertNotNull(answers);
        assertEquals(5, exercises.size(), "Exercises.txt 应包含 5 行");
        assertEquals(5, answers.size(), "Answers.txt 应包含 5 行");

        // 确认每道题对应的答案非空
        for (String ans : answers) {
            assertNotNull(ans);
            assertFalse(ans.trim().isEmpty());
        }
    }

    @Test
    public void testFourOperationsFailureWhenRangeTooSmall() {
        FourOperations generator = new FourOperations();

        try {
            // 调用可能会抛异常,也可能在随机尝试中恰好生成成功
            generator.generateExercises(1000, 2);

            // 如果没有抛异常,则验证生成的文件存在且看起来合理
            List<String> ex = IOUtils.readLines("Exercises.txt");
            List<String> an = IOUtils.readLines("Answers.txt");

            // 基本断言:至少写入了 1 道题,答案行数与题目行数一致
            assertNotNull(ex);
            assertNotNull(an);
            assertTrue(ex.size() > 0, "当未抛异常时应至少生成 1 道题");
            assertEquals(ex.size(), an.size(), "题目数与答案数应相等");

        } catch (RuntimeException e) {
            // 抛异常也接受,断言消息包含我们期望的提示(可根据实际消息微调)
            String msg = e.getMessage() == null ? "" : e.getMessage();
            assertTrue(msg.contains("尝试次数") || msg.toLowerCase().contains("range") || msg.contains("失败"),
                    "抛出的异常信息应表明生成失败或范围/约束不足: " + msg);
        } catch (IOException io) {
            fail("IO 操作异常: " + io.getMessage());
        }
    }


    @Test
    public void testCheckAnswerAllCorrectAndSomeWrong() throws Exception {
        // 创建简单的题目、标准答案与用户答案文件
        List<String> exercises = Arrays.asList("1 + 1", "1/2 + 1/2", "2 + 3");
        List<String> answers = Arrays.asList("2", "1", "5"); // Answers.txt (标准答案)
        List<String> userAllCorrect = Arrays.asList("2", "1", "5");
        List<String> userSomeWrong = Arrays.asList("2", "2", "5"); // 第二题答错

        // 写入文件(CheckAnswer.check 期望读取 Exercises.txt、Answers.txt,用户答案由参数传入)
        IOUtils.writeLines("Exercises.txt", exercises);
        IOUtils.writeLines("Answers.txt", answers);
        IOUtils.writeLines("UserAllCorrect.txt", userAllCorrect);
        IOUtils.writeLines("UserSomeWrong.txt", userSomeWrong);

        // 1) 全对
        CheckAnswer checker = new CheckAnswer();
        checker.check("Exercises.txt", "UserAllCorrect.txt");

        List<String> grade1 = IOUtils.readLines("Grade.txt");
        // 应包含 Correct: 3
        boolean containsCorrect3 = grade1.stream().anyMatch(l -> l.startsWith("Correct:") && l.contains("3"));
        assertTrue(containsCorrect3, "Grade.txt 应显示 3 道题正确");

        // 2) 有错
        checker.check("Exercises.txt", "UserSomeWrong.txt");
        List<String> grade2 = IOUtils.readLines("Grade.txt");
        boolean containsCorrect2 = grade2.stream().anyMatch(l -> l.startsWith("Correct:") && l.contains("2"));
        boolean containsWrong1 = grade2.stream().anyMatch(l -> l.startsWith("Wrong:") && l.contains("1"));
        assertTrue(containsCorrect2, "Grade.txt 应显示 2 道题正确");
        assertTrue(containsWrong1, "Grade.txt 应显示 1 道题错误");
    }
}

1. 分数处理核心类(Fraction)的正确性

测试方法:testFractionParsingAndArithmetic
解析功能验证:测试整数("3")、真分数("3/5")、带分数("2’3/8" 和 "2'3/8")的解析是否正确,确保不同格式的分数都能被准确转换为内部表示。
运算逻辑验证:通过具体案例(如 1/6 + 1/8 = 7/24、3 × 2 = 6)验证加减乘除的结果是否符合数学逻辑,同时检查负数判断(如 2 - 3 = -1)是否准确。
格式输出验证:确保分数的字符串输出符合小学题目要求(如带分数用 ’ 分隔,真分数直接显示 a/b)。
结论:分数的解析、运算和格式化均通过测试,说明基础数据处理层正确。

2. 文件操作工具(IOUtils)的可靠性

测试方法:testIOUtilsReadWriteFiles
读写一致性验证:向临时文件写入内容后立即读取,检查读写内容是否完全一致,确保文件操作无数据丢失或篡改。
边界情况验证:测试空文件的读写(写入空列表,读取后仍为空),确保工具类对特殊场景的处理正确。
结论:文件读写功能稳定,数据传输准确,为题目生成和批改提供了可靠的 IO 支持。

3. 题目生成模块(FourOperations)的合规性

测试方法:testFourOperationsGenerateExercisesAndAnswersFiles 和 testFourOperationsFailureWhenRangeTooSmall
正常场景验证:生成 5 道范围为 20 的题目,检查输出文件(Exercises.txt 和 Answers.txt)的行数是否匹配,且答案非空,确保题目生成流程完整。
约束与容错验证:测试极端场景(如生成 1000 道范围为 2 的题目),验证程序是否能正确处理 “因范围过小导致生成失败” 的情况(要么生成部分题目,要么抛出包含明确提示的异常)。
结论:题目生成逻辑符合预期,能在合理范围内生成合规题目,并对极端情况做出容错处理。

4. 答案批改模块(CheckAnswer)的准确性

测试方法:testCheckAnswerAllCorrectAndSomeWrong
全对场景验证:用户答案与标准答案完全一致时,检查批改结果(Grade.txt)是否正确统计 “全对”(3 道正确)。
部分错误场景验证:人为设置 1 道错误答案,检查批改结果是否准确识别错误题目,统计 “2 对 1 错”。
结论:答案比对逻辑准确,批改结果的统计和输出符合预期,能正确反映用户答题情况。

七、项目小结

1、系统核心功能分为两大模块

题目生成:支持指定题目数量(-n 参数)和数值范围(-r 参数),自动生成包含整数、分数(含带分数)、括号的四则运算题,输出 Exercises.txt(题目文件)和 Answers.txt(标准答案文件),并确保题目无重复、运算约束合规(减法中间结果非负、除法结果为真分数)。
答案批改:读取题目文件(-e 参数)、用户答案文件(-a 参数)和标准答案文件,逐题比对答案并生成 Grade.txt(批改结果),包含每题详情(题目、用户答案、标准答案、对错)及总评(正确 / 错误题数与题号)。
系统技术架构采用 Java 语言开发,通过 5 个类实现功能解耦:Main(入口与命令解析)、FourOperations(题目生成)、CheckAnswer(答案批改)、Fraction(精确分数处理)、IOUtils(文件操作工具),确保代码可维护性与扩展性。

2、开发分工与协作流程

我们采用 “功能模块分工 + 交叉 review” 的协作模式,具体分工如下:

核心负责模块 辅助工作
分数处理(Fraction)、题目生成(FourOperations) 参与测试用例设计、命令行交互优化
答案批改(CheckAnswer)、文件工具(IOUtils) 负责 Main 类逻辑、测试代码编写、文档整理

协作流程遵循 “小步迭代 + 即时沟通” 原则:

  • 每日同步:开发前明确当天任务目标,开发后同步进度与问题(如在实现分数除法时遇到 “分母为负” 问题,两人讨论后确定 “符号转移到分子” 的解决方案);
  • 交叉 review:完成一个模块后,对方需 review 代码(如 review FourOperations 时,发现 “括号插入索引计算错误”,及时修正避免后续问题);
  • 共同调试:遇到集成问题(如题目生成后批改结果异常),两人共同排查,定位到 “题目末尾未加‘=’导致读取时格式不匹配” 的问题,快速修复。

3、总结结对项目的要点以及改进方向

前期需统一接口规范(如文件格式、方法参数)避免后期返工,开发中应同步编写测试用例实现早发现早修复,未来还可从功能扩展等方面完善项目,让协作更高效、成果更贴合需求。

posted @ 2025-10-12 13:48  吴泓霏  阅读(13)  评论(0)    收藏  举报