一、Coding.Net项目地址:https://git.coding.net/zhaoliguaner/SecondCalculate.git
URL:http://47.93.197.5:8080/zhaol/login.jsp
二、PSP表格记录程序在各个模块的开发上耗费的时间
PSP | 任务内容 | 计划共完成需要的时间(min) | 实际完成需要的时间(min) |
Planning | 计划 | 10 | 6 |
· Estimate | · 估计这个任务需要多少时间,并规划大致工作步骤 | 10 | 6 |
Development | 开发 | 2548 | 4209 |
· Analysis | · 需求分析 (包括学习新技术) | 30 | 60 |
· Design Spec | · 生成设计文档 | 5 | 5 |
· Design Review | · 设计复审 (和同事审核设计文档) | 5 | 3 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 3 | 3 |
· Design | · 具体设计 | 15 | 40 |
· Coding | · 具体编码 | 1200 | 1800 |
· Code Review | · 代码复审 | 90 | 300 |
· Test | · 测试(自我测试,修改代码,提交修改) | 1200 | 2000 |
Reporting | 报告 | 145 | 370 |
· Test Report | · 测试报告 | 20 | 15 |
· Size Measurement | · 计算工作量 | 5 | 5 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 120 | 260 |
三、如何对接口实现设计
1、
1 private static boolean isOperator(String oper){ 2 if (oper.equals("+")||oper.equals("-")||oper.equals("÷")||oper.equals("*") 3 ||oper.equals("(")||oper.equals(")")) { 4 return true; 5 } 6 return false; 7 } 8 9 //计算操作符的优先级 10 private int priority(String s){ 11 switch (s) { 12 case "+":return 1; 13 case "-":return 1; 14 case "*":return 2; 15 case "÷":return 2; 16 case "(":return 3; 17 case ")":return 3; 18 default :return 0; 19 } 20 }
接口化设计能大大降低程序的耦合度,低耦合一定离不开接口化设计。在程序的设计及编码过程中,尽量采用模块化设计思想。此次结对项目是用网页版的形式完成的,运用了MVC设计模式,前后端彻底分离,不同的处理方法分属不同的类,在控制层方法,进行前后台的交互,接收前台传来的数据、进行数据处理,再调用模型层的方法(即核心类的计算方法)得到数据,最后由控制层将数据传给视图层,显示在网页上。
3、
松耦合系统通常是基于消息的系统,此时客户端和远程服务并不知道对方是如何实现的。客户端和服务之间的通讯由消息的架构支配。只要消息符合协商的架构,则客户端或服务的实现就可以根据需要进行更改,而不必担心会破坏对方。更新一个模块不会引起其它模块的改变。
四、计算模块接口的设计与实现过程
先随机生成用户所需个数的操作符,再随机生成表达式,这时,根据用户的需求决定是否生成括号。再将前缀表达式转换为后缀表达式,caculate()对后缀表达式进行计算,并将不符合要求的式子排除掉,接着打印到文件,并把式子输出到网页,用js比较正确答案与输入答案判断输入答案是否正确,并记录答题的时间和正确率。
比较独到的地方:计算式的产生(可以控制是否产生括号、随机产生出现括号的位置、对不合理的括号进行排查)
函数:
1. LinkedList<String> expression(int n,int c,int b,int min,int max ) 产生运算式,并可以选择是否需要括号
2. int[] operator(int n,int c) 产生随机操作符(加减乘除),对于乘除,可以选择是否需要
3. int decide(int x,int y) 通过递归实现整除
4. int transferToPostfix(LinkedList<String> list,int n,String[] ss,int calRange) 将中缀表达式转化为后缀表达式
5. int calculate(int n,String[] ss,int calRange) 计算后缀表达式
6. boolean isOperator(String oper) 进行操作符的判断
7. int priority(String s) 进行操作符优先级排序
8. int cal(int num1,int num2,String operator) 进行数字间的运算
9. print()将生成的计算式输出到文件
10.List<Exercise> Generate(int quesnum,int o,int min,int max,int c,int b,int calRange) 进行出题
五、计算模块接口部分的性能改进
在性能分析后,发现代码了代码的不足之处,在花费了40分钟的时间进行排查后,发现是因为IO输入流没有关闭所致,在输入结束后,将输入流关闭,会提高性能。程序中消耗最大的函数如下,原因是:1.在生成括号时,由于括号的产生是随机的,所以很有可能产生一些无效的括号,程序需要对这些无效括号进行排查、删除并重新生成;2.随机生成计算式后,可能会出现不能整除、在计算过程中超出范围限制的情况,所以要对随机生成的计算式进行检测,若不满足情况,则删除重新生成;3.在控制整除以及运算符种类时,用到了递归思想。由于上述原因,导致消耗资源。
1 public LinkedList<String> expression(int n,int c,int b,int min,int max ){ 2 char[] operator=new char[]{'+','-','*','÷'}; 3 Random random=new Random(); 4 LinkedList<String> expression=new LinkedList<String>(); 5 int ope[]= operator(n,c); //产生的运算符的个数 6 int[] number=new int[ope.length+1]; //运算数的个数,该数组存储运算数 7 8 for(int j=0;j<=ope.length;j++){ 9 number[j]=random.nextInt(max-min)+min; //限制产生的数字在上下界之间 10 } 11 12 int bracketnum=random.nextInt(ope.length); //随机产生括号的个数,题目限定括号个数小于运算符个数 13 if (b==1&&bracketnum>0) { //产生括号 14 // System.out.println("括号数:"+bracketnum); 15 int [] lbracketloc=new int[bracketnum]; 16 int [] rbracketloc=new int[bracketnum]; 17 int [] leftnum=new int[ope.length+1]; //记录每个数前的左括号的数量 18 int [] rightnum=new int[ope.length+1]; //记录每个数后的右括号的数量 19 for (int i = 0; i <bracketnum; i++) { 20 lbracketloc[i]=random.nextInt(ope.length); //随机产生左括号的位置,此左括号在第几个运算数前面,运算数不包括最后一个数, 21 rbracketloc[i]=random.nextInt(ope.length)+1;//随机产生右括号的位置,此右括号在第几个运算数后面,运算数不包括第一个数 22 if (rbracketloc[i]<=lbracketloc[i]) { //若右括号的位置在左括号前面或左右两个括号只括住一个数字,则去掉本次产生的括号 23 i--; //这个方法只是初次保证在一次循环下产生的左右括号不括住一个数字,但仍有可能出现这种情况 24 } //在分次循环时,仍有可能出现括住一个数字的情况 25 } 26 27 for (int i = 0; i < bracketnum; i++) { //利用桶函数的思想,记录每个运算数对应的括号的个数 28 leftnum[lbracketloc[i]]++; 29 rightnum[rbracketloc[i]]++; 30 } 31 32 for (int i = 0; i < ope.length+1; i++) { // 再次进行限制,保证左右括号不是只括住一个数字。对一个数进行检查,当其左右两边的括号数相等时, 33 if (!(leftnum[i]==0||rightnum[i]==0)) { // 就将它两边的括号都删掉,这样只是对单个数进行了成功的限制,但若将一个表达式作为整体看待, 34 while (!(leftnum[i]==0||rightnum[i]==0)) { //还是会出现这种情况 35 leftnum[i]--; 36 rightnum[i]--; 37 } 38 } 39 } 40 41 int right=0; //记录加进式子的右括号的数量 42 int left=0; // 记录加进式子的左括号的数量 43 for (int i = 0; i < ope.length; i++) { 44 for (int j = 0; j < leftnum[i]; j++) { 45 expression.add("("); 46 left++; 47 } 48 49 expression.add(String.valueOf(number[i])); 50 51 for (int j = 0; j < rightnum[i]; j++) { 52 expression.add(")"); 53 right++; 54 } 55 56 expression.add(String.valueOf(operator[ope[i]])); 57 if(ope[i]==3){ 58 number[i+1]=decide(number[i],number[i+1]); //此处为整除的初步控制,控制可能会无效,因为在某些情况下 59 //加上括号后,整体的运算整个都变了,要在计算过程中去二次控制整除 60 } //在下面不加括号的处理中,也会因为运算顺序的改变而出现控制无效的情况 61 } //所以这段代码只能初步控制整除 62 expression.add(String.valueOf(number[ope.length])); 63 for (int i = right; i < left; i++) { 64 expression.add(")"); 65 } 66 } 67 else { 68 for(int i=0;i<ope.length;i++){ 69 expression.add(String.valueOf(number[i])); 70 expression.add(String.valueOf(operator[ope[i]])); 71 if(ope[i]==3){ 72 number[i+1]=decide(number[i],number[i+1]); 73 } 74 } 75 expression.add(String.valueOf(number[ope.length])); 76 } 77 return expression; 78 }
1 private static int decide(int x,int y){//通过递归实现整除 2 Random random=new Random(); 3 if(x%y!=0){ 4 y=random.nextInt(10)+1; 5 return decide(x,y); 6 } 7 else{ 8 return y; 9 } 10 }
六、计算模块部分单元测试展示
构造测试数据的思路:因为在核心类中写了很多的方法,一开始也并没有想到要用单元测试,又用 if--else语句分了多种情况,所以在单元测试中采用多组数据对不同的情况进行测试。
单元测试覆盖率截图如下,从图中看出,代码测试覆盖率已经达到90%。
将单元测试得到的测试覆盖率截图:
可以看出,方法中大多数方法语句已经测试到(绿色覆盖),少部分没有测试完全(黄色覆盖),很少的部分没有测试到(红色覆盖),我也会继续努力改进,再提高代码的测试覆盖率的。
下面是项目部分单元测试代码,所测试的函数已在代码备注中体现,如下:
1 @Test 2 public void testOperator(){//测试产生操作符 3 mQuestion.operator(3, 1); 4 mQuestion.operator(5, 0); 5 mQuestion.operator(1, 0); 6 mQuestion.operator(1, 1); 7 } 8 @Test 9 public void testCore(){//测试产生计算式、中缀转后缀、计算 10 LinkedList<String> list=mQuestion.expression(7, 1, 1, 2, 99); 11 LinkedList<String> list1=mQuestion.expression(1, 0, 0, 2, 99); 12 Iterator<String> it=list.iterator(); 13 StringBuilder sd=new StringBuilder(); 14 while (it.hasNext()) { 15 sd.append(it.next()).append(" "); 16 } 17 String[] ss=sd.toString().split(" "); 18 19 Iterator<String> it1=list1.iterator(); 20 StringBuilder sd1=new StringBuilder(); 21 while (it1.hasNext()) { 22 sd1.append(it1.next()).append(" "); 23 } 24 String[] ss1=sd1.toString().split(" "); 25 26 mQuestion.transferToPostfix(list, 6, ss, 1000, "test.txt"); 27 mQuestion.calculate(6, ss, 1000, "test.txt"); 28 mQuestion.transferToPostfix2(list, 6, ss, 1000); 29 mQuestion.calculate2(6, ss, 1000); 30 31 mQuestion.transferToPostfix(list1, 1, ss1, 1000, "test.txt"); 32 mQuestion.calculate(1, ss1, 1000, "test.txt"); 33 mQuestion.transferToPostfix2(list1, 1, ss1, 1000); 34 mQuestion.calculate2(1, ss1, 1000); 35 } 36 @Test 37 public void testPrint(){ //测试打印 38 mQuestion.print("succeed", "test.txt"); 39 } 40 @Test 41 public void testDecide(){//测试控制整除的递归 42 int m=mQuestion.decide(10, 5); 43 assertThat(m, is(5)); 44 int n=mQuestion.decide(10, 3); 45 assertThat(10%n, is(0)); 46 } 47 @Test 48 public void testIsOpe(){ 49 assertThat(mQuestion.isOperator("5"), is(false)); 50 assertThat(mQuestion.isOperator("+"), is(true)); 51 assertThat(mQuestion.isOperator("-"), is(true)); 52 assertThat(mQuestion.isOperator("*"), is(true)); 53 assertThat(mQuestion.isOperator("÷"), is(true)); 54 assertThat(mQuestion.isOperator("("), is(true)); 55 assertThat(mQuestion.isOperator(")"), is(true)); 56 }
七、计算模块部分异常处理说明
命令行部分单元测试
题目个数异常测试,
1 @Test
2 public void testIn0(){ 3 String args[]={"-n","20000","-o","6","-m","2","100","-c","-b"}; 4 Command.main(args); 5 }
运算符个数异常测试,
1 @Test 2 public void testIn1(){ 3 String args[]={"-n","3","-o","11","-m","2","100","-c","-b"}; 4 Command.main(args); 5 }
运算数下界异常测试,正常的下界参数范围为1~100,对下界不在正常范围的情况进行测试。
1 @Test 2 public void testIn2(){ 3 String args[]={"-n","3","-o","6","-m","-1","100","-c","-b"}; 4 Command.main(args); 5 }
运算数上界异常测试,正常的上界参数范围为50~1000,对上界不在正常范围的情况进行测试。
1 @Test 2 public void testIn3(){ 3 String args[]={"-n","3","-o","6","-m","2","10000","-c","-b"}; 4 Command.main(args); 5 }
错误输入异常测试
1 @Test 2 public void testIn4(){ 3 String args[]={"-b","3","-o","6","-m","2","100","-d","-n"}; 4 Command.main(args); 5 }
正常情况测试,改变命令行参数的前后顺序进行测试。
1 @Test 2 public void testIn(){ 3 String args[]={"-b","3","-o","6","-m","2","100","-c","-n"}; 4 Command.main(args); 5 }
八、界面模块的详细设计过程
登录界面:用户通过输入学号登录,若不输入学号,或者所输入的学号不全为数字,则登录不成功。
首页界面:首页部分主要包括三个部分:出题做题、查看记录、最佳记录,点击能查看相应内容。
出题界面:在这个页面能生成用户所需要的题目,用表单验证验证限制输入(题目数量、运算值的上界、运算值的下界、运算符个数),若输入的要求不在限制的范围之内,或者所输入的不是数字,则不能提交成功。提交成功后会跳转到做题界面。
做题界面:在此页面会得到由后端生成的题目,首先点击开始按钮开始计时,用户开始答题,回答完之后点击停止按钮并提交,得到答对的题目数量和所用时间。
1 <script>//将结果进行比对 2 var Allda=document.getElementsByClassName("daan"); 3 var count=0; 4 for(var i=0;i<Allda.length;i++){ 5 if(Allda[this]==c[this]){ 6 count++; 7 } 8 var x=count/ALLda.length; 9 } 10 </script>
1 <script type="text/javascript">//统计做题的时长 2 var po=document.getElementById('po'); 3 var n=null; 4 var timert=0; 5 function timeCount(){ 6 clearInterval(timert); 7 timert=setInterval(function(){n=n+1;po.innerHTML=n;},1000); 8 } 9 function stopCount(){ 10 clearInterval(timert); 11 } 12 </script>
1 //出题数量框 2 Quenum.onfocus = function() { 3 Quenump.innerHTML = "请输入出题数量"; 4 } 5 6 Quenum.onblur = function() { 7 if(this.value < 1) { 8 Quenump.innerHTML = '<i class="no"></i>出题数量不能小于1'; 9 } else if(this.value>10000) { 10 Quenump.innerHTML = '<i class ="no"></i>出题数量不能大于10000'; 11 } else { 12 Quenump.innerHTML = '<i class="ok"></i>正确'; 13 } 14 }
九、界面模块与计算模块的对接
用servlet将界面模块与计算模块进行对接。用控制层的方法前后台交互,接收前台传来的数据,进行处理,调用模型层的方法得到数据,再由控制层将数据传到视图层。
1 public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 2 3 response.setContentType("text/html"); 4 request.setCharacterEncoding("utf-8"); 5 response.setCharacterEncoding("utf-8"); 6 String flag=request.getParameter("flag"); 7 8 switch (flag) { 9 case "login": 10 getLogin(request,response); 11 case "score": 12 getScore(request, response); 13 break; 14 case "record": 15 getRecord(request, response); 16 break; 17 case "question": 18 MakeQue(request, response); 19 break; 20 } 21 22 }
1 public void getLogin(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException { 2 String stuID = request.getParameter("stuID"); 3 request.getSession().setAttribute("stuID", stuID); 4 request.getRequestDispatcher("/index.jsp").forward(request, response); 5 } 6 7 public void getScore(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException { 8 String queNum=request.getParameter("queNum"); 9 String corNum=request.getParameter("corNum"); 10 String time=request.getParameter("time"); 11 String stuID=(String) request.getSession().getAttribute("stuID"); 12 double correct=Double.valueOf(corNum)/Double.valueOf(queNum)*100.0;//Double类将一个String字符串转换为浮点型的方式有两个,一个是parseDouble(java.lang.String) 方法, 13 //一个是valueOf(java.lang.String)方法。 14 long l = System.currentTimeMillis(); //得到日期 15 //new日期对象 16 Date date = new Date(l); 17 //转换提日期输出格式 18 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 19 20 try { 21 String content=stuID+"+"+correct+"+"+time+"+"+ dateFormat.format(date)+"\r\n"; 22 FileOutputStream fos = new FileOutputStream(new File("score.txt"),true);//建立文件流,用以输出答题结果 23 fos.write(content.getBytes()); 24 fos.flush(); 25 fos.close(); 26 request.getRequestDispatcher("/index.jsp").forward(request, response); 27 } catch (FileNotFoundException e) { 28 e.printStackTrace(); 29 } catch (IOException e) { 30 e.printStackTrace(); 31 } 32 } 33 34 public void getRecord(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException { 35 File file = new File("score.txt"); 36 BufferedReader reader = null; 37 List<StuInfor> stuInfor=new ArrayList<StuInfor>(); 38 String[] a; 39 try { 40 reader = new BufferedReader(new FileReader(file)); 41 String tempString = null; 42 while ((tempString = reader.readLine()) != null) { 43 StuInfor stu=new StuInfor(); 44 a=tempString.split("\\+"); //写成tempString.split("+")是不对的 45 stu.setStuID(a[0]); 46 stu.setCorrect(a[1]); 47 stu.setTime(a[2]); 48 stu.setDate(a[3]); 49 stuInfor.add(stu); 50 } 51 reader.close(); 52 } catch (IOException e) { 53 e.printStackTrace(); 54 } finally { 55 if (reader != null) { 56 try { 57 reader.close(); 58 } catch (IOException e1) { 59 } 60 } 61 } 62 int k=Integer.parseInt(request.getParameter("k")); 63 if(k==0){ 64 request.setAttribute("stuInfor", stuInfor); 65 // request.setAttribute("k", 0); 66 request.getRequestDispatcher("/record.jsp").forward(request, response); 67 }else{ 68 69 Collections.sort(stuInfor, new Comparator<StuInfor>(){ //根据正确率进行排序 70 71 public int compare(StuInfor o1, StuInfor o2) { 72 if(Double.valueOf(o1.getCorrect())<Double.valueOf(o2.getCorrect())){ 73 return 1; 74 } 75 if(o1.getCorrect() == o2.getCorrect()){ 76 return 0; 77 } 78 return -1; 79 } 80 }); 81 82 for(int i = 0 ; i < stuInfor.size() ; i++) { 83 for(int j = i+1 ; j < stuInfor.size() ; j++) { 84 String aString=stuInfor.get(i).getStuID(); 85 String bString=stuInfor.get(j).getStuID(); 86 System.out.println("a:"+aString); 87 System.out.println("b:"+bString); 88 System.out.println("xiangde:"+bString.equals(aString)); 89 if(bString.equals(aString)){ 90 stuInfor.remove(j); 91 j--; 92 } 93 } 94 } 95 96 request.setAttribute("stuInfor", stuInfor); 97 // request.setAttribute("k", 1); 98 request.getRequestDispatcher("/record.jsp").forward(request, response); 99 } 100 } 101 102 public void MakeQue (HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException { 103 int quesnum = Integer.parseInt(request.getParameter("quesnum"));//题目数量,必须项 104 int minnum = Integer.parseInt(request.getParameter("minnum")); //题目的数值范围,必须项 105 int maxnum = Integer.parseInt(request.getParameter("maxnum")); //题目的数值范围,必须项 106 int maxresult = maxnum+1000; //题目计算过程的数值范围,非必须,默认为题目上界数+1000 107 int maxoperator=1 ;//最多的操作符数量,非必须,默认为1 108 int c=0;//是否要乘除,默认不要,为0 109 int b=0;//是否要括号,默认不要,为0 110 111 if(request.getParameter("maxoperator")!=null&&!(request.getParameter("maxoperator").equals(""))){//这句代码之前一直是复制粘贴 112 maxoperator = Integer.parseInt(request.getParameter("maxoperator")); //没有这么在意,自己这次自己写这句话 113 } //还是出了这么些问题,博客记录一下吧 114 115 if(request.getParameter("maxresult")!=null&&!(request.getParameter("maxresult").equals(""))){ 116 maxresult = Integer.parseInt(request.getParameter("maxresult")); 117 } 118 119 if(request.getParameter("mulorsub")!=null){//选择单选框后,会给后台传值on 120 c=1; 121 } 122 123 if(request.getParameter("bracket")!=null){ 124 b=1; 125 } 126 List<Exercise> eList=new MakeQuestion().Generate(quesnum, maxoperator, minnum, maxnum, c, b, maxresult); 127 request.setAttribute("eList", eList); 128 request.getRequestDispatcher("answer.jsp").forward(request, response); 129 } 130 131 }
十、描述结对的过程
十一、结对编程的优点和缺点
结对编程
优点:结对编程是两个人共同完成一个项目,因此,对对方写的代码也能比较熟悉,在这个过程中,能学习到很多对方在编程上一些好的方法与习惯,能使自己在较短的时间内获得进步。遇到问题,能两个人一起解决,相较与个人独立编程,解决问题的时间明显缩短了。结对的时候,每个人可以尽量发挥自己的优势,自己不够擅长的地方,在结对的过程中多学习,不仅将项目完成的更好了,也能让自己学习到更多的东西。
缺点:结对编程由于是合作的过程,可能自己擅长的更擅长,但是,不擅长的地方有时候提升得就比较慢。结对编程对合作的要求比较高,如果两个人在编程时都自己做自己的,并且交流比较少的话,可能会导致在两部分合起来的时候遇到很多问题。
个人
我的优点:1.比较认真,能在项目上投入大量的时间
2.遇到不懂得问题,认真向他人学习
3.即使遇到问题也尽力解决,不浮躁
我的缺点:1.编程能力较弱,一个比较简单的功能也要花很多的时间
2.自己独立修改Bug的能力较差
赵莉的优点:1.编程能力很强,能将自己的任务做得十分完整。
2.编程时十分认真投入,遇到问题都能独立解决
3.在我遇到问题的时候鼓励我,并且帮助我解决问题
十二、实际花费的时间
实际花费的时间在第二部分。
总结
这次结对项目花了很多时间,清明节前几天就开始准备了,再加上老师延长了提交时间,时间上虽然足够充裕,但是过程却是一波三折,常常一个小问题几天也解决不了,曾经也有过想要放弃的念头。现在项目已经完成了,对编程有了进一步的熟悉,学到了很多,也感觉到了一定的成就感。