结对编程项目-四则运算 第二周 输出整体总结博客
结对对象:
学号 :20162329
姓名 :张旭升
伙伴第二周博客地址:http://www.cnblogs.com/Zhangxusheng/p/6882684.html
- 担任角色:(本周有进行角色互换)
- 驾驶员:张旭升
- 副驾驶:刘伟康
需求分析:
- 自动生成小学四则运算题目(加、减、乘、除)
- 支持整数
- 支持多运算符(比如生成包含100个运算符的题目)(使用栈生成)
- 支持真分数
- 统计正确率
- 扩展需求:
- 文件:
- 处理生成题目并输出到文件
- 完成题目后从文件读入并判题
- 多语言支持:
简体中文
,繁體中文
,English
- 生成题目不能重复(参考:http://www.cnblogs.com/jiel/p/4810756.html)
- 文件:
- 第一周分析:如果要生成10以内的(包括分数在内)四则运算题目、运算符较少则可以不用栈的方法;如果支持多运算符且范围较大则使用栈的方法。对于以后的拓展需求,主要是能够使得使用者明确运算顺序,所以加括号是一个关键;其次,能够根据使用者的输入结果自动判断对错、生成题目不能通过有限次交换变成同一个题目等细节也不能忽视。
- 第二周分析:如果添加括号,要考虑各个运算符的位置以及生成题目中数字的索引、有没有多余的半括号等问题,可以使用
ArrayList
泛型方法解决;使用IO流写入文件则需考虑IO异常抛出的问题,可以用try-catch
解决;实现多语言支持则需要添加合理的条件循环语句,可以用while、if等语句解决;除此之外,实现多次测试,选择的级别和题目数量的输入等细节也需要考虑在内。
设计思路:
UML类图
思路一:
思路二:
思路一:使用条件/循环语句实现
(与第一周的相同)- 优点:设计思路比较简单,不容易出错;
缺点:不支持多级运算,只适合二级运算。
思路二:使用栈实现
(在第一周的基础上扩展需求)- 创建一个分数的计算类(课本中)
- 创建一个后缀表达式的计算类
- 创建一个中缀表达式转后缀表达式的转化类
- 创建一个题目自动生成并判断正误的类
创建测试类
- 有关IO的类:
- 创建一个IO中题目自动生成并判断正误的类
- 创建一个将题目写入文件的测试类
- 创建一个将测试结果写入文件的测试类
- 优点:可进行多级运算,可写入文件、选择语言,并且普遍性更强,出现错误相比第一周更少;
缺点:设计模式复杂,修改一个地方影响较大。
代码实现(关键代码解释)
for (int i = 0; i < A; i++) {
int B = ran.nextInt(2);
int C = ran.nextInt(4);
in1 = IntNumber.obj();
in2 = IntNumber.obj();
score1 = Score.obj();
score2 = Score.obj();
if (B == 0) {
switch (C) {
case 0:
num = in1.add(in2);
...
CorrectJudgment.judgment(N == num,num1);
break;
case 1:
num = in1.subtract(in2);
...
CorrectJudgment.judgment(N == num,num1);
break;
case 2:
num = in1.multiply(in2);
...
CorrectJudgment.judgment(N == num,num1);
break;
case 3:
num1 = in1.divide(score1);
...
CorrectJudgment.judgment(Q.equals(num1),num1);
break;
}
}
else {
switch (C) {
case 0:
num2 = score1.add(score2);
num1 = num2.toString();
...
CorrectJudgment.judgment(Q.equals(num1),num1);
break;
case 1:
num2 = score1.subtract(score2);
num1 = num2.toString();
...
CorrectJudgment.judgment(Q.equals(num1),num1);
break;
case 2:
num2 = score1.multiply(score2);
num1 = num2.toString();
...
CorrectJudgment.judgment(Q.equals(num1),num1);
break;
case 3:
num2 = score1.divide(score2);
num1 = num2.toString();
...
CorrectJudgment.judgment(Q.equals(num1),num1);
break;
}
}
}
以上的代码来自思路一(有删减),第一个关键点是设置了B,C两个变量,并根据输出转到相应的条件语句,尤其注意B,C的返回数值范围;第二个关键点将整数计算与分数计算划分开,并实现随机生成题目;第三点是增加了判断类中方法的调用,从而可以给出正误的输出。
思路二:
关键代码一(和第一周一样)
public class Original {
private Stack<String> stack1;
private List<String> list1;
private String message,Message="";
public Original() {
stack1 = new Stack<String>();
list1 = new ArrayList<String>();
}
public void evaluate(String expr) {
int op1, op2, result = 0;
String token;
StringTokenizer tokenizer = new StringTokenizer(expr);
while (tokenizer.hasMoreTokens()) {
token = tokenizer.nextToken();
if (token.equals("("))
stack1.push(token);
else if (token.equals("+") || token.equals("-")) {
if(!stack1.empty()) {
if (stack1.peek().equals("*") || stack1.peek().equals("/")){
list1.add(stack1.pop());
stack1.push(token);
} else
stack1.push(token);
}
else {
stack1.push(token);
}
}
else if (token.equals("*") || token.equals("/")) {
if(!stack1.empty()){
if(stack1.peek().equals("*")||stack1.peek().equals("/")) {
list1.add(stack1.pop());
}
}
stack1.push(token);
}
else if (token.equals(")")) {
while (true) {
String A = stack1.pop();
if (!A.equals( "("))
list1.add(A);
else break;
}
}else list1.add(token);
}
while (!stack1.empty()) {
list1.add(stack1.pop());
}
ListIterator<String > li = list1.listIterator();
while (li.hasNext()) {
Message += li.next() + " ";
li.remove();
}
message = Message;
}
public String getMessage(){
return message;
}
}
- 这串思路二中的代码实现了中缀转后缀的表达式转换。注意其中的几个关键方法:
hasMoreTokens()
这个方法可以测试此tokenizer
的字符串中是否还有更多的可用标记。如果此方法返回 true,那么后续调用无参数的nextToken
方法将成功地返回一个标记。hasNext()
这是一种遍历栈中每个对象的方法,如果此扫描器的输入中有另一个标记,则返回 true。注意:当且仅当此扫描器有另一个标记时才返回 true 。
思路二:
关键代码二
public void Ti(int number, int many,String language) {
if(language.equalsIgnoreCase("A"))
many = TiclassC(many);
else if(language.equalsIgnoreCase("B"))
many = TiclassE(many);
else many = TiclassF(many);
for (int j = 0; j < number; j++) {
String ti = "";
//开始进入题目生成
for (int i = 0; i < many; i++) {
int A = ran.nextInt(20) + 1;
int D = ran.nextInt(20) + 1;
int B = ran.nextInt(5);
int C = ran.nextInt(5);
RationalNumber si = new RationalNumber(A, D);
//正式进入生成题目
if (parity(i)) {
//判断前面是否有左括号
if (list1.indexOf("( ") == -1)
list1.add(getSym() + " ");
//判断将加的右括号和上一个左括号的距离
else if (list1.size() - list1.lastIndexOf("( ") > 4) {
//判断前面的左括号是否已经有了相对应的右括号了
if (list1.lastIndexOf(") ") - list1.lastIndexOf("( ") < 0 && B == 0) {
list1.add(") ");
list1.add(getSym() + " ");
} else list1.add(getSym() + " ");
} else list1.add(getSym() + " ");
} else if (i == many - 1) {
//循环结束时判断前面是否还有一个没有加右括号的左括号
if (list1.lastIndexOf("( ") - list1.lastIndexOf(") ") > 0) {
if (C == 0) {
list1.add(si.toString() + " ");
list1.add(") ");
} else {
list1.add(A + " ");
list1.add(") ");
}
} else if (C != 0)
list1.add(A + " ");
else list1.add(si.toString() + " ");
} else if (i == 0) {
if (C != 0)
list1.add(A + " ");
else list1.add(si.toString() + " ");
} else if (list1.lastIndexOf(") ") != -1) {
if (list1.lastIndexOf(") ") - list1.lastIndexOf("( ") > 0 && B == 0) {
list1.add("( ");
if (C != 0)
list1.add(A + " ");
else list1.add(si.toString() + " ");
} else if (C != 0)
list1.add(A + " ");
else list1.add(si.toString() + " ");
} else if (list1.indexOf("( ") == -1 && B == 0) {
list1.add("( ");
if (C != 0)
list1.add(A + " ");
else list1.add(si.toString() + " ");
} else if (C != 0)
list1.add(A + " ");
else list1.add(si.toString() + " ");
}
for (String i : list1)
ti += i;
list1.clear();
list.add(ti);
}
}
public String getSym() {
int A = ran.nextInt(4);
switch (A) {
case 0:
sym = "+";
break;
case 1:
sym = "-";
break;
case 2:
sym = "*";
break;
case 3:
sym = "/";
break;
}
return sym;
}
public boolean parity(int num) {
if (num % 2 == 1)
return true;
else
return false;
}
public int TiclassC(int many) {
Scanner scan = new Scanner(System.in);
int A = 1;
while (true) {
try {
if (many > 0) {
for (int i = 0; i < many; i++) {
A += 2;
}
break;
} else throw new Exception();
}
catch (Exception e) {
System.out.println("级别输入错误,请重新输入(要求级别至少为1)");
many = scan.nextInt();
}
}
return A;
}
public int TiclassE(int many) {
Scanner scan = new Scanner(System.in);
int A = 1;
while (true) {
try {
if (many > 0) {
for (int i = 0; i < many; i++) {
A += 2;
}
break;
} else throw new Exception();
} catch (Exception e) {
System.out.println("The level of questions is incorrect, Please re-enter it (at least 1)");
many = scan.nextInt();
}
}
return A;
}
解释:首先在用户输入题目数量
number
和题目等级many
后many
会在Ticlass
方法中转换为相应等级的长度,根据输入的两个参数建立两个循环,具体思路为:生成的题目都是在双数位为数字,但数位为符号,在此基础上,在加数字时可能会在数字的左边加一个“(”,在加符号时会在符号的左边加一个“)”,但在加“)”时需要进行判断:- 1.前面是否存在一个“(”
- 2.前面的“(”后面是否已经有了“)”
- 3.前的“(”距离现在要加的“)”是否存在一定距离(以此保证括号中至少扩入一个两个数的计算)
- 1.前面是否存在一个“(”
- 为解决这个问题,张旭升使用了
ArrayList
中的Indexof
和lastIndexof
方法,将每一个生成的符号或数字都顺序加入到list
中用最后一个“)”位置与最后一个“(”位置做差后所的数的大小来判断和保证括号生成都是一对的。
测试方法(使用Junit测试):
运行过程截图:
思路一:
思路二:
直接生成题目测试:
写入IO并作答测试:
代码托管地址:
遇到的困难及解决方法:
1.在级别重新输入时出现错误,直接退出程序。
解决方法:使用debug调试,发现了错误来源,是在Ticlass类中的循环出了问题。
这个问题比较简单,主要是因为在使用Ticlass中的对象时嵌套了太多条件循环,导致难以分辨哪一个出了问题,所以使用debug是一种很好的方法。
经过调试,我发现在传入变量值0的时候,执行了一遍循环直接返回A了,所以while应该加在try的外层,这样才能确保循环体包含catch后面的语句,从而输出正常。2.如图,在等号后面直接写答案,有些不美观,想要在等号后面加一个空格再写入答案。
解决方法:自己尝试,与队友讨论。
首先,在IO写入题目的文件中不能随意添加括号,不然结果输出会出错误。
之后又尝试了两种修改传入字符串的方法:
尝试一:
由于在判断正误时加上了等号后的空格,所以判断结果全部是错误的。
尝试二:
由于修改了写入文件的题目,导致判断时直接将题目中第一个随机生成的数字当成结果,所以结果仍然错误。
经过一些和队友的讨论后,我们在if的条件中加上了含空格的判断形式,最后成功输出并正确判定结果。3.仍然是一个嵌套循环的问题,以下的外层循环在级别输入错误时,显示重新输入题目数量的错误。
解决方法:与队友讨论,使用debug检查一遍。
由于调用了另一个类当中的变量,其中包括了级别输入错误的循环语句,所以在重新输错一次题目数量或者级别后,弹出的就又会是输入需要题目的数量
,因此只需要修改while为输入需要题目的数量
下的内层循环就好。4.在测试IO写入时,经常在运行后IDEA下方显示`Hot Swap failed xxx :changes to method modifiers not implemented xxx : Operation not supported by VM`。
解决方法:(搜索相关文档)
开始出现这个问题时我并没有太在意,直到反复出现这种类型的语句,语句的大意为:私有属性或者方法在热部署的时候不被VM支持。
我大致了解了一些关于热部署的内容,网上说:热部署就是容器状态在运行的情况下部署或者重新部署整个项目。在这种情况下一般整个内存会清空,重新加载。
虽然没怎么看懂,不过我觉得之所以反复弹出这类问题就源于在热部署一直不被VM支持,而网上的几个提问和资料并没有给出热部署失败的明确的解决方法。
直到在我查看了一篇英文资料之后恍然大悟!
这篇资料中有提到很多class,意思大致是说每进行一次热部署都会重新布置class文件,热部署失败就表明不能更新class。然而,我查看了一下我的class文件集合,之前设置的excluded的文件夹中并没有class文件,于是在环境配置中新建了一个文件夹,并设置为excluded,注意要在路径中设置成绝对路径(我之前克隆新项目时使用的是继承路径,所以热部署失败),之后就可以了。5.在使用另一个类中的变量时,出现调用错误导致程序运行时异常退出。
解决方法:与队友讨论
原来我在一个类中创建了一个公有变量,在另一个类中使用class.类名
方式调用,结果运行异常退出,讨论之后尝试在另一个类中直接定义一个相同类型的变量,并直接在最初创建公有变量的类中增加调用方法时的变量即可运行正常。6.运行程序再次测试时正确率计算错误。
解决方法:与队友讨论
在与张旭升的讨论中,我了解到这是因为测试类中的嵌套循环太多导致在另一个类中调用累积测试从而出现问题,所以需要修改给trues赋值的位置,原来是在定义类型时直接赋值,
现在只需要在不同的语言对应的判断正误类中赋值即可,这样每次调用不会累积,之后运行正常。
对结对的小伙伴做出评价:
我的结对小伙伴本周依然保持着较好的状态,在星期二早晨上课时就已经完成了所有主要需求,不得不说我在代码实现上一直没有跟上他的节奏,在他加完IO写入之后,我仔细查看他的代码,并且进行了多次测试,直到修改后没有什么问题,我就开始实现一些拓展需求,比如说实现多语言选择、给题目数量和级别增加输入错误的循环、多次可选择测试、以及一些输出细节问题的处理,并适当修改了张旭升以前的代码,遇到不懂的代码也及时向张旭升请教,他使用泛型方法将题目写入文件,而且在给题目加括号时花费了很多精力,更值得学习的是:他设计的类封装性很强,访问权限尽可能地小对此我没有做一点修改。不过我对他写的添加括号的代码还有一些疑问。
由于我们觉得第二个版本(ED2)具有更强的拓展性,所以本周是对第二个版本进行加强的,第一个版本(ED1)与第一周一样。在本周适当的角色交换下,我充分了解了自己的不足,以及自己在什么方面需要加强,在什么方面比较擅长。结对编程的意义大概就是让我们互相弥补,共同进步。我会尽量向他靠拢,在代码上赶上他的节奏,不局限于修改。
我们结对项目仍然存在不足之处,唯一没有实现的拓展需求是生成题目不能重复,其次嵌套循环有些多,其余已实现的需求我觉得比较完美。
压力测试:
- 对张旭升的题目进行多运算级数压力测试,结果如下:
- 等级10:0.5秒以下;
等级100:1秒以下;
等级1000:1秒以下;
等级10000: 3秒左右;
等级100000: 4分钟左右;
PSP时间统计:
-
PSP2.1 Personal Software Process Stages 预估耗时(小时) 实际耗时(小时) Planning 计划 1 1 · Estimate · 估计这个任务需要多少时间 1 1 Development 开发 11.5 13.5 · Analysis · 需求分析 (包括学习新技术) 1 3 · Design Spec · 生成设计文档 0.5 0.5 · Design Review · 设计复审 (和同事审核设计文档) 1 0.5 · Coding Standard · 代码规范 (为目前的开发制定合适的规范) 1 0.5 · Design · 具体设计 2 3 · Coding · 具体编码 2 4 · Code Review · 代码复审 2 1 · Test · 测试(自我测试,修改代码,提交修改) 2 1 Reporting 报告 3.5 5.5 · Test Report · 测试报告 2 3 · Size Measurement · 计算工作量 0.5 1 · Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 1 1.5 合计 16 20
感想:
- 这两周的结对编程有利有弊,两个人互相帮助、分工合作可以增加代码的质量,而在代码上我总是在张旭升将框架结构写好之后才做一些添加和修改,大体上我还需要及时赶上他的节奏。另外,在这两周的时间里,我和张旭升在第一周讨论的时间并不多,直到我们开始交换角色时才进行一些讨论,所以作为结对编程的一员,及时与队友交换角色至关重要,以后要经常交换角色才能取得更大的收获。
附加:改进情况
- 根据上周谢涛老师及娄老师的建议,本周我将大部分时间用在了代码实现上,但是语法问题仍然需要练习。我也没有因为环境配置的问题占用太多时间,我会继续保持这种状态并尽快赶上张旭升的进度。