一、测试的url地址及Coding.net地址
可测试的url地址:http://39.105.6.214/myWeb_war/
Coding.net源码仓库地址:https://git.coding.net/wanghz499/2016012032partnerWork.git
命令行测试command.java类(用到自己做的core.jar):
(注:请使用win+R命令提示符,不要使用PowerShell)
1.打开命令行进入src文件夹:cd C:\Users\wangh\Desktop\2016012032partnerWork\src(注意根据自己放的位置调整路径,下同)2.编译command.java和core.jar: javac -cp C:\Users\wangh\Desktop\2016012032partnerWork\web\WEB-INF\lib\core.jar -encoding utf-8 C:\Users\wangh\Desktop\2016012032partnerWork\src\Command.java(-cp表示路径,core.jar包放在web的WEB-INF的lib文件夹下)3.运行command.java和core.jar,并输入参数:java -cp ..\web\WEB-INF\lib\core.jar; Command -n 10 -m 1 100 -o 5运行成功,result.txt生成在与src同级的目录下。
二、估计项目开发时间
已记录在结尾的PSP表格,此处不重复显示。
三、设计接口原则
在设计接口之前,我们应该明白接口是做什么的,它有什么设计原则,怎样才能设计出好的接口。于是我去知乎上浏览了相关内容,有几点是我印象比较深刻的:
1.职责单一化,不要想着做件大事,提供各种通用性,尽量接口拆细,交给调用方自己去组合。
这句话表明一个接口就是做一件事的,因此我们在设计接口的时候要明确接口的作用,且作用不能广泛,应该清晰而单一。
2.业务的独立性,尽量对外屏蔽你的业务细节,同时也对调用者更加友好。
接口是提供给用户调用的,因此最好将接口的信息隐藏,而用户不必知道接口内的具体代码实现。而在本次结对项目中,我们的接口用户就是自己,尽管如此,我们也应该将它的具体实现隐藏起来,因此我们最终会将接口做成一个core.jar包。
3.拼写要准确,接口函数一旦发布就不能改了。函数最好是动宾结构doSomething,如openFile、setName。站在使用者的角度去思考,API设计也要讲究用户体验。
这些是接口命名的规范,接口的命名要能一眼就看出它的作用,这样会有更好的用户体验。并且在设计接口时,要从使用者的角度出发,如果这个接口让用户觉得使用起来很复杂,那么这就是个失败的接口了。
四、计算模块接口的设计与实现过程
在设计接口前,我先分析了下这个接口的作用:产生一定数量的符合条件的式子。因此这个接口需要接收关于数量和条件限制的参数,分别是以下6个参数:题目数量n,数值下界downBound,数值上界upBound,最大运算符数largeOperatorCount,是否有乘除hasMulDiv,是否有括号hasBracket。
首先,这个接口里共有2个类:Creat类和Calculator类,前者作用是产生规定数量的式子,后者的作用是判断某条式子是否符合条件。在Create类中,专门设计了一个方法generate()来接收并判断上述6个参数正确性,随后generate()方法再通过for循环调用n次createProblem()方法,createProblem()每次产生一条式子,在产生式子时createProblem()方法会调用Caculator类的algorithm()方法来预先计算这条式子的答案,由于algorithm()方法是用的调度场算法和后缀表达式求值,因此可以判断当前式子在计算过程中是否会产生小数、负数等,以此来达到筛选符合条件的式子的作用。这个接口最终返回的是一个装满符合条件式子的字符串数组。最后再考虑到接口信息隐藏的特性,将这两个类封装成了一个core.jar包,然后就可以直接调用了。core.jar内部结构示意图:
实现的关键在于generate()接收的6个参数在以上各个方法之间的传递,如generate()调用createProblem()时会将6个参数都传过去,随后createProblem()根据参数进行条件判断产生相应的式子;在createProblem()调用algorithm()时,会将数值下界downBound和上界upBound传过去以筛选计算过程和最终结果都在数值范围内的式子。
五、计算模块接口部分的性能改进
起初我的计算模块只是稍微改了一下第一次个人作业的代码,多接收了几个参数而已。后来发现当限制的数值范围比较小的时候,控制台报栈溢出异常。我去网上搜才发现是递归调用过度,线程已满导致程序崩溃。后来我检查我的代码的确好几处都用了递归,比如当前生成的式子不满足条件时就再递归调用生成式子的createProblem方法,再如运算符下标数组只要全部一样(即式子的运算符全一样)我也会再递归调用index()方法重新生成一个下标数组,总之多处用到了递归。当数值范围比较小时,生成的式子大多是不满足条件的,于是会频频递归产生新式子,当运算符个数比较少时,下标数组也很容易一样,会频繁递归调用index()方法,最终导致程序跑不了。
在知道是递归调用过度的原因后,根据报错信息我得知index()方法是程序中消耗最大的函数,于是我修改了index()方法,使其不使用递归。修改思路:当下标数组的前n-1个都一样时,第n个一定与前n个不一样,这样就保证了下标数组至少有2个不同,即保证了一条式子至少有2中运算符。经过这次栈溢出异常后,我明白了递归要慎用,虽然它简单,但它及可能会拖慢程序速度或使程序崩溃,我也是第一次意识到代码性能分析的重要性。下面是效能分析的图。
六、
Command类测试代码:
import org.junit.Before;import org.junit.Test;public class CommandTest { @Before public void setUp() throws Exception { Command command = new Command(); } @Test public void main() throws Exception { String[] args = {"-n","10","-m","1","999","-o","9","-c","-b"}; Command.main(args); String[] args1 = {"-n","10","-m","1","999","-o","9"}; Command.main(args1); String[] args2 = {"-n","10","-m","1","999"}; Command.main(args2); String[] args3 = {"啦啦啦"}; Command.main(args3); String[] args4 = {"-n","10","-m","110","999","-o","9"}; Command.main(args4); String[] args5 = {"-n","10","-m","1","99999","-o","9"}; Command.main(args5); String[] args6 = {"-n","10","-m","99","60","-o","9"}; Command.main(args6); String[] args7 = {"-n","0","-m","99","60","-o","9"}; Command.main(args7); String[] args8 = {"-n","a","-m","99","60","-o","9"}; Command.main(args8); String[] args9 = {"-n"}; Command.main(args9); String[] args10 = {"-n","3","-m","60","-o","9"}; Command.main(args10); String[] args11 = {"-n","3","-m"}; Command.main(args11); String[] args12 = {"-o","-2","-m"}; Command.main(args12); String[] args13 = {"-o"}; Command.main(args13); String[] args14 = {"-o","b"}; Command.main(args14); String[] args15 = {"-m","1","999","-o","9"}; Command.main(args15); String[] args16 = {"-n","10","-m","1","999","-o","9","-b"}; Command.main(args16); String[] args17 = {"-n","10","-o","9","-c","-b"}; Command.main(args17); }}
import org.junit.Before;import org.junit.Test;import static org.junit.Assert.*;public class CreateTest { private Create create; @Before public void setUp() throws Exception { create = new Create(); } @Test public void generate() throws Exception { create.generate(10,10,100,3,true,true); create.generate(10,100,10,12,true,true); create.generate(10,10,1000,12,true,true); create.generate(10,1,10000,8,true,true); create.generate(10,101,1000,8,true,true); create.generate(0,1,1000,8,true,true); create.generate(10,1,1000,8,true,true); create.generate(10,1,1000,8,false,true); create.generate(10,1,1000,8,true,false); create.generate(10,1,1000,8,false,false); } @Test public void createProblem() throws Exception { create.generate(10,1,1000,8,true,true); create.generate(10,1,1000,8,false,true); create.generate(10,1,1000,8,true,false); create.generate(10,1,1000,8,false,false); } @Test public void index() throws Exception { create.index(3,4); }}
七、计算模块部分异常处理说明
计算模块共需接收的参数有6个,异常都围绕这6个参数展开。
1.关于题目数量n的参数异常有3种:数量范围越界、未输入题目数量和非法字符输入。
case "-n": { try { n = Integer.parseInt(args[i + 1]); if (n < 1 || n > 10000) { System.out.println("对不起,题目数量只能是1-10000!"); return; //结束运行 } } catch (ArrayIndexOutOfBoundsException e) { //未输入数字时args[i+1]会数组越界 System.out.println("未输入题目数量!"); return; }catch (NumberFormatException e) { //输入非数字字符等 System.out.println("对不起,题目数量只允许输入1-10000的数字!"); return; //结束运行 } break; }
单元测试样例:
@Test public void main() throws Exception { String[] args7 = {"-n","0","-m","99","60","-o","9"}; Command.main(args7); String[] args8 = {"-n","a","-m","99","60","-o","9"}; Command.main(args8); String[] args9 = {"-n"}; }
2.关于数值上下界的异常有4种:数值下界大于上界、数值上下界没有在相应规定的范围、未输入数值上下界、非法字符输入。
case "-m": { try { downBound = Integer.parseInt(args[i + 1]); upBound = Integer.parseInt(args[i + 2]); if (downBound >= upBound) { System.out.println("数值下界不能大于等于上界!"); return; } else if (downBound < 1 || downBound > 100 || upBound < 50 || upBound > 1000) { System.out.println("数值下界只能是1-100,数值上界只能是50-1000!"); return; } } catch (ArrayIndexOutOfBoundsException e) { System.out.println("未分别输入数值上下界!"); return; }catch (NumberFormatException e) { System.out.println("对不起,数值上下界只允许数字格式!"); return; } break; }
单元测试样例:
@Test public void main() throws Exception { String[] args6 = {"-n","10","-m","99","60","-o","9"}; Command.main(args6); String[] args11 = {"-n","3","-m"}; Command.main(args11); String[] args4 = {"-n","10","-m","110","999","-o","9"}; Command.main(args4); String[] args18 = {"-n","10","-m","hhh","kkk","-o","9"}; Command.main(args18);
3.关于最大运算符数量的异常3种:不在规定范围、未输入、非法输入
case "-o": { try { largeOperatorCount = Integer.parseInt(args[i + 1]); if (largeOperatorCount < 1 || largeOperatorCount > 10) { System.out.println("对不起,运算符数量只能是1-10!"); return; } } catch (ArrayIndexOutOfBoundsException e) { System.out.println("未输入最大运算符数量!"); return; }catch (NumberFormatException e) { System.out.println("对不起,运算符数量只允许输入1-10的数字!"); return; } break; }
单元测试样例:
@Test public void main() throws Exception { String[] args12 = {"-o","-2","-m"}; Command.main(args12); String[] args13 = {"-o"}; Command.main(args13); String[] args14 = {"-o","b"}; Command.main(args14); }
4.什么相关的参数都没输,只输非法字符等:
if (n == 0 && downBound == 0 && upBound == 0) { System.out.println("参数格式有误!"); return; } else if (n == 0) { System.out.println("未输入题目数量!"); return; } else if (downBound == 0 || upBound == 0) { System.out.println("未分别输入数值上下界!"); return; }
单元测试样例:
@Test public void main() throws Exception { String[] args3 = {"啦啦啦"}; Command.main(args3); }
八、界面模块的详细设计过程
2.注册页面:有判断用户名已存在、两次密码输入不一致的功能。
3.出题大厅界面:用户在此可以设定出题条件。文本框采用了html5的number类型,保证用户只能输入限定范围的数字。
题目数量:数值范围: -
4.点击开始出题,会跳到result.txt文件下载页面:
5.点击下载:
6.下载完后,点击开始做题,进入答题界面:此页面有计时器。
7.答完题后,点击提交,进入成绩反馈界面:会显示答对多少题,答错多少题,所用时长,以及错题卡片(包含错题与正确答案),可供孩子巩固复习。
8.随后点击查看历史答题记录,进入历史答题界面:可以看到刚刚的答题已经呈现在历史列表里。此页面会按时间先后顺序展示以往的答题记录,并会显示历史最佳成绩,还可显示该练习的类型是系统产生还是用户自己上传的。
9.点击其中一条答题记录,查看该次练习的详情。点击橙色下拉按钮可查看具体答题记录(做对和做错的题都显示)。再点击按钮可收起,此效果由javascript实现。
10.点击侧栏“上传题目”,用户可上传自己的题目进行答题:这里有个文件格式限制,若上传正确格式文件,会出现一个开始答题按钮;若上传错误格式的文件开始答题的按钮是不会出现的。此效果是通过javascript识别后台传来el表达式的值实现的。
11.点击开始答题就会进入答题界面,题目内容是用户自己上传的题。界面与上述答题界面一样就不展示啦。
12.修改密码界面:要求输入原密码与新密码。有原密码输入错误判断,有一定安全性。
13.点击退出账号,退回登录页面并显示退出成功。
九、界面模块与计算模块的对接
界面模块与计算模块的对接即jsp与servlet的对接。由于计算模块已经封装好了,servlet只需调用计算模块的Create类产生题目,将答案存于另一个数组中,并将题目置于ArrayList中利用request.setAttribute()传给jsp,在jsp中利用jstl标签<c:foreach>循环列出。用户答完题后通过Form表单将答案传给servlet,servlet用一个数组接收用户答案,再将用户答案数组与正确答案数组对比,即可判断对错。
相关代码如下:
/** * 生成题目 * @param request * @param response * @throws ServletException * @throws IOException */ private void create(HttpServletRequest request,HttpServletResponse response)throws ServletException, IOException { int n = Integer.parseInt(request.getParameter("n")); int downBound = Integer.parseInt(request.getParameter("downBound")); int upBound = Integer.parseInt(request.getParameter("upBound")); int MulDiv = Integer.parseInt(request.getParameter("hasMulDiv")); int Bracket = Integer.parseInt(request.getParameter("hasBracket")); int largeOperatorCount = Integer.parseInt(request.getParameter("largeOperatorCount")); RequestDispatcher rd; if(downBound>=upBound){ request.setAttribute("msg","数值下界不能大于上界!"); rd = request.getRequestDispatcher(WebContents.MAIN); rd.forward(request,response); } boolean hasBracket = false; boolean hasMulDiv = false; if(Bracket==1){ hasBracket=true; } if(MulDiv==1){ hasMulDiv=true; } //将生成的result.txt放在web/file下 Create create = new Create(); String[] result = create.generate(n,downBound,upBound,largeOperatorCount,hasMulDiv,hasBracket); //将题目(不含答案)存入session String[] question = new String[n]; for(int i=0;ilist= readFile.getFileContent(file,n); request.setAttribute("list",list); //将读的文件内容赋给list,给前台遍历 request.setAttribute("n",n); RequestDispatcher rd; rd = request.getRequestDispatcher(WebContents.BEGIN); rd.forward(request,response); } /** * 检查用户答案 * @param request * @param response * @throws ServletException * @throws IOException */ private void check(HttpServletRequest request,HttpServletResponse response)throws ServletException, IOException { String timelong = request.getParameter("timelong"); System.out.println(timelong); int n = Integer.parseInt(request.getParameter("n")); String[] userAnswer = new String[n]; for(int i=0;i feedBackList = new ArrayList<>(); //答错的题+正确答案 for(int i=0;i
由于实现了用户功能,所以此次项目用到了数据库,数据库中有2张表——用户表和练习题表。
整个项目结构如下:
其中entity是实体,dao层负责数据库的连接实现数据的增删改查,servlet负责业务逻辑处理,sql封装了所有用到的sql语句,util是servlet需要用到的的一些工具类,test放着所有的测试类(包括增删改查方法测试、计算模块Create类测试、命令行测试),web的WEB-INF下放着jar包和jsp等。
十、
十一、
优点:我觉得结对编程的优点在于2个人会互相督促,加快项目进度,而且因为都不想拖后腿,所以态度会很积极。当遇到困难时,两个人共同面对会使心理压力小很多,会降低对bug和其他技术困难的畏惧。由于结对编程时每一行代码都由两个人思考过,因此出错和修改代码的几率会小很多,从而提高编程效率。最重要的是,结对编程能够加深两个人的友谊,觉得对方就是与自己共患难的战友。
缺点:结对编程最大的缺点就是两个人的工作量不一致,很可能会出现一人奋斗、一人打酱油的局面。如果在很多小问题上没沟通好两人容易发生矛盾。
我对邓旭的评价:优点是态度很积极,而且很谦虚,会倾听我的建议,总是给人带来正能量,会鼓励支持我。缺点就是这次去参加比赛,没能和我一起敲大部分的代码,不过这也没办法呀,省运会比较重要,我很理解~总之这次与邓旭一起的结对项目还是比较愉快的!
SP2.1 | 任务内容 | 计划共完成需要的时间(h) | 实际完成需要的时间(h) |
Planning | 计划 | 74.5 | 108.5 |
· Estimate | · 估计这个任务需要多少时间,并规划大致工作步骤 | 1 | 1 |
Development | 开发 | 67.5 | 98.5 |
· Analysis | · 需求分析 (包括学习新技术) | 3 | 5 |
· Design Review | · 代码设计 | 4 | 5 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 0.5 | 0.5 |
· Design | · 具体设计 | 3 | 5 |
· Coding | · 在计算模块花费的时间 在UI模块花费的时间 在后台处理模块花费的时间 | 15 10 30 | 20 10 40 |
· Code Review | · 代码复审 | 2 | 3 |
· Test | · 测试(自我测试,修改代码,提交修改) | 2 | 10 |
Reporting | 报告 | 6 | 9 |
· Test Report | · 测试报告 | 5 | 8 |
· Size Measurement | · 计算工作量 | 0.5 | 0.5 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 0.5 | 0.5 |