一、测试的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:UserswanghDesktop2016012032partnerWorksrc
(注意根据自己放的位置调整路径,下同)
2.编译command.java和core.jar:
javac -cp C:UserswanghDesktop2016012032partnerWorkwebWEB-INFlibcore.jar -encoding utf-8 C:UserswanghDesktop2016012032partnerWorksrcCommand.java
(-cp表示路径,core.jar包放在web的WEB-INF的lib文件夹下)
3.运行command.java和core.jar,并输入参数:
java -cp ..webWEB-INFlibcore.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类型,保证用户只能输入限定范围的数字。
题目数量:<input placeholder="1-10000" type="number" min="1" max="10000" name="n"/> 数值范围:<input placeholder="1-100" type="number" min="1" max="100" name="downBound"> - <input placeholder="1-100" type="number" min="50" max="1000" name="upBound">
4.点击开始出题,会跳到result.txt文件下载页面:
5.点击下载:
6.下载完后,点击开始做题,进入答题界面:此页面有计时器。
7.答完题后,点击提交,进入成绩反馈界面:会显示答对多少题,答错多少题,所用时长,以及错题卡片(包含错题与正确答案),可供孩子巩固复习。
8.随后点击查看历史答题记录,进入历史答题界面:可以看到刚刚的答题已经呈现在历史列表里。此页面会按时间先后顺序展示以往的答题记录,并会显示历史最佳成绩,还可显示该练习的类型是系统产生还是用户自己上传的。
9.点击其中一条答题记录,查看该次练习的详情。点击橙色下拉按钮可查看具体答题记录(做对和做错的题都显示)。再点击按钮可收起,此效果由javascript实现。
<script language="javascript" type="text/javascript"> function aaa(){ if(document.getElementById("contentTable").style.display == 'none') {document.getElementById("contentTable").style.display='';} else {document.getElementById("contentTable").style.display='none';} } </script>
10.点击侧栏“上传题目”,用户可上传自己的题目进行答题:这里有个文件格式限制,若上传正确格式文件,会出现一个开始答题按钮;若上传错误格式的文件开始答题的按钮是不会出现的。此效果是通过javascript识别后台传来el表达式的值实现的。
<script type="text/javascript"> var msg = "${msg}"; if(msg=='题目上传成功!'){ document.getElementById('begin').style.display="inline-block"; }else { document.getElementById('begin').style.display="none"; } </script>
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;i<n;i++){ int index = result[i].indexOf("="); question[i]=result[i].substring(0,index); } request.getSession().setAttribute("question",question); //将正确答案存入session String[] realAnswer = new String[n]; for(int i=0;i<n;i++){ int index = result[i].indexOf("="); realAnswer[i]=result[i].substring(index+1); } request.getSession().setAttribute("realAnswer",realAnswer); MakeFile2 makeFile2 = new MakeFile2(); String path = request.getSession().getServletContext().getRealPath("file")+"/"; File f = new File(path); //文件夹也是个文件 if(!f.exists()){ f.mkdirs(); //如果该文件夹不存在,则创建该文件夹 } String fileName = UUID.randomUUID()+"result.txt"; File file = makeFile2.creatFile(result,path+fileName); //将该练习存入数据库 String relativePath = "../../../file/"+fileName; User user = (User) request.getSession().getAttribute("user"); int userId = user.getId(); String creatType = "系统产生"; PracticeDao practiceDao = new PracticeDaoImpl(); Practice practice = new Practice(relativePath,creatType,userId); practiceDao.insertPractice(practice); Practice practice1 = practiceDao.selectPracticeByPath(relativePath); request.getSession().setAttribute("practiceId",practice1.getId()); request.setAttribute("filepath",path+fileName); request.getSession().setAttribute("file",file); request.setAttribute("n",n); rd = request.getRequestDispatcher(WebContents.DOWNLOAD); rd.forward(request,response); } /** * 开始答题(返回题目内容) * @param request * @param response * @throws ServletException * @throws IOException */ private void begin(HttpServletRequest request,HttpServletResponse response)throws ServletException, IOException { File file = (File)request.getSession().getAttribute("file"); int n = Integer.parseInt(request.getParameter("n")); ReadFile readFile = new ReadFile(); List<Problem> list= 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<n;i++){ userAnswer[i] = request.getParameter("s"+i); //获得用户答案 } String[] question = (String[]) request.getSession().getAttribute("question"); String[] userContent = new String[n]; for(int i=0;i<n;i++){ userContent[i]=question[i]+"="+userAnswer[i]; } String[] realAnswer = (String[])request.getSession().getAttribute("realAnswer"); int correctCount=0; int wrongCount=0; List<FeedBack> feedBackList = new ArrayList<>(); //答错的题+正确答案 for(int i=0;i<n;i++){ if(!userAnswer[i].equals(realAnswer[i])){ wrongCount++; String a = userContent[i]; String b = realAnswer[i]; FeedBack feedBack = new FeedBack(a,b); feedBackList.add(feedBack); }else correctCount++; } String[] wrongProblem = new String[feedBackList.size()]; String[] correctAnswer = new String[feedBackList.size()]; for(int i=0;i<feedBackList.size();i++){ wrongProblem[i] = feedBackList.get(i).getWrongUserContent(); correctAnswer[i] = feedBackList.get(i).getCorrectAnswer(); } //将此次练习信息存入数据库 int practiceId = (int)request.getSession().getAttribute("practiceId"); Practice practice = new Practice(practiceId,Arrays.toString(wrongProblem),Arrays.toString(correctAnswer),Arrays.toString(userContent),correctCount,wrongCount,timelong); PracticeDao practiceDao = new PracticeDaoImpl(); practiceDao.updatePractice(practice); request.setAttribute("correctCount",correctCount); request.setAttribute("wrongCount",wrongCount); request.setAttribute("timelong",timelong); request.setAttribute("feedBackList",feedBackList); RequestDispatcher rd; rd = request.getRequestDispatcher(WebContents.FEEDBACK); rd.forward(request,response); }
由于实现了用户功能,所以此次项目用到了数据库,数据库中有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 |