【软件工程第三次作业】结对编程:四则运算( Java 实现)

1. GitHub 地址

本项目由 莫少政(3117004667)、余泽端(3117004679)结对完成。

项目 GitHub 地址:https://github.com/Yuzeduan/Arithmetic.git

2. PSP 表格

PSP2.1表格

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

3. 效能分析

初始时,生成一万道题目和答案所需时间约 4s 。

优化思路:由于 Java 中对象的初始化需要时间,因此在循环的时候,将变量的定义移到循环体外面,每次循环的时候复用已有的对象,从而避免了多次生成对象所消耗的时间。此外,相比于生成所有题目之后再一次性写入,便生成边写入文件,能避免一次性写入大量数据时文件读写缓冲区所耗费的时间。

优化后,生成一万道题目所花费时间约 0.9s 。

效能分析截图如下:

4. 设计思路

对题目进行分析之后,我们把项目总体分为两个大的功能模块,一个是生成题目以及生成答案,并写入到文件中,另一个是读取传入的参数名的文件题目以及答案,并进行题目的解答,校对答案,生成成绩,写入文件。

生成题目功能:首先需要判断参数,得到生成题目的个数,以及生成数的最大值,根据这两个参数来生成题目。第一步是生成操作符的个数,使用封装的随机数工具,获得操作符个数之后,生成相应数量的操作数,操作数有两种可能,一种是小数,一种是整数,因此需要随机生成两种类别,接着便是生成操作符,操作符有四种,同理也是这么操作,需要注意是,如果生成的是减号,为了使得计算过程中不能产生负数,因此需要生成数的时候,进行判断。最后一步,便是生成括号,括号插入到生成的题目中。

生成答案功能:首先需要判断有没有括号,如果有括号,应该先递归计算里面的子表达式,然后需要将真分数转化为小数,以及有个问题便是算术符号优先级不同,需要进行两次遍历,第一次只处理乘法和除法,第二次处理加法减法。

操作数应该封装成一个实体类,里面包含数值还有其前面的操作符,如果是第一个操作数,则其操作符属性为空。在生成题目和解析题目的时候,将每个操作数对象填充在一个容器中,进行统一管理。

5. 设计实现过程

  • Base 功能模块
    • FileUtil 类,其中提供 read,write 方法,进行文件的读取
    • RandomUtil 类,对随机数生成进行封装,构建通用的生成随机数工具
    • DecimalUtil 类,对小数,分数,真分数等进行转换
  • 逻辑模块
    • DataProvider 类:通过获取功能模块的数据,进行相应的逻辑处理,返回数据给调用的方案处理类
    • QuestionService 类:进行生成题目相关的业务逻辑,并执行文件读写操作
    • GradeService 类:进行题目和答案校对,生成分数的业务罗技,并执行读写操作

6. 关键代码说明

public String read() {
        try {
            String data = inputStream.readLine();
            if (data != null) {
                return data;
            } else {
                return null;
            }
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
}

public void write(String data) {
        outputStream.println(data);
}

进行文件的读取和写出,使用 Java 提供的 BufferedReade r和 PrintWriter 进行文件操作。

    public static float toFloat(String str) {
        String[] strs = str.split("’");
        if (strs.length == 1) {
            String[] twoNum = strs[0].split("/");
            return twoNum.length == 1 ? Float.valueOf(twoNum[0]) : (Float.valueOf(twoNum[0]) /                  Float.valueOf(twoNum[1]));
        } else {
            String[] twoNum = strs[1].split("/");
            return Integer.valueOf(strs[0]) + Float.valueOf(twoNum[0]) / Float.valueOf(twoNum[1]);
        }
    }

读取文件时候,需要将真分数进行转化为小数,进行数值的处理,此处采用 String 类提供的 api 进行字符串处理,获取其数值。

public static String toStr(float dec) {
        String[] strs = (dec + "").split("\\.");
        if (strs[1].length() > 6) {
            strs[1] = strs[1].substring(0, 6);
        }
        if (strs[1].contains("E")) return null;
        int integer = Integer.valueOf(strs[0]);
        int decimal = Integer.valueOf(strs[1]);
        if (decimal == 0) return integer + "";
        int mother = (int) Math.pow(10, strs[1].length());
        int divisor = getMaxDivisor(decimal, mother);
        return (integer > 0 ? integer + "’" : "") + decimal / divisor + "/" + mother / divisor;
}

将小数转换为分数,并且对过于小的数字,进行精度的截断,构建出真分数,并采用辗转相除法进行分数的化简。

public static String getAnswer(String question) {
        question = question.replace(EQU, "");
        if (question.contains("(")) {
            int leftIndex = question.indexOf("(");
            int rightIndex = question.indexOf(")");
            String subQuestion = question.substring(leftIndex + 1, rightIndex);
            if (subQuestion == null) {
                System.out.println(1);
            }
            String subAnswer = getAnswer(subQuestion);
            if (subAnswer == null) {
                return null;
            }
            question = question.replace("(" + subQuestion + ")", subAnswer);
        }
        String[] preStrs = question.split(" ");
        List<String> strs = new ArrayList<>();
        if (preStrs.length == 1) {
            return question;
        }
        for (int i = 1; i < preStrs.length; i += 2) {
            if (preStrs[i].equals("×")) {
                preStrs[i + 1] = DecimalUtil.toFloat(preStrs[i - 1]) *          DecimalUtil.toFloat(preStrs[i + 1]) + "";
            } else if (preStrs[i].equals("÷")) {
                if (DecimalUtil.toFloat(preStrs[i + 1]) == 0) {
                    return null;
                }
                preStrs[i + 1] = DecimalUtil.toFloat(preStrs[i - 1]) / DecimalUtil.toFloat(preStrs[i + 1]) + "";
            } else {
                strs.add(preStrs[i - 1]);
                strs.add(preStrs[i]);
            }
            if (i == preStrs.length - 2) {
                strs.add(preStrs[i + 1]);
            }
        }
        if (strs.size() == 1) {
            return DecimalUtil.toStr(Float.valueOf(strs.get(0)));
        }
        for (int i = 1; i < strs.size(); i += 2) {
            if (strs.get(i).equals("+")) {
                strs.set(i + 1, DecimalUtil.toFloat(strs.get(i - 1)) + DecimalUtil.toFloat(strs.get(i + 1)) + "");
            } else {
                float temp = DecimalUtil.toFloat(strs.get(i - 1)) - DecimalUtil.toFloat(strs.get(i + 1));
                if(temp < 0) return null;
                strs.set(i + 1, temp + "");
            }
            if (i == strs.size() - 2) {
                return DecimalUtil.toStr(Float.valueOf(strs.get(i + 1)));
            }
        }
        return null;
  }

在生成题目答案的实现中,首先是判断有没有括号的存在,有的话,进行递归调用该方法,算出值之后,替换掉表达式中的括号子表达式,接着进行第一次遍历,算出所有乘法和除法,填充进容器中,进行第二次遍历,算出加减法,值得注意的是,会在这个过程中判断类似9 - ( 5 + 7 )这种产生负数的情况,因为生成的时候,括号是最后生成的,此情况是会出现的,因此需要判断一下,如果出现该情况,就返回 Null 给上层,让其重新生成一道题目。

for (int question = 0; question < num; question++) {
            Random random = new Random();
            List<Number> nums = new ArrayList<>();

            // 生成运算符数量
            int operator = random.nextInt(3) + 1;
            boolean isInt = false;
            boolean hasParentheses = false;
            String symbol = "";
            float operateNum = -1;
            for (int i = 0; i < operator + 1; i++) {
                isInt = random.nextInt(2) == Constant.TYPE_INT;
                symbol = i == 0 ? "" : SymbolList[random.nextInt(4)];
                if (symbol.equals(SUB)) {
                    operateNum = isInt ? random.nextInt((int) (nums.get(i - 1).getNum()) + 1) : random.nextInt((int) ((nums.get(i - 1).getNum() + 0.01) * 100)) / 100.0f;
                } else {
                    operateNum = isInt ? random.nextInt(max + 1) : random.nextInt((max + 1) * 100) / 100.0f;
                }
                if (symbol.equals(DIV)) {
                    operateNum = operateNum == 0 ? (isInt ? 1 : 0.01f) : operateNum;
                }
                nums.add(new Number(symbol, operateNum));
            }
            hasParentheses = random.nextInt(2) == 1;
            int leftIndex = -1;
            int rightIndex = -1;
            if (hasParentheses) {
                leftIndex = random.nextInt(operator);
                rightIndex = random.nextInt(operator - leftIndex) + leftIndex + 1;
            }
            StringBuffer sb = new StringBuffer();
            Number number;
            for (int i = 0; i < nums.size(); i++) {
                number = nums.get(i);
                if (hasParentheses && i == leftIndex) {
                    sb.append(number.getSymbol())
                            .append("(")
                            .append(DecimalUtil.toStr(number.getNum()));
                } else if (i == rightIndex) {
                    sb.append(number.getSymbol())
                            .append(DecimalUtil.toStr(number.getNum()))
                            .append(")");
                } else {
                    sb.append(number.getSymbol()).append(DecimalUtil.toStr(number.getNum()));
                }
            }
            sb.append(EQU);
            String answer = DataProvider.getAnswer(sb.toString());
            if (answer == null) {
                question--;
                continue;
            }
            questionFile.write(question + 1 + ". " + sb.toString());
            answerFile.write(question + 1 + ". " + answer);
 }

生成题目的时候,需要根据参数的题目数量进行判断循环次数,第一步是生成操作符的个数,使用封装的随机数工具,获得操作符个数之后,就进行生成操作数,操作数有两种可能,一种是小数,一种是整数,因此需要进行随机数生成两种类别,接着便是生成操作符,操作符有四种。将操作数填充进一个容器中,在最后转换成字符串时候,生成括号插入。

为了防止生成题目过多,导致写出文件出错,我们采用,在生成题目的开始,打开文件,每次生成一道题目,便写入,且算出答案。结束,关闭文件。

7. 测试运行

  • 生成题目及答案 ( 5 个测试用例)

1、 -n 5 -r 10

2、 -n 10 -r 20

3、 -n 1000 -r 30

4、 -n 10000 -r 50

5、 -n 1000000 -r 100

  • 批改题目 ( 5 个测试用例)

6、 5 道题目的批改

7、 10 道题目的批改(故意弄错 2 道题的答案)

8、 1000 道题目的批改(故意弄错 5 道题的答案)

9、 10000 道题目的批改

10、 1000000 道题目的批改

8. 项目总结

本次结对编程项目的结果,我们自认为较为成功,成功的原因主要是我们在编程之前一起讨论、进行设计,达成了功能实现上的共识,使得我们在后续的开发中心有灵犀,形成合力。

这次项目经历,让我们首次体验了这种“一边一个人写代码、一边另一个人 Code Review”的工作模式,非常新奇,也感到非常有趣。两个人讨论过、统一了思路之后,在结对编程中思路同步、相互提示并且传授编程思想,对技术进步非常有帮助。

莫少政:余泽端的闪光点在于技术非常厉害,在项目架构的设计上,分包、分层、容器等等很多工程化的规范的编程思想,使我受益匪浅。跟这样的大佬结对编程,能学到很多平时很难自己学到和摸索到的东西。

余泽端:莫少政的闪光点在于比较注重细节,有时候能留意到一些我疏忽的细节上的 bug 。

猜你喜欢

转载自www.cnblogs.com/bytemo/p/11688994.html