项目克隆地址:https://git.coding.net/Donsparks/Calculator.git
线上测试地址:http://39.105.6.214:80/hxd_war/
一、前言
开篇之前,我想先介绍一下我的搭档,汪义华(以下以华仔称呼)。在我看来,华仔是我特别中意的一个搭档(至于为什么我将在下文告诉大家)。所以我觉得我们两个的合作,应该可以擦出思想的火花,我期待我们的结对项目的效果是1+1>2的。抱着这样一个信念,我们开始了我们的第一次合作...
二、开始前PSP展示
三、结对编程对接口的设计
1、一些方法的了解:
-
信息隐藏:指在设计和确定模块时,使得一个模块内包含的特定信息(过程或数据),对于不需要这些信息的其他模块来说,是不可访问的。 信息隐藏在设计的所有层次上都有很大作用:从用具名常量代替字面常量,到创建数据类型,再到类的设计、子程序的设计以及子系统的设计等等。
-
接口设计:面向接口编程是我们在编程时常用的一种手段
-
松耦合度:这一概念看起来很抽象,但是我举个例子,就可以很轻松的理解这一概念:
有一百人分成10个团队做开发 你写了一个类A,供其他人调用,怎么办? 简单的方法就是把这个类打成jar包,然后给他们 他们就A a = new A(); 然后调用a的方法。 但是有一天,A类升级了,怎么办? 再打jar包,再给其他9个组每个组发一份,告诉他们,替换一下以前的jar包。 有可能你的a中,方法签名还发生了变化,那么他们就得重新改代码来适应你新的jar包了。 如果这样的事频繁发生呢,那么你就等着挨骂吧。 这就是紧耦合,他们的程序紧密地耦合着你的程序。 如果是松耦合的话,我想我可能会定义一个接口给他们,然后IOC的方式将实现类给他们,最好是远程的通过webservice的方式进行调用,这样我的A更新了,只需要切换掉远程的A的实现类,他们根本啥也不知道,啥也不用改,就更新了功能,怎么样,是不是很方便? 这就是松耦合
2、我们团队在编程过程中对以上方法的体现:
-
在很多类的数据成员的定义上面,我们使用private去定义。这样一来就避免了全局数据可能带来的一些问题,避免了我们把类内数据误认为是全局数据并使用它,减少了很多编程风险。并且,通过对信息隐藏的理解,我发现了信息隐藏的独特的启发力,它能够激发我们设计出有效的方案。并且有助于我们设计类的公开接口。在设计的所有层面上,都可以通过询问该隐藏些什么来促成好的设计决策。 我觉得如果有一天我们能够养成问“我该隐藏些什么?”的习惯,就会惊奇的发现,有很多棘手的设计难题都会迎刃而解。
-
至于接口部分,更是在我们编程过程中充分体现,比如在出题板块GetFomula中,其中运算式的生成和运算式结果的计算都运用了接口,增强了整个程序的可扩展性。
-
耦合度部分,我们也是通过对不同方法的封装,降低了程序的耦合度,尽量保证了整个程序的松耦合。
四、计算模块接口的设计与实现过程
我们的计算模块主要有三部分: Command、GetFomula
-
Command类主要是接受从命令行传来的控制信息,并且能够从中提取出我们需要的参数。并针对一些情况进行了异常处理,反馈给用户合理的提示信息。
- GetFomula类主要是产生算式,并且能够处理从命令行传来的各种参数,其中包括:quesNum(运算式个数),operNum(运算符个数),minNum(运算数范围最小值),maxNum(运算数范围最大值),ifSetArgsC(是否包含乘除法),ifSetArgsB(是否包含括号),从而达到产生不同要求的运算式效果。 并且在产生运算式之后,还可以用getResult()方法对运算式的结果进行运算,这里采用了逆波兰算法,通过将中缀表达式转换为后缀表达式,很好的实现了对于不同类型运算式的一个统一的算法。
五、计算模块接口部分的性能改进
通过一番自主学习,我采用JProfiler对代码进行性能调教和分析:
通过上面的能效分析图,可以看出,代码的性能并非特别完美,还有改进的空间。
六、计算模块部分单元测试展示
这里主要是针对Command类做的单元测试:
Command类代码测试如下:
import org.junit.Test; import java.util.ArrayList; import java.util.List; public class CommandTest { @Test public void main() throws Exception { Command command = new Command(); String[] args1 = {"-m", "1", "100", "-n", "7"};//正确参数类型 String[] args2 = {};//缺少参数 String[] args3 = {"-m", "1", "100"};//缺少部分参数 String[] args4 = {"-m", "1", "10", "-n","10"};//参数范围错误 List<String[]> list = new ArrayList(); list.add(args1); list.add(args2); list.add(args3); list.add(args4); for (int i = 0; i < list.size(); i++) Command.main(list.get(i)); } }
对Command类进行代码覆盖率测试,结果如下:
七、异常处理
异常处理主要体现在参数的合法性和运算结果的正确性上面,包括对生成算式的个数,运算符的个数,运算数的范围等,都做了相应的异常处理:
for(int i=0;i<args.length;i++){
switch(args[i]){
case "-n":{
try{
quesNum=Integer.parseInt(args[++i]);
}catch(Exception ex){
System.out.println("输入的题目数量不符合规定,请输入1-1000之间的整数");
return;
}
if(quesNum<1||quesNum>10000){
System.out.println("输入的题目数量不符合规定,请输入1-1000之间的整数");
return;
}
break;
}
case "-m":{
try{
minNum=Integer.parseInt(args[++i]);
maxNum=Integer.parseInt(args[++i]);
}catch(Exception ex){
System.out.println("输入的运算数范围不合理,请输入整数值");
return;
}
if(minNum<1||maxNum<50||minNum>100||maxNum>1000){
System.out.println("输入的运算数范围不合理,请将下界设置于为1-100之间的整数,上界设置为50-1000的整数");
return;
}
if(maxNum<minNum){
System.out.println("输入的运算数范围不合理,请设置上界参数大于下界参数");
return;
}
break;
}
case "-c":ifSetArgsC=true;break;
case "-o":{
try{
operNum=Integer.parseInt(args[++i]);
}catch(Exception ex){
System.out.println("输入的运算符个数不合理,请输入1-10之间的整数");
return;
}
if(operNum<1||operNum>10){
System.out.println("输入的运算符个数不合理,请输入1-10之间的整数");
return;
}
break;
}
case "-b":ifSetArgsB=true;break;
}
}
同样的,针对这些异常情况,我在测试类中设计了一些测试,从而验证这些异常的处理情况:
八、界面模块的详细设计过程
(一)登陆注册部分:
1、登陆界面:
2、注册界面:
(二)用户使用界面:
1、主页面(生成题目):
2、查看历史界面:
3、上传题目界面:
4、密码修改界面:
代码展示:
1、控制用户登录部分的servlet代码:
@WebServlet( name = "UserServlet" ,urlPatterns = "/user",asyncSupported = true) public class UserServlet extends HttpServlet{ public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //使客户端浏览器,区分不同种类的数据,并根据不同的MIME调用浏览器内不同的程序嵌入模块来处理相应的数据。MIME映射策略就是在网页中使用哪个应用程序(即插件),打开哪种文件。另外还有使用权限问题。 response.setContentType("text/html;charset=UTF-8"); //设置对客户端请求进行重新编码的编码为utf-8 request.setCharacterEncoding("utf-8"); //服务器向客户端反馈的时候需要用流向客户端输出数据 PrintWriter out = response.getWriter(); //获取jsp页面发生的事件 String state = request.getParameter("state"); switch (state){ case "toLogin":{ request.getRequestDispatcher(WebContents.LOGIN).forward(request,response); break; } case "login":{ loginCheck(request,response); break; } case "toRegister":{ request.getRequestDispatcher(WebContents.REGISTER).forward(request,response); break; } case "register": { register(request, response); break; } case "toUpdatePassword": { toUpdatePassword(request, response); break; } case "updatePassword": { updatePassword(request, response); break; }case "exit":{ exit(request,response); break; } } }
2、控制用户做题部分的servlet代码:
WebServlet( name = "PracticeServlet" ,urlPatterns = "/practice",asyncSupported = true) @MultipartConfig public class PracticeServlet extends HttpServlet{ public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //使客户端浏览器,区分不同种类的数据,并根据不同的MIME调用浏览器内不同的程序嵌入模块来处理相应的数据。MIME映射策略就是在网页中使用哪个应用程序(即插件),打开哪种文件。另外还有使用权限问题。 response.setContentType("text/html;charset=UTF-8"); //设置对客户端请求进行重新编码的编码为utf-8 request.setCharacterEncoding("utf-8"); //服务器向客户端反馈的时候需要用流向客户端输出数据 PrintWriter out = response.getWriter(); //获取jsp页面发生的事件 String state = request.getParameter("state"); switch (state){ case "main":{ request.getRequestDispatcher(WebContents.MAIN).forward(request,response); break; } case "create":{ create(request,response); break; } case "check":{ check(request,response); break; } case "download":{ download(request,response); break; } case "begin":{ begin(request,response); break; } case "toUpload":{ request.getRequestDispatcher(WebContents.UPLOAD).forward(request,response); break; } case "upload":{ upload(request,response); break; } case "beginSelf":{ beginSelf(request,response); break; } case "checkSelf":{ checkSelf(request,response); break; } case "history":{ history(request,response); break; } case "onePractice":{ onePractice(request,response); break; } } }
九、界面模块与计算模块的对接
上图是项目的结构,其实也可以说是一个web项目的标准结构。首先介绍一下com文件夹下面的子文件夹:1、dao层:主要是一些接口以及接口的实现类。2、entity(实体层):定义一些属性还有构造方法。3、servlet:主要控制页面之间的跳转。4、sql:用来存放sql语句,方便调用和更改。5、util:主要包含一些工具类,比如生成运算式、计算运算式的逻辑部分,都可以直接在这里调用。
项目中前后端交互采用servlet实现(servlet代码在上面已经展示)。当用户发送一个请求,会进入到相应的servlet方法中,在servlet方法中获取对应的参数,调用接口中的方法,并对客户端做出响应。为了提高代码的安全性,jsp页面放在了web-INF目录下面,因此只有通过servlet才能访问到页面。servlet中包含了上传文件,下载文件,读取文件,生成题目,答题以及各个页面之间跳转的方法。
十、结对过程的描述
1、结对编程的历程:
在和华仔写项目的过程中,我们前期的思路还是很明确的:一开始我想单独把Calculator作一个计算部分,也就是说这里不负责生成算式,而且生成算式本身也不是逆波兰算法的内容, 并且两者掺杂在一起,很容易混乱,所以我在这里单独用逆波兰算法,做了一个功能十分强大的逻辑运算部分,也就是可以运算所有的四则运算表达式(带加减乘除括号的)而且刚刚好,这个四则运算无法判断运算式的合理性,所以我只需要保证产生的四则运算式的合理性,就可以堪称一个完美的逻辑部分。并且,我可以顺便在产生四则运算式子的部分进行一些条件控制,从而使得满足项目的条件要求(这部分在MakeFormula中完成)最后还有一个产生formula的部分,也就是运算式,可以从MakeFormula中传过来。对了,我的所输入的一些命令行语句要能够传到MakeFormula中(这部分在Command中进行)最后的生成文件部分,可以在Command中调用MakeFile来完成。注意,你需要把表达式传到MakeFile中,至于表达式的结果,我觉得可以传到MakeFile中保存起来,为以后UI的设计埋好伏笔。
但是后来中期出现了算法上面的能效问题,就导致我们的项目不能继续进行了。认真思考之后,我觉得不仅仅是算法上的问题吧,很大一部分原因在于我们的项目开发计划的不合理性。或许我们应该不在算法那一块纠结那么久,而是先做一个简易版本,然后把整个项目的框架做出来,至少得到一个像样的成品来。之后再回头去升级我们的算法,岂不是一件完美的事。正如助教说的,一个程序设计的基本原则:Make it run,make it run right,make it run fast.我觉得这是给我和华仔上的最好的一课,并且能够在这样一门课程的一次结对作业中,就能如此深刻的理解这一道理。我觉得对于我们来说是收获极大的,必将成为我们今后在学习,工作中的一笔宝贵财富。
............分界线.............
经过了又一阶段的学习,我们决定放弃之前的GUI,转战web。然而并不是一切都那么顺利,在一次又一次的尝试,一次又一次的失败下。我终于有了一个属于自己的功能较为完善的web项目。当然在这一阶段,我必须要感谢我工作室小伙伴们对我的帮助,以及潜移默化的影响。当然,最最最要感谢的还是美丽的慧珍同学,给予了我最大的帮助。
2、结对编程剪影:
十一、结对编程的优缺点
1、结对优缺点:
优:能够锻炼合作和沟通能力,收获不一样的编程喜悦,这是自己一个人编程所体会不到的。
缺:两个人编程习惯的不同,会导致开发过程中的一些阻碍和矛盾。
2、我和我的搭档:
华仔:
优点:算法能力强;思考问题考虑周全;想法独到有见解
缺点:做事有些急躁
我:
优点:乐观积极,不怕困难;沟通能力强;编程思路清晰
缺点:做事不细心