前言:还是写写前言来记录一下自己的心情吧。这次的结对项目我还真是做了很久的,因为之前个人项目做的不够完善,所以开始的工作是在个人项目的基础上实现括号功能,大约花了一两天的空闲时间吧;接下来才开始正式着手本次结对项目,刚刚开始时,觉着好难,题目都不懂(现在想来,主要是因为我不能沉下心来去读那一篇题目描述,看到那么长,就退缩了);在起初开发时,心态也不够稳定,开发效率并不高,基本上属于指哪打哪,没有在开发之前对整个系统进行一个完整的规划,近两天心态平稳一些进行了一个规划,效率提高不少,就算是一个经验吧,以后要好好规划项目,然后再进行编码,应该效率会更高(通过下面的PSP表格,也能看出我在代码设计方面确实存在问题)。在这儿,想感谢我工作室亲爱的小伙伴们,在这次项目中给了我很多帮助,也让我明白了团队的力量,“独学而无友,孤陋则寡闻”,大家一起学习,相互帮助,共同进步,是很美好的事情。
一、项目地址 https://git.coding.net/zhaoliguaner/SecondCalculate.git
URL:http://47.93.197.5:8080/zhaol/login.jsp
项目说明:项目整体放在了SecondCalculate文件夹下,里面包含命令行出题部分代码Calculate,以及网页版完整源代码(web文件夹下)。命令行测试入口为src下Command.java;在src下将其编译,即可输入数据进行测试运行。网页版测试因需要用到服务器,故事先将项目传到服务器,通过上述网址,可以进入此四则运算系统。
特别说明:为了保证出题的合理性和有效必要性,我在设置计算过程中的最大范围时,将其设置为运算数上界+1000,所以如果得到的题目在计算过程中或最后结果大于运算数上界(正常情况下,计算过程中的结果应该小于等于上界+1000,运算过程中出现的最小数为运算数上界*(-1)),并非代码问题,而是设置问题。
二、PSP表格记录程序在各个模块的开发上耗费的时间
PSP |
任务内容 |
计划共完成需要的时间(min) |
实际完成需要的时间(min) |
Planning |
计划 |
10 |
6 |
· Estimate |
· 估计这个任务需要多少时间,并规划大致工作步骤 |
10 |
6 |
Development |
开发 |
2548 |
4209 |
· Analysis |
· 需求分析 (包括学习新技术) |
30 |
60 |
· Design Spec |
· 生成设计文档 |
5 |
3 |
· 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 |
60 |
· Size Measurement |
· 计算工作量 |
5 |
10 |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划(包括写博客时间) |
120 |
300 |
三、接口设计
1、
private static boolean isOperator(String oper){ if (oper.equals("+")||oper.equals("-")||oper.equals("÷")||oper.equals("*") ||oper.equals("(")||oper.equals(")")) { return true; } return false; } //计算操作符的优先级 private int priority(String s){ switch (s) { case "+":return 1; case "-":return 1; case "*":return 2; case "÷":return 2; case "(":return 3; case ")":return 3; default :return 0; } }
接口设计和低耦合是相辅相成的,接口化设计能大大降低程序的耦合度,低耦合一定离不开接口化设计。在程序的设计及编码过程中,尽量采用模块化设计思想。此次结对项目是用网页版的形式完成的,运用了MVC设计模式,前后端彻底分离,数据层层传递,在后台代码中进行模块化设计,(第四问的回答对模块化的方法有所介绍)并且不同的处理方法分属不同的类,是一种低耦合的设计;在控制层方法,进行前后台的交互,接收前台传来的数据、进行数据处理,再调用模型层的方法(即核心类的计算方法)得到数据(在调用核心类中的计算方法时,并不需要知道此计算方法如何实现,只需关注该方法可以实现的功能,是一种接口化的设计),最后由控制层将数据传给视图层,显示在网页上。
四、在结对编程中利用方法对接口进行设计
设计3个类、10个函数。该代码的重点是前缀表达式转化为后缀表达式及后缀表达式的计算。我认为比较独到的地方是计算式的产生(可以控制是否产生括号、随机产生出现括号的位置、对不合理的括号进行排查),计算式中括号的产生依托于计算式中数字的产生。
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.在控制整除以及运算符种类时,用到了递归思想。由于上述原因,导致消耗资源
public LinkedList<String> expression(int n,int c,int b,int min,int max ){ char[] operator=new char[]{'+','-','*','÷'}; Random random=new Random(); LinkedList<String> expression=new LinkedList<String>(); int ope[]= operator(n,c); //产生的运算符的个数 int[] number=new int[ope.length+1]; //运算数的个数,该数组存储运算数 for(int j=0;j<=ope.length;j++){ number[j]=random.nextInt(max-min)+min; //限制产生的数字在上下界之间 } int bracketnum=random.nextInt(ope.length); //随机产生括号的个数,题目限定括号个数小于运算符个数 if (b==1&&bracketnum>0) { //产生括号 // System.out.println("括号数:"+bracketnum); int [] lbracketloc=new int[bracketnum]; int [] rbracketloc=new int[bracketnum]; int [] leftnum=new int[ope.length+1]; //记录每个数前的左括号的数量 int [] rightnum=new int[ope.length+1]; //记录每个数后的右括号的数量 for (int i = 0; i <bracketnum; i++) { lbracketloc[i]=random.nextInt(ope.length); //随机产生左括号的位置,此左括号在第几个运算数前面,运算数不包括最后一个数, rbracketloc[i]=random.nextInt(ope.length)+1;//随机产生右括号的位置,此右括号在第几个运算数后面,运算数不包括第一个数 if (rbracketloc[i]<=lbracketloc[i]) { //若右括号的位置在左括号前面或左右两个括号只括住一个数字,则去掉本次产生的括号 i--; //这个方法只是初次保证在一次循环下产生的左右括号不括住一个数字,但仍有可能出现这种情况 } //在分次循环时,仍有可能出现括住一个数字的情况 } for (int i = 0; i < bracketnum; i++) { //利用桶函数的思想,记录每个运算数对应的括号的个数 leftnum[lbracketloc[i]]++; rightnum[rbracketloc[i]]++; } for (int i = 0; i < ope.length+1; i++) { // 再次进行限制,保证左右括号不是只括住一个数字。对一个数进行检查,当其左右两边的括号数相等时, if (!(leftnum[i]==0||rightnum[i]==0)) { // 就将它两边的括号都删掉,这样只是对单个数进行了成功的限制,但若将一个表达式作为整体看待, while (!(leftnum[i]==0||rightnum[i]==0)) { //还是会出现这种情况 leftnum[i]--; rightnum[i]--; } } } int right=0; //记录加进式子的右括号的数量 int left=0; // 记录加进式子的左括号的数量 for (int i = 0; i < ope.length; i++) { for (int j = 0; j < leftnum[i]; j++) { expression.add("("); left++; } expression.add(String.valueOf(number[i])); for (int j = 0; j < rightnum[i]; j++) { expression.add(")"); right++; } expression.add(String.valueOf(operator[ope[i]])); if(ope[i]==3){ number[i+1]=decide(number[i],number[i+1]); //此处为整除的初步控制,控制可能会无效,因为在某些情况下 //加上括号后,整体的运算整个都变了,要在计算过程中去二次控制整除 } //在下面不加括号的处理中,也会因为运算顺序的改变而出现控制无效的情况 } //所以这段代码只能初步控制整除 expression.add(String.valueOf(number[ope.length])); for (int i = right; i < left; i++) { expression.add(")"); } } else { for(int i=0;i<ope.length;i++){ expression.add(String.valueOf(number[i])); expression.add(String.valueOf(operator[ope[i]])); if(ope[i]==3){ number[i+1]=decide(number[i],number[i+1]); } } expression.add(String.valueOf(number[ope.length])); } return expression; }
private static int decide(int x,int y){//通过递归实现整除 Random random=new Random(); if(x%y!=0){ y=random.nextInt(10)+1; return decide(x,y); } else{ return y; } }
六、计算模块部分单元测试展示
构造测试数据的思路:因为在核心类中写了很多的方法,一开始也并没有想到要用单元测试,又用 if--else语句分了多种情况,所以在单元测试中采用多组数据对不同的情况进行测试。
单元测试覆盖率截图如下,从图中看出,代码测试覆盖率已经达到90%。
可以看出,方法中大多数方法语句已经测试到(绿色覆盖),少部分没有测试完全(黄色覆盖),很少的部分没有测试到(红色覆盖),我也会继续努力改进,再提高代码的测试覆盖率的。
下面是项目部分单元测试代码,所测试的函数已在代码备注中体现,如下:
@Test public void testOperator(){//测试产生操作符 mQuestion.operator(3, 1); mQuestion.operator(5, 0); mQuestion.operator(1, 0); mQuestion.operator(1, 1); } @Test public void testCore(){//测试产生计算式、中缀转后缀、计算 LinkedList<String> list=mQuestion.expression(7, 1, 1, 2, 99); LinkedList<String> list1=mQuestion.expression(1, 0, 0, 2, 99); Iterator<String> it=list.iterator(); StringBuilder sd=new StringBuilder(); while (it.hasNext()) { sd.append(it.next()).append(" "); } String[] ss=sd.toString().split(" "); Iterator<String> it1=list1.iterator(); StringBuilder sd1=new StringBuilder(); while (it1.hasNext()) { sd1.append(it1.next()).append(" "); } String[] ss1=sd1.toString().split(" "); mQuestion.transferToPostfix(list, 6, ss, 1000, "test.txt"); mQuestion.calculate(6, ss, 1000, "test.txt"); mQuestion.transferToPostfix2(list, 6, ss, 1000); mQuestion.calculate2(6, ss, 1000); mQuestion.transferToPostfix(list1, 1, ss1, 1000, "test.txt"); mQuestion.calculate(1, ss1, 1000, "test.txt"); mQuestion.transferToPostfix2(list1, 1, ss1, 1000); mQuestion.calculate2(1, ss1, 1000); } @Test public void testPrint(){ //测试打印 mQuestion.print("succeed", "test.txt"); } @Test public void testDecide(){//测试控制整除的递归 int m=mQuestion.decide(10, 5); assertThat(m, is(5)); int n=mQuestion.decide(10, 3); assertThat(10%n, is(0)); } @Test public void testIsOpe(){ assertThat(mQuestion.isOperator("5"), is(false)); assertThat(mQuestion.isOperator("+"), is(true)); assertThat(mQuestion.isOperator("-"), is(true)); assertThat(mQuestion.isOperator("*"), is(true)); assertThat(mQuestion.isOperator("÷"), is(true)); assertThat(mQuestion.isOperator("("), is(true)); assertThat(mQuestion.isOperator(")"), is(true)); }
七、异常处理说明
命令行部分单元测试
题目个数异常测试, 题目个数不在正常参数范围内的情况进行测试
@Test public void testIn0(){ String args[]={"-n","20000","-o","6","-m","2","100","-c","-b"}; Command.main(args); }
运算符个数异常测试,
@Test public void testIn1(){ String args[]={"-n","3","-o","11","-m","2","100","-c","-b"}; Command.main(args); }
运算数下界异常测试,正常的下界参数范围为1~100,对下界不在正常范围的情况进行测试
@Test public void testIn2(){ String args[]={"-n","3","-o","6","-m","-1","100","-c","-b"}; Command.main(args); }
运算数上界异常测试,正常的上界参数范围为50~1000,对上界不在正常范围的情况进行测试
@Test public void testIn3(){ String args[]={"-n","3","-o","6","-m","2","10000","-c","-b"}; Command.main(args); }
错误输入异常测试
@Test public void testIn4(){ String args[]={"-b","3","-o","6","-m","2","100","-d","-n"}; Command.main(args); }
正常情况测试,改变命令行参数的前后顺序进行测试
@Test public void testIn(){ String args[]={"-b","3","-o","6","-m","2","100","-c","-n"}; Command.main(args); }
八、界面模块详细设计过程
1.登录界面:用户在此界面输入学号进行登录,没有输入则不能登陆,登陆成功跳转到首页。
2.首页:用户可以选择要进行的功能(有三个功能:出题做题、查看记录、最佳记录)
3.做题界面:在Jsp页面中进行界面的设计(界面风格美观可爱,充满轻松的氛围),并用JS对参数(题目个数、运算符个数、运算数上下界、是否加乘除和括号)进行限制,用表单提交的方式将用户输入的数据传到控制层。
<body> <form id="form1" name="form1" method="post" action="AllServlet" onSubmit="return (check1() && check2()&&check3()&&check4());"> <div class="main"> <table> <tr> <td>输入题目数量</td> <td><input name="quesnum" type="text" id="quesnum"/></td> </tr> <tr> <td>运算数最小值</td> <td><input name="minnum" type="text" id="minnum"/></td> </tr> <tr> <td>运算数最大值</td> <td><input name="maxnum" type="text" id="maxnum"/></td> </tr> <tr> <td>运算符个数最大值</td> <td><input name="maxoperator" type="text" id="maxoperator" value="1"/></td> </tr> <tr> <td>是否包含乘除(包含请点击)</td> <td><input type="radio" name="mulorsub"/></td> </tr> <tr> <td>是否包含括号(包含请点击)</td> <td><input type="radio" name="bracket"/></td> </tr> <tr> <td> <input type="submit" value="提交"/></td> </tr> </div> </form> <script language="javascript"> function check1() { if((parseInt(form1.quesnum.value)) <1||(parseInt(form1.quesnum.value)) >10000) { alert("题目数量为1~10000"); return false; } else{ return true; } } function check2() { if((parseInt(form1.minnum.value <1))||(parseInt(form1.minnum.value)) >100) { alert("题目数值的下界为1~100"); return false; } else{ return true; } } function check3() { if((parseInt(form1.maxnum.value)<50)||(parseInt(form1.maxnum.value)>1000)) { alter("题目数值的上界为50~1000"); return false; } else{ return true; } } function check4() { if((parseInt(form1.maxoperator.value)) <1||(parseInt(form1.maxoperator.value)) >10) { alter("运算符的个数为1~10"); return false; } else{ return true; } } </script> </body>
4.做题页面,控制层通过调用核心计算方法,将产生的数据传递给answer.jsp展示界面,用jstl和EL表达式进行接收,将数据显示在前端,并通过JS进行答案的正误判断及做题时间的记录。
<body background="img/hehehe.jpg"> <form id="form2" name="form2" method="post" action="default.asp" onSubmit="return check5()"> <div class="main"> <table> <c:forEach items="${eList}" var="c"> <tr> <td>${c.queString}</td> <td><input name="corans" type="hidden" value="${c.ansString}"></td> </tr> <tr> <td>请输入运算结果</td> <td><input name="answer" type="text" id="answer"></td> </tr> </c:forEach> <tr> <td> <input type="submit" value="提交"/></td> </tr> </table> </div> </form> <div class="one"> <p id="po"></p> <button onclick="timeCount()">start</button> <button onclick="stopCount()">stop</button> </div> <script> var Allda=document.getElementsByClassName("daan"); var count=0; for(var i=0;i<Allda.length;i++){ if(Allda[this]==c[this]){ count++; } var x=count/ALLda.length; } </script> <script type="text/javascript"> // setInterval计时器 var po=document.getElementById('po'); var n=null; var timert=0; function timeCount(){ clearInterval(timert); timert=setInterval(function(){n=n+1;po.innerHTML=n;},1000); } function stopCount(){ clearInterval(timert); } </script> </body>
5.查看记录(最佳纪录):用户可在该页面查看做题记录,可查寻项包括学号、得分、用时、做题日期
九、界面模块与计算模块的对接
用servlet进行对接,在控制层的方法中,进行前后台的交互,接收前台传来的数据、进行数据处理,再调用模型层的方法(即核心类的计算方法)得到数据,最后由控制层将数据传给视图层,显示在网页上。
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); request.setCharacterEncoding("utf-8"); response.setCharacterEncoding("utf-8"); String flag=request.getParameter("flag"); switch (flag) { case "login": getLogin(request,response); case "score": getScore(request, response); break; case "record": getRecord(request, response); break; case "question": MakeQue(request, response); break; } }
public void getLogin(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException { String stuID = request.getParameter("stuID"); request.getSession().setAttribute("stuID", stuID); request.getRequestDispatcher("/index.jsp").forward(request, response); } public void getScore(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException { String queNum=request.getParameter("queNum"); String corNum=request.getParameter("corNum"); String time=request.getParameter("time"); String stuID=(String) request.getSession().getAttribute("stuID"); double correct=Double.valueOf(corNum)/Double.valueOf(queNum)*100.0;//Double类将一个String字符串转换为浮点型的方式有两个,一个是parseDouble(java.lang.String) 方法, //一个是valueOf(java.lang.String)方法。 long l = System.currentTimeMillis(); //得到日期 //new日期对象 Date date = new Date(l); //转换提日期输出格式 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); try { String content=stuID+"+"+correct+"+"+time+"+"+ dateFormat.format(date)+" "; FileOutputStream fos = new FileOutputStream(new File("score.txt"),true);//建立文件流,用以输出答题结果 fos.write(content.getBytes()); fos.flush(); fos.close(); request.getRequestDispatcher("/index.jsp").forward(request, response); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } public void getRecord(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException { File file = new File("score.txt"); BufferedReader reader = null; List<StuInfor> stuInfor=new ArrayList<StuInfor>(); String[] a; try { reader = new BufferedReader(new FileReader(file)); String tempString = null; while ((tempString = reader.readLine()) != null) { StuInfor stu=new StuInfor(); a=tempString.split("\+"); //写成tempString.split("+")是不对的 stu.setStuID(a[0]); stu.setCorrect(a[1]); stu.setTime(a[2]); stu.setDate(a[3]); stuInfor.add(stu); } reader.close(); } catch (IOException e) { e.printStackTrace(); } finally { if (reader != null) { try { reader.close(); } catch (IOException e1) { } } } int k=Integer.parseInt(request.getParameter("k")); if(k==0){ request.setAttribute("stuInfor", stuInfor); // request.setAttribute("k", 0); request.getRequestDispatcher("/record.jsp").forward(request, response); }else{ Collections.sort(stuInfor, new Comparator<StuInfor>(){ //根据正确率进行排序 public int compare(StuInfor o1, StuInfor o2) { if(Double.valueOf(o1.getCorrect())<Double.valueOf(o2.getCorrect())){ return 1; } if(o1.getCorrect() == o2.getCorrect()){ return 0; } return -1; } }); for(int i = 0 ; i < stuInfor.size() ; i++) { for(int j = i+1 ; j < stuInfor.size() ; j++) { String aString=stuInfor.get(i).getStuID(); String bString=stuInfor.get(j).getStuID(); System.out.println("a:"+aString); System.out.println("b:"+bString); System.out.println("xiangde:"+bString.equals(aString)); if(bString.equals(aString)){ stuInfor.remove(j); j--; } } } request.setAttribute("stuInfor", stuInfor); // request.setAttribute("k", 1); request.getRequestDispatcher("/record.jsp").forward(request, response); } } public void MakeQue (HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException { int quesnum = Integer.parseInt(request.getParameter("quesnum"));//题目数量,必须项 int minnum = Integer.parseInt(request.getParameter("minnum")); //题目的数值范围,必须项 int maxnum = Integer.parseInt(request.getParameter("maxnum")); //题目的数值范围,必须项 int maxresult = maxnum+1000; //题目计算过程的数值范围,非必须,默认为题目上界数+1000 int maxoperator=1 ;//最多的操作符数量,非必须,默认为1 int c=0;//是否要乘除,默认不要,为0 int b=0;//是否要括号,默认不要,为0 if(request.getParameter("maxoperator")!=null&&!(request.getParameter("maxoperator").equals(""))){//这句代码之前一直是复制粘贴 maxoperator = Integer.parseInt(request.getParameter("maxoperator")); //没有这么在意,自己这次自己写这句话 } //还是出了这么些问题,博客记录一下吧 if(request.getParameter("maxresult")!=null&&!(request.getParameter("maxresult").equals(""))){ maxresult = Integer.parseInt(request.getParameter("maxresult")); } if(request.getParameter("mulorsub")!=null){//选择单选框后,会给后台传值on c=1; } if(request.getParameter("bracket")!=null){ b=1; } List<Exercise> eList=new MakeQuestion().Generate(quesnum, maxoperator, minnum, maxnum, c, b, maxresult); request.setAttribute("eList", eList); request.getRequestDispatcher("answer.jsp").forward(request, response); } }
十、描述结对的过程
这是我和我亲爱的小伙伴邓茜茜的合作留念照片,也在这儿祝愿我们友谊长存,嘻嘻(●'◡'●)
十一、关于结对
结对优点:1、两个人进行项目分工,可以减轻负担
2、遇到问题,两人一起讨论,有利于加快问题的解决,可以提高效率,学习氛围也更加轻松
3、利于促进友情
结对缺点:1、如果缺乏足够的交流或是某一方词不达意,会出现项目理解上的误解
2、两人分工,虽然可以减轻负担,但是也失去了强制自己学习更多的压力/动力。
队友:认真、努力、可靠,对java语言不够熟悉
我:乐观、坚持、不怕困难,容易烦躁