结对项目
结对项目
这个作业属于哪个课程 | 软件工程 |
---|---|
作业要求 | 结对项目 |
作业目标 | 实现一个自动生成四则运算题目的程序,熟悉结对编程的流程和工作方式 |
GitHub链接 | GitHub |
成员组成 | 3219005445何婉莹+3219005451肖丽萍 |
一、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | 840 | 1010 |
· Analysis | · 需求分析 (包括学习新技术) | 150 | 200 |
· Design Spec | · 生成设计文档 | 50 | 55 |
· Design Review | · 设计复审 | 10 | 5 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 100 | 150 |
· Design | · 具体设计 | 120 | 100 |
· Coding | · 具体编码 | 250 | 350 |
· Code Review | · 代码复审 | 30 | 20 |
· Test | · 测试(自我测试,修改代码,提交修改) | 130 | 130 |
Reporting | 报告 | 95 | 85 |
· Test Repor | · 测试报告 | 50 | 50 |
· Size Measurement | · 计算工作量 | 15 | 15 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 20 |
· 合计 | 965 | 1125 |
二、效能分析
- 性能
- 类的内存占用情况
-
由上图可以得知,主要因为字符处理时,数字和Fraction实体转换的较多,最后的输出和输入多用string的方式,所以char[] 占比较大。
-
亮点:
-
使用HashMap、ArrayList:
HashTable、Vector等使用在多线程的场合,内部使用了同步机制,这个会降低程序的性能。 -
当你要创建一个比较大的hashMap时,充分利用另一个构造函数
public HashMap(int initialCapacity, float loadFactor)避免HashMap多次进行了hash重构。
-
三、接口的设计与实现过程
类名 | 功能 |
---|---|
Generator | 生成器:随机生成符合要求的运算表达式 |
FigureUtil | 数字处理器:判断并处理表达式中的数字类型 |
Calculator | 计算器:利用栈计算并处理运算式的结果 |
Corrector | 批改器:批改答案并输出grade |
- 整体流程图
- util包下类的UML图
四、代码说明
- Map():生成运算表达式
/**
* @param num 生成题目的个数
* @param range 题目中数值(自然数、真分数和真分数分母)的范围
*/
private static Map<String,String> createExp(int num, int range){
//答案-表达式map
Map<String,String> map = new HashMap<>();
//生成表达式
Random random = new Random();
for (int i=0; i<num; ){
StringBuilder sb = new StringBuilder();
//随机生成运算符
int opCount = random.nextInt(3)+1;
String[] ops = new String[opCount];
for (int j=0; j<opCount; j++){
ops[j] = OPERATORS[random.nextInt(4)];
}
//随机生成运算数
int figureCount = opCount + 1;
String[] figures = new String[figureCount];
for (int j=0; j<figureCount; j++){
figures[j] = createFigure(range);
}
//随机决定是否生成括号,若是则决定左括号生成的位置
boolean isBracket = false;
int index = 0;
if (opCount > 1){
isBracket = random.nextBoolean();
}
if (isBracket){
index = random.nextInt(figureCount - 1);
}
//组合表达式
for (int j=0; j<figures.length-1; j++){
if (isBracket && j==index){
sb.append("( ");
}
sb.append(figures[j] + " ");
if (isBracket && j==index+1){
sb.append(") ");
}
sb.append(ops[j] + " ");
}
sb.append(figures[figures.length-1]);
if (isBracket && figures.length-1==index+1){
sb.append(" )");
}
String exp = sb.toString();
//计算结果,检验生成的表达式是否合法,合法的才能存入map中
try {
String result = Calculator.calculate(exp);
if (!result.equals("error") && map.get(result)==null){
map.put(result, exp);
i++;
}
} catch (Exception e) {
e.printStackTrace();
}
}
return map;
}
- calculate():运算表达式求值
/**
* @param infixExp 运算表达式字符串
* @return 计算结果字符串
* @throws Exception
*/
public static String calculate(String infixExp) throws Exception {
String res = "";
Stack<String> numStack = new Stack<>();
//先将中序表示的算式转为后序表达式(以list存放各个运算符和运算数)
List<String> suffixExp = infixToSuffix(infixExp);
//计算
for (String str : suffixExp) {
if (str.matches("[+\\-×÷]")) { //遇到符号则取出前两个数进行计算
String next = numStack.pop();
String pre = numStack.pop();
res = calTwo(pre, next, str);
//当运算过程中出现负数时,中断计算
if (res.equals("error")) {
return res;
}
numStack.push(res);
} else { //遇到数字则入栈
numStack.push(str);
}
}
//将计算结果化至最简
Fraction fraction = FigureUtil.parseFigure(res);
int gcd = ArithmeticUtils.gcd(fraction.getNumerator(), fraction.getDenominator());
fraction.setNumerator(fraction.getNumerator() / gcd);
fraction.setDenominator(fraction.getDenominator() / gcd);
//整理结果:假分数则化为真分数或整数
if (fraction.getNumerator() >= fraction.getDenominator()) {
fraction.setInteger(fraction.getNumerator() / fraction.getDenominator());
fraction.setNumerator(fraction.getNumerator() % fraction.getDenominator());
}
if (fraction.getNumerator() == 0) {
res = fraction.getInteger().toString();
} else {
res = FigureUtil.parseFraction(fraction);
}
return res;
}
- infixToSuffixr():中序表达式转后序表达式
/**
* @param infix 中序表达式字符串
* @return 1条list形式的后序表达式字符串
* @throws Exception
*/
private static List<String> infixToSuffix(String infix) throws Exception {
if (StringUtils.isEmpty(infix.trim())) {
return null;
}
ArrayList<String> suffix = new ArrayList<>(); // 保存后缀表达式
Stack<String> opStack = new Stack<>();
String[] split = infix.split(" ");
for (String str : split) {
if (str.matches("[+\\-×÷\\(\\)]")) {
if (str.equals("(")) { //遇到"("入栈
opStack.push(str);
} else if (str.equals(")")) { //遇到")",出栈直到"("
String topOperator;
while (!(topOperator = opStack.pop()).equals("(")) {
suffix.add(topOperator);
}
} else { //遇到运算符
//出栈,直到栈顶操作符的优先级大于当前操作符的优先级
while (!opStack.isEmpty() && opPriority(str) <= opPriority(opStack.peek())) {
suffix.add(opStack.pop());
}
//当前操作符入栈
opStack.push(str);
}
} else { //遇到数字直接输出
suffix.add(str);
}
}
if (!opStack.isEmpty()) {
while (!opStack.isEmpty()) {
suffix.add(opStack.pop());
}
}
return suffix;
}
五、测试运行
- 使用 -n 参数控制生成题目的个数
- 使用 -r 参数控制题目中数值
- 生成的题目中计算过程不能产生负数,相关代码:
case "-":
if ((a * y - b * x) < 0) {
//计算结果出现非正数时,直接置标志位
return "error";
} else {
result.setNumerator(a * y - b * x);
result.setDenominator(x * y);
}
//当运算过程中出现负数时,中断计算
if (res.equals("error")) {
return res;
}
- 计算过程中有负数怎么办:生成一条算式后马上计算出结果,如果中间的计算过程出现小于0的结果,直接抛弃这条算式生成新的。(计算过程中出现分母为0同理,直接抛弃)
-
生成的题目中如果存在形如e1÷ e2的子表达式,那么*其结果应是真分数*。
-
每道题目中出现的运算符个数不超过3个。
//随机生成运算符
int opCount = random.nextInt(3)+1;
String[] ops = new String[opCount];
for (int j=0; j<opCount; j++){
ops[j] = OPERATORS[random.nextInt(4)];
}
- 程序一次运行生成的题目不能重复。
- 观察到题目能通过有限次交换变成同一个题目时,其计算结果是相同的,根据计算结果查找map,对计算结果相同的算式进行比较,看是否“重复”,决定去留。
- 生成的题目存入执行程序的当前目录下的Exercises.txt文件,在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件。
- 程序应能支持一万道题目的生成。
- 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,统计结果输出到文件Grade.txt
六、单元测试
本次单元测试设置了11个单元测试用例,具体设置如下:
特殊情况测试
- 传入参数个数不正确
- 输入非法参数
- 传入的作答文件路径不存在
/** 传入参数个数不正确 */
@Test
public void test0_1(){
String[] args = {"-n","10"};
Main.main(args);
}
/** 输入非法参数 */
@Test
public void test0_2(){
String[] args = {"-n","10","?","?"};
Main.main(args);
}
/** 传入的作答文件路径不存在 */
@Test
public void test0_3(){
String[] args = new String[4];
args[0] = "-e";
args[1] = PATH + "/a.txt";
args[2] = "-a";
args[3] = PATH + "/b.txt";
Main.main(args);
}
功能测试
- 测试生成题目功能
@Test
public void test1_1(){
String[] args = {"-n","10","-r","20"};
Main.main(args);
}
@Test
public void test1_2(){
String[] args = {"-n","100","-r","20"};
Main.main(args);
}
@Test
public void test1_3(){
String[] args = {"-n","1000","-r","10"};
Main.main(args);
}
@Test
public void test1_4(){
String[] args = {"-n","10000","-r","15"};
Main.main(args);
}
- 测试批改答案功能
@Test
public void test2_1(){
String[] args = new String[4];
args[0] = "-e";
args[1] = PATH + "/src/test/resources/Test1.txt";
args[2] = "-a";
args[3] = PATH + "/src/test/resources/Answers1.txt";
Main.main(args);
}
@Test
public void test2_2(){
String[] args = new String[4];
args[0] = "-e";
args[1] = PATH + "/src/test/resources/Test2.txt";
args[2] = "-a";
args[3] = PATH + "/src/test/resources/Answers2.txt";
Main.main(args);
}
@Test
public void test2_3(){
String[] args = new String[4];
args[0] = "-e";
args[1] = PATH + "/src/test/resources/Test3.txt";
args[2] = "-a";
args[3] = PATH + "/src/test/resources/Answers3.txt";
Main.main(args);
}
@Test
public void test2_4(){
String[] args = new String[4];
args[0] = "-e";
args[1] = PATH + "/src/test/resources/Test4.txt";
args[2] = "-a";
args[3] = PATH + "/src/test/resources/Answers4.txt";
Main.main(args);
}
- 测试覆盖率
七、项目小结
- 何婉莹:
- 此次作业的要求对我而言还是比较有挑战性的,但是在挑战的过程中很好地提升了自己的代码能力,让我意识到了自己容易眼高手低,还是要亲手写代码才能出真知。另外就是提升了自己对项目开发的整体规划能力,在大部分模块都设计好时才开始动手。
- 此次作业需要结对完成,因此这次的开发经历让我注意到应如何与队友进行分工和沟通,在开发过程中也更加注重代码的可读性,学会了如何与队友进行对接。
- 肖丽萍:
- 两人合作时想法更加丰富,能针对问题提出更多元化的解决方案,计划的制定更完善,效率也更加地高,平均的工作量会降低,能互相学习对方的写代码优点,专注自己擅长的部分,弱势部分由队友补足。本次项目的实现整体并不难,但是有些地方要优化的话还是得多花时间精力。由于本学期课程与实验作业过多,本项目暂时在此告一段落,之后有时间的话会继续改进。
- 在此郑重鸣谢队友老何带飞我这个fw。