一、github地址:https://github.com/Jasminejiamei/pairProgramming
二、PSP表格
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 40 |
· Estimate | · 估计这个任务需要多少时间 | 60 | 40 |
Development | 开发 | 1440 | 1505 |
· Analysis | · 需求分析 | 30 | 15 |
· Design Spec | · 生成设计文档 | 20 | 15 |
· Design Review | · 设计复审 | 30 | 15 |
· Coding Standard | · 代码规范 | 20 | 20 |
· Design | · 具体设计 | 80 | 80 |
· Coding | · 具体编码 | 900 | 980 |
· Code Review | · 代码复审 | 30 | 30 |
· Test | · 测试(自我测试,修改代码,提交修改) | 330 | 400 |
Reporting | 报告 | 130 | 100 |
· Test Report | · 测试报告 | 80 | 60 |
· Size Measurement | · 计算工作量 | 30 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 20 |
合计 | 1630 | 1695 |
三、效能分析
由于之前采用单线程执行,在文件IO流的处理上花费了不少的时间,包括代码上的执行存在部分冗余,代码上可以提高利用率。打开了线程池以后,多线程执行,大大提高了执行速度,在代码逻辑改进优化后,对大量生成题目的效果十分显著,由30s时间完成优化到2s:
四、设计过程
(一)流程图
视图设计过程:
生成题目设计过程:
判断对错设计过程:
(二)项目目录:
分包思路:
1)视图层:view 包含主页面与两个功能页面
2)实体类:po 包含题目存放类Deposit、ChildDeposit,分数处理类
3)逻辑处理层:service 处理题目生成等的逻辑、处理题目判错的逻辑
4)工具类:util 包含文件的读写功能
(三)总体实现思路
程序运行,进入主页面,点击选择进入相应功能页面(生成题目or判断对错),如果为生成题目,用户需要输入相关参数(题目数量、数的大小范围),视图层获取输入的数据,传入至逻辑层中进行处理,先判断输入是否有误,有误则终止程序,无误则调用题目生成的方法CreateAth;如果为判断对错,用户需要选择相应的文件(Exersises.txt和Answers.txt),视图层获取输入的数据,传入到逻辑层进行处理,判断输入无误后,调用题目盘错的方法Judge。
题目生成的思路:题目要求生成的算术运算符少于3个,先随机生成一个运算符的个数,传入到CreateAth方法中,先生成一个根结点即算术运算符,然后随机生成在左右子树小于总运算符个数的运算符个数,同时生成运算符,当生成运算符个数为0,则生成叶子结点即运算操作数,左右子树也是重复上述过程。把生成的算式放在一个动态数组里面,每当生成一个算式,就去检查里面是否包含这个算式,如果重复就去掉,达到查重的目的。
判断对错的思路:使用readfile读取Exersises.txt文件内容,使用正则表达式分割开题目的序号和算式,算式是中缀表达式的表示方式,通过build方法把算式改为前缀表达式,如:1 + (( 2 + 3)* 4 ) – 5,转换成前缀则为- + 1 * + 2 3 4 5,计算其答案,读取Answers.txt的内容,使用正则表达式分割开题目的序号和答案,根据两个文件的序号,对比答案是否相同,如果相同则记录对的题目的数量,和题目序号,写出到Correction.txt文件。
(四)细节实现思路
1)如何保证基本的数值运算,确定参数的范围?
自然数的数值之间的运算可简单实现,但是自然数、真分数、带分数之间的运算之间的格式需要自己设计的,并且题目要求“如果存在形如e1÷ e2的子表达式,那么其结果应是真分数”,经过讨论之后,决定把所有数据统一当成分数来处理,整数的分母则为1,在运算的过程中把分子与分母独立出来分别操作加减乘除运算,到最后再进行约分等化简处理。
2)怎么生成算式并且查重?
生成的算式要求不能产生负数、生成的题目不能重复,且即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目,经过讨论,决定使用二叉树来实现,由于二叉树的运算次序是孩子结点的运算次序要优先于根结点的,所以使用非叶子节点存放运算符,叶子结点存放数值,可以解决前后符号之间的优先级别关系,解决括号的添加问题,当父母结点的算术符优先级高于右孩子结点的算术运算符时,左右都要加括号,当相等时,则右孩子树要加括号;出现了负数时,交换左右子树即可;此外,解决了查重的复杂度问题,开始的方案想采用遍历的方式来达到查重,现只需要判断两棵树是否相同即可。
五、程序关键代码
从页面获取到数据,进行处理:
package service; import java.util.HashMap; import java.util.Map; public class EntryJudge{ public boolean Entry(String[] args){ //向主页面返回的运行成功与否的标志 boolean tag = false; //判断用户输入是否正确 if(args.length == 0 || args.length % 2 != 0) { tag = false; return tag; } //取出参数 Map<String, String> params = checkParams(args); //执行相应处理 CreateAth opera = new CreateAth(params); Judge check = new Judge(params); if(params.containsKey("-e")&¶ms.containsKey("-a")){ check.Judge(); tag = true; return tag; } else if(params.containsKey("-n") || params.containsKey("-r") || params.containsKey("-d")) { opera.createAth(); tag = true; return tag; } return tag; } private Map<String, String> checkParams(String[] args) { Map<String, String> params = new HashMap<>(); for (int i = 0; i < args.length; i = i + 2) { params.put(args[i], args[i+1]); } return params; } }EntryJudge
题目生成逻辑处理:
CreateAth
题目判错处理:判断答案的正确性,并记录下来正确题目与错误题目序号,打印到Grade.txt
package service; import po.Fraction; import util.FileUtil; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * 答案判错类 */ public class Judge{ private int trueNum; //正确数目 private int wrongNum; //错误数目 private String exerciseFileName; // 题目文件名 private String answerFileName; // 答案文件名 public Judge(Map<String, String> params) { for (String str : params.keySet()) { if (str.equals("-e")) { exerciseFileName = params.get(str); } else if (str.equals("-a")) { answerFileName = params.get(str); } } } /** * 判断错误 ,并把错误写入文件 */ public void Judge() { long start = System.currentTimeMillis(); List<String> correctNums = new ArrayList<>(); List<String> wrongNums = new ArrayList<>(); FileUtil.readFile((exercise, answer) -> { String[] strs1 = exercise.split("\\."); //匹配每一行 String[] strs2 = answer.split("\\."); if (strs1[0].equals(strs2[0])) { CreateAth exes = new CreateAth(false); exes.build(strs1[1].trim()); //去掉两端的空格后,将后缀表达式生成树变成前缀的, if (exes.getResult().equals(new Fraction(strs2[1].trim()))) { //答案两边都相等,继续执行下面的 correctNums.add(strs1[0]); trueNum++; } else { wrongNums.add(strs1[0]); wrongNum++; } } }, exerciseFileName, answerFileName); FileUtil.writeFile(printResult(correctNums, wrongNums), "Correction.txt"); long end = System.currentTimeMillis(); System.out.println("题目答案对错统计存在当前目录下的Correction.txt文件下,耗时为:" + (end - start) + "ms"); } private String printResult(List<String> correctNums, List<String> wrongNums) { StringBuilder builder = new StringBuilder(); builder.append("Correct: ").append(trueNum).append(" ("); for (int i = 0; i < correctNums.size(); i++) { if (i == correctNums.size() - 1) { builder.append(correctNums.get(i)); break; } builder.append(correctNums.get(i)).append(", "); } builder.append(")").append("\n"); builder.append("Wrong: ").append(wrongNum).append(" ("); for (int i = 0; i < wrongNums.size(); i++) { if (i == wrongNums.size() - 1) { builder.append(wrongNums.get(i)); break; } builder.append(wrongNums.get(i)).append(", "); } builder.append(")").append("\n"); return builder.toString(); } }Judge
分数处理类:把所有随机生成的数都当成是分数处理,同时在些定义分数的四则运算方法
package po; /** * 1. 把所有随机生成的数都当成是分数处理(解决了自然整数,分数,带分数之间的差异) * 2. 定义了分数的四则运算类 */ public class Fraction{ private int mol; //分子 private int den; //分母 /** * 处理随机生成的数值(约分等),组合分数对象 */ public Fraction(int mol, int den) { this.mol = mol; this.den = den; if (den <= 0) { throw new RuntimeException("分数分母不能为0"); } //否则就进行约分 int mod = 1; int max = den > mol ? den : mol; for (int i = 1; i <= max; i++) { if (mol % i == 0 && den % i == 0) { mod = i; } } this.mol = mol / mod; this.den = den / mod; } /** * 处理随机生成的数值,这个用于分解分数对象(仅在判错中使用) */ public Fraction(String str) { int a = str.indexOf("'"); int b = str.indexOf("/"); if (a != -1) { //取出数组,转换类型 int c = Integer.valueOf(str.substring(0, a)); den = Integer.valueOf(str.substring(b + 1)); mol = c * den + Integer.valueOf(str.substring(a + 1, b)); } else if (b != -1) { String[] sirs = str.split("/"); mol = Integer.valueOf(sirs[0]); den = Integer.valueOf(sirs[1]); } else { mol = Integer.valueOf(str); den = 1; } }/** * 定义加减乘除类,返回值类型(全都当成分数处理),由于要返回这个类的内容,所以方法前要加类名 */ public Fraction add(Fraction fraction) { return new Fraction(this.mol * fraction.den + this.den * fraction.mol, this.den * fraction.den); } public Fraction subtract(Fraction fraction) { return new Fraction(this.mol * fraction.den - this.den * fraction.mol, this.den * fraction.den); } public Fraction multiply(Fraction fraction) { return new Fraction(this.mol * fraction.mol, this.den * fraction.den); } public Fraction divide(Fraction fraction) { return new Fraction(this.mol * fraction.den, this.den * fraction.mol); } }Fraction
题目实现存放类以及其子类:
package po; import java.util.Objects; /** * 用于存放题目的类,用二叉树的形式 */ public class Deposit{ private Deposit left; private Deposit right; private Fraction value; //用于二叉树结点的是符号与运算结果数值的之间的变化 public Deposit(Fraction value, Deposit left, Deposit right){ this.value = value; this.left = left; this.right = right; } /** * 取结点数据 */ public Fraction getValue() { return value; } public Deposit getRight(){ return right; } public Deposit getLeft(){ return left; } /** * 设置结点数据 */ public Deposit(Fraction value){ this.value = value; } public void setLeft(Deposit left){ this.left = left; } public void setRight(Deposit right){ this.right = right ; } public void setValue(Fraction value) { this.value = value; } @Override public String toString() { return value.toString(); } /** * 用于查重,判断二棵树是否相同 */ @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Deposit)) return false; Deposit node = (Deposit) o; return Objects.equals(value, node.value) && Objects.equals(left, node.left) && Objects.equals(right, node.right); } } —————————————————————————————————————分割线—————————————————————————————————————————————— package po; /** * 用于记录符号结点,与Deposit类是一样的道理 */ public class ChildDeposit extends Deposit{ private String symbol; public ChildDeposit(String symbol, Deposit left, Deposit right){ super(null, left, right); this.symbol = symbol; } public String getSymbol() { return symbol; } @Override public String toString() { return " " + symbol + " "; } /** * 用于查重 */ @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof ChildDeposit)) return false; ChildDeposit that = (ChildDeposit) o; boolean flag = this.symbol != null && symbol.equals(that.symbol); if(!flag) return false; boolean left = this.getLeft() != null && getLeft().equals(that.getLeft()); boolean right = this.getRight() != null && getRight().equals(that.getRight()); //左右子树相同 if(left && right) { return true; } if(left ^ right) { return false; } //如果是加法或乘法由于满足交换律所以要判断 if(this.symbol.equals("+") || this.symbol.equals("x")) { left = this.getLeft() != null && getLeft().equals(that.getRight()); right = this.getRight() != null && getRight().equals(that.getLeft()); } return left && right; } }Deposit,ChildDeposit
文件读写工具类:处理文件的写入与读出
package util; import java.io.*; public class FileUtil{ /** * 写入文件中 */ public static void writeFile(String content, String fileName) { File file = new File(fileName); try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))){ if(!file.exists()){ file.createNewFile(); } bw.write(content); bw.flush(); } catch (IOException e) { System.out.println("文件操作失败..."); } } /** * 读文件内容 * @param callBack 回调接口,分别处理每一行 * @param exerciseFileName 题目文件 * @param answerFileName 答案文件 */ public static void readFile(ReaderCallBack callBack, String exerciseFileName, String answerFileName) { File exerciseFile = new File(exerciseFileName); File answerFile = new File(answerFileName); if(!exerciseFile.exists() || !answerFile.exists()) { System.out.println("文件不存在!"); return; } try (BufferedReader br1 = new BufferedReader(new FileReader(exerciseFileName)); BufferedReader br2 = new BufferedReader(new FileReader(answerFileName))) { String line1, line2; while ((line1 = br1.readLine()) != null && (line2 = br2.readLine()) != null) { callBack.deal(line1, line2); } } catch (IOException e) { System.out.println("读取文件失败!"); } } public interface ReaderCallBack { void deal(String exercise, String answer) throws IOException; } }FileUtil
图形界面:
MainView :主页面,用户在两种操作中选一进行
package view; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class MainView { public static void main(String[] args) { new MainJFrame(); } } class MainJFrame extends JFrame { /** * 程序主界面 */ private static final long serialVersionUID = 1; //定义全局变量 private JLabel title; private JButton generate,judge; private JLabel result; private JPanel down = new JPanel(); //创建一个容器 Container ct; MainJFrame(){ ct=this.getContentPane(); this.setLayout(null);//设置容器为空布局,绝对定位 //标题 title= new JLabel("四则运算题目生成程序"); title.setFont(new Font("微软雅黑",Font.BOLD, 30)); title.setBounds(140, 40, 340, 100); //生成 generate = new JButton("生成题目"); generate.setBounds(120, 220, 140, 40); generate.addActionListener(new generateListener()); //判错 judge = new JButton("判断对错"); judge.setBounds(300, 220, 140, 40); judge.addActionListener(new judgeListenner()); //添加组件 ct.add(title); ct.add(generate); ct.add(judge); ct.add(down); this.setTitle("MyApp"); this.setSize(600, 450);// 设置窗口大小 this.setLocationRelativeTo(null);//基本设置 把窗口位置设置到屏幕中心 this.setVisible(true);//显示窗口 this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //关闭窗口 当点击窗口的关闭按钮时退出程序(没有这一句,程序不会退出) } class generateListener implements ActionListener { //监听生成按钮点击事件 public void actionPerformed(ActionEvent e) { new GenerateView(); } } class judgeListenner implements ActionListener{ //监听判错按钮点击事件 public void actionPerformed(ActionEvent e) { new JudgeView(); } } }MainView
GenerateView:用户输入参数范围、生成题目的页面
package view; import service.EntryJudge; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class GenerateView extends JFrame { private JLabel title,result; private JButton confirm; private JLabel subjectNum,intArea,denArea; private JTextField subjectNumField,intAreaField,denAreaField; private JPanel down = new JPanel(); public GenerateView() { //创建一个容器 Container ct; ct=this.getContentPane(); // this.setLayout(null);//设置容器为空布局,绝对定位 this.setSize(600, 450);//基本设置 this.setLocationRelativeTo(null); this.setLayout(null); title = new JLabel("生成题目"); title.setFont(new Font("微软雅黑",Font.BOLD, 20)); title.setBounds(200,20,280,60); //生成题目数 subjectNum = new JLabel("请输入生成题目数:"); subjectNum.setFont(new Font("微软雅黑",Font.BOLD, 16)); subjectNum.setBounds(70,100,160,50); subjectNumField = new JTextField (10); subjectNumField.setBounds(260,100,260,40); //整数范围 intArea = new JLabel("请输入整数范围:"); intArea.setFont(new Font("微软雅黑",Font.BOLD, 16)); intArea.setBounds(70,150,160,50); intAreaField = new JTextField (20); intAreaField.setBounds(260,150,260,40); //分母范围 denArea = new JLabel("请输入分数分母的范围:"); denArea.setFont(new Font("微软雅黑",Font.BOLD, 16)); denArea.setBounds(70,200,180,50); denAreaField = new JTextField (20); denAreaField.setBounds(260,200,260,40); confirm = new JButton("确定"); confirm.setBounds(250,270, 60, 50); confirm.addActionListener(new ConfirmActionListener()); //设置底部panel down.setBounds(130, 330, 280, 50); //设置底部panel背景透明 down.setBackground(null); down.setOpaque(false); result= new JLabel(); result.setFont(new Font("微软雅黑",Font.BOLD, 18)); //添加组件 down.add(result); ct.add(title); ct.add(subjectNum); ct.add(subjectNumField); ct.add(intArea); ct.add(intAreaField); ct.add(denArea); ct.add(denAreaField); ct.add(confirm); ct.add(down); this.setVisible(true);//显示窗口 this.setLocationRelativeTo(null);//基本设置 把窗口位置设置到屏幕中心 this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); //关闭窗口 } class ConfirmActionListener implements ActionListener { public void actionPerformed(ActionEvent e) { String args1 = subjectNumField.getText(); String args2 = intAreaField.getText(); String args3 = denAreaField.getText(); String [] args = new String[]{"-n",args1,"-r",args2,"-d",args3}; EntryJudge ej = new EntryJudge(); boolean res = ej.Entry(args); //获取命令执行结果 if(res == true) { result.setText("结果:生成题目成功" ); } else{ result.setText("结果:生成题目失败" ); } } } }GenerateView
JudegeView:用户选择文件,是判断对错的页面
package view; import service.EntryJudge; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class JudgeView extends JFrame { private JLabel title,result; private JButton confirm; private JLabel subjectNum,intArea,file1,file2; private JButton btn1,btn2; private JPanel down = new JPanel(); public JudgeView() { //创建一个容器 Container ct; ct=this.getContentPane(); this.setSize(600, 450);//基本设置 this.setLocationRelativeTo(null); this.setLayout(null); title = new JLabel("判断对错"); title.setFont(new Font("微软雅黑",Font.BOLD, 20)); title.setBounds(200,20,280,60); //生成题目数 subjectNum = new JLabel("请选择题目文件:"); subjectNum.setFont(new Font("微软雅黑",Font.BOLD, 16)); subjectNum.setBounds(70,100,160,50); btn1 = new JButton ("选择文件"); btn1.setBounds(240,100,120,40); btn1.addActionListener(new ChooseFile1ActionListener()); file1 = new JLabel(); file1.setFont(new Font("微软雅黑",Font.BOLD, 14)); file1.setBounds(390,100,130,50); //整数范围 intArea = new JLabel("请输入答案文件:"); intArea.setFont(new Font("微软雅黑",Font.BOLD, 16)); intArea.setBounds(70,150,160,50); btn2 = new JButton ("选择文件"); btn2.setBounds(240,150,120,40); btn2.addActionListener(new ChooseFile2ActionListener()); file2 = new JLabel(); file2.setFont(new Font("微软雅黑",Font.BOLD, 14)); file2.setBounds(390,150,130,50); confirm = new JButton("确定"); confirm.setBounds(250,270, 60, 50); confirm.addActionListener(new ConfirmActionListener()); //设置底部panel down.setBounds(130, 330, 280, 50); //设置底部panel背景透明 down.setBackground(null); down.setOpaque(false); result= new JLabel(); result.setFont(new Font("微软雅黑",Font.BOLD, 18)); //添加组件 down.add(result); ct.add(title); ct.add(subjectNum); ct.add(btn1); ct.add(file1); ct.add(intArea); ct.add(btn2); ct.add(file2); ct.add(confirm); ct.add(down); this.setVisible(true);//显示窗口 this.setLocationRelativeTo(null);//基本设置 把窗口位置设置到屏幕中心 this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); //关闭窗口 } class ChooseFile1ActionListener implements ActionListener { public void actionPerformed(ActionEvent e) { JFileChooser chooser = new JFileChooser(); //设置选择器 chooser.setMultiSelectionEnabled(true); //设为多选 int returnVal = chooser.showDialog(new JLabel(),"选择"); String filename = chooser.getSelectedFile().getName(); //获取绝对路径 file1.setText(filename); } } class ChooseFile2ActionListener implements ActionListener { public void actionPerformed(ActionEvent e) { JFileChooser chooser2 = new JFileChooser(); //设置选择器 chooser2.setMultiSelectionEnabled(true); //设为多选 int returnVal = chooser2.showDialog(new JLabel(),"选择"); String filename = chooser2.getSelectedFile().getName(); //获取绝对路径 file2.setText(filename); } } class ConfirmActionListener implements ActionListener { public void actionPerformed(ActionEvent e) { //获取结果 String [] args = new String[]{"-e", "Exercises.txt","-a","Answers.txt"}; EntryJudge ej = new EntryJudge(); boolean res = ej.Entry(args); //获取命令执行结果 if(res == true) { result.setText("结果:生成题目成功" ); } else{ result.setText("结果:生成题目失败" ); } } } }JudegeView
六、测试测试结果
程序运行,成功打开图形界面:
点击生成题目:
测试题目生成是否成功:
输入相关参数,点击确定 ,生成题目成功:
查看Exersises.txt,成功生成题目:
查看Answers.txt,答案:
测试判错程序:
改错二道题的答案:
打开判错的图形界面:
选择算式文件:
选择答案文件:
选择文件后:
点击确定,查看统计文件Grade.txt:
生成一万道算式:
详细的github地址:
算式生成文件:https://github.com/Jasminejiamei/pairProgramming/blob/master/Exercises.txt
答案生成文件:https://github.com/Jasminejiamei/pairProgramming/blob/master/Answers.txt
部分截图:
七、项目小结
此次的结对编程,我们均涉及了代码的设计、编写以及测试。讨论项目的基本构思对我负责的前期编码十分重要,从基础数值的定义和生成,再到采用二叉树存放算式,最后一步步完成项目,通过两个人的讨论,使得项目的整体思路变得清晰。在后期的编码中我的伙伴对我的编程方式提出了更有条理性的建议,最终在小改动下让整个项目的代码变得更为整洁与条理。
多讨论交流意见对编程的帮助很大,在判错功能中,从开始的没有头绪,到讨论出来通过应用一个栈的实例:中缀表达式转前缀表达式,解决了判错的问题;开始项目生成大数量题目的速度很慢,在小伙伴丰富的编程经验下,指出了关于多线程运行程序的方式,并且通过优化一些代码,大大提高了效率。
项目成员:黄思扬(3117004657)、刘嘉媚(3217004685)