结对项目
| 软件工程 | <网工1934> |
|---|---|
| 作业要求: 1.在文章开头给出两位同学的姓名、学号、Github项目地址。 2.记录下估计将在程序的各个模块的开发上耗费的时间。 3.效能分析,记录你在改进程序性能上花费了多少时间,描述改进的思路,并展示一张性能分析的图。如果可能,展示程序中消耗最大的函数。 4.设计实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图? 5.代码说明。展示出项目关键代码,并解释思路与注释说明。 6.测试运行。共享你对程序进行测试的至少10个测试用例,以及说明为什么你能确定你的程序是正确的。 7.在你实现完程序之后,在PSP表格记录下在程序的各个模块上实际花费的时间。 8.项目小结。结对项目总结成败得失,分享经验,总结教训。 |
<作业要求> |
| 作业的目标: 实现一个自动生成小学四则运算题目的命令行程序,可以控制生成题目的个数和题目中数值的范围,以及支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计。 |
Github链接
郭泽杰 3119005371
黄仁辉 3119005374
一.PSP表格
| *PSP2.1* | *Personal Software Process Stages* | *预估耗时(分钟)* | *实际耗时(分钟)* |
|---|---|---|---|
| Planning | 计划 | ||
| · Estimate | · 估计这个任务需要多少时间 | 1450 | 1600 |
| Development | 开发 | ||
| · Analysis | · 需求分析 (包括学习新技术) | 320 | 300 |
| · Design Spec | · 生成设计文档 | 120 | 100 |
| · Design Review | · 设计复审 | 40 | 20 |
| · Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 40 | 20 |
| · Design | · 具体设计 | 340 | 300 |
| · Coding | · 具体编码 | 380 | 420 |
| · Code Review | · 代码复审 | 100 | 60 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 100 | 280 |
| Reporting | 报告 | ||
| · Test Repor | · 测试报告 | 30 | 50 |
| · Size Measurement | · 计算工作量 | 30 | 20 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 50 | 30 |
| · 合计 | 1210 | 1600 |
二.效能分析
单道题目生成分析,可以看出字符串拼接占用的内存空间最大


一万道题目生成分析


批改功能分析


三.设计实现过程
1.主要类
1) 数值类Type

一个抽象类数值类Type,具体子类为:Ordinary 普通整数,Fraction 分数
public abstract class Type {
/** 数字类型标识 */
protected NumType numType;
/** 真实数值 */
protected double realVal;
/** 获取随机值 */
public abstract void getRandom(int range);
// Constructor, getter and setter...
}
public class Ordinary extends Type{
/** 数值 */
private int value;
public Ordinary() {
this.numType = NumType.ORDINARY;
}
/** 获取随机值 */
@Override
public void getRandom(int range) { ... }
/** 判断相等 */
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
// getter and setter...
}
public class Fraction extends Type {
/** 前面带的数 **/
private int headNum;
/** 分子 不能是0 要比分母小 **/
private int numer;
/** 分母 不能是0 **/
private int denomin;
/** 真实数值 */
private double value;
public Fraction() {
this.numType = NumType.FRACTION;
}
/** 数值生成对应的字符串 */
public String generateStr() { ... }
/** 获取随机值 */
@Override
public void getRandom(int range) { ... }
/** 判断相等 */
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
// getter and setter...
}
2) 题目类Question
public class Question {
/** 数字数量 */
int numsCount;
/** 算式类型 */
int equation;
/** 数字集合 */
private List<Type> nums;
/** 操作符队列 */
private Queue<OP> nops = new LinkedList<>();
/** 题目字符串 */
private StringBuilder qStr = new StringBuilder();
/** 答案 初始化默认为普通整数否则会报空指针错误*/
private Type ans = new Ordinary();
/** 随机对象 */
private Random random = new Random();
/** 数值限定范围 */
private int range;
/** 生成题目 */
public void generateQuestion() { ... }
/** 两数算式计算 */
public Type twoNumEquationCal(Type a, Type b, OP op) { ... }
/** 操作符是否为减号且a < b */
public Boolean isNeedSwap(Type a, Type b, OP op) { ... }
/** 计算 */
public Type cal(Type a, Type b, int code) { ... }
/** 生成2数字算式字符串 */
public void generateEquationStr(Type a, Type b, OP op, Boolean hasBreaket) {..}
/** 获取一个随机类型的数 */
public Type getRandomTypeNum() { ... }
/** 随机生成运算符存放到队列 */
public void getRandomOP() { ... }
public Question(int range) {
this.range = range;
}
// getter and setter...
}
3) 题目容器类QuestionContainer
public class QuestionContainer {
/** 容器id */
private int id;
/** 容器中题目容量 */
private int capcity;
/** 题目字符串 - 答案 */
HashMap<String, Type> question2Ans;
/** 题目输出文件路径 */
private String questionPath;
/** 答案输出文件路径 */
private String ans;
/** 题目数值范围指定 */
private int range;
/**
* 将题目字符串与答案输出保存到指定路径
*/
public void save() { ... }
/**
* 生成指定数目的题目并存入容器
*/
public void loadQuestions() { ... }
public QuestionContainer() {
this.question2Ans = new HashMap<>();
}
public QuestionContainer(int capcity, int range) {
this.question2Ans = new HashMap<>();
this.capcity = capcity;
this.range = range;
}
// getter and setter ...
}
4) 常量枚举类
/**
* 数字类型标识
*/
public enum NumType {
/** 普通的数,例如1, 2, 3.... **/
ORDINARY(1),
/** 真分数, 1/2, 1/3, 1/4, 1'1/2 **/
FRACTION(2);
int code;
NumType(int code) {
this.code = code;
}
// getter and setter...
}
/**
* 操作符
*/
public enum OP {
ADD(1, '+'), SUB(2, '-'), MUL(3, '×'), DIV(4, '÷');
int code;
char sign;
OP(int code, char sign) {
this.code = code;
this.sign = sign;
}
// getter and setter...
}
5) 工具类
/**
* 文件工具类
*/
public class FileUtil {
/** 输出题目题目和答案文件 **/
public static void outputFile(QuestionContainer container){ ... }
/** 输出结果文件 **/
public static void CorrectingOutput(File queFile, File ansFile){
// 读取题目文件和答案文件进行比对
}
}
/**
* 计算类
*/
public class Calculator {
/** 求a和b的最大公约数 **/
public static int f(int a,int b){ ... }
/** 求两个分数运算后的分子 **/
public static int CalculateNumer(Fraction fra1, Fraction fra2, int op){ ... }
/** 求两个分数运算后的分母 **/
public static int CalculateDenomin(Fraction fra1, Fraction fra2){ ... }
/** 求带分数化假分数的分子 **/
public static int CalculateNumer(Fraction fra){ ... }
/** 两个数都是 Ordinary 类型 **/
public static Type calculate(Ordinary ord1, Ordinary ord2, int op){ ... }
/** 第一个数是 Fraction 类型,第二个数是 Ordinary 类型 **/
public static Type calculate(Fraction fra1, Ordinary ord2, int op){ ... }
/** 第一个数是 Ordinary 类型,第二个数是 Fraction 类型 **/
public static Type calculate(Ordinary ord1, Fraction fra2, int op){ ... }
/** 两个数都是 Fraction 类型 **/
public static Type calculate(Fraction fra1, Fraction fra2, int op){ ... }
/** 对带分数进行约分 **/
private static Type getType(Fraction fraction) { ... }
}
6)程序入口类
/**
* 程序入口类
*/
public class Application {
private Scanner sc = new Scanner(System.in);
public static void main(String[] args) {
Application app = new Application();
while(true) {
app.menu();
}
}
/** 主界面函数 */
public void menu() { ... }
/** 生成题目界面函数 */
public void gQuestionMenu() { ... }
/** 批改作业界面函数 */
public void correctMenu() { ... }
}
2.程序流程

四.代码说明
public class QuestionContainer {
/** 生成指定数目的题目并存入容器 */
public void loadQuestions() {
// 直到存满
while(this.question2Ans.size() < this.capcity) {
// 生成一道题目
Question question = new Question(this.range);
question.generateQuestion();
// 获取题目的题目字符串和答案
String qStr = question.getqStr().toString();
Type ans = question.getAns();
// 对于两数算式如 1 + 2 = 与 2 + 1 = 重复情况进行特殊查重处理
Boolean isRepeat = false;
if(qStr.length() == 7) {
char a = qStr.charAt(0), b = qStr.charAt(4);
for (String str : question2Ans.keySet()) {
if(str.length() == 7 && (
(a == str.charAt(0) && b == str.charAt(4)) ||
(b == str.charAt(0) && a == str.charAt(4))
)) {
isRepeat = true;
break;
}
}
}
if(isRepeat) continue;
// 其他的若题目字符串相同则表示重复,在HashMap中会自动覆盖重复的题
this.question2Ans.put(qStr, ans);
}
}
}
public class Question {
/** 生成一道题目 */
public void generateQuestion() {
// 数字数量 随机: 2 3 4
numsCount = random.nextInt(3) + 2;
switch (numsCount) {
case 2 : {
equation = 1;
// 随机生成两个数 a b
Type a = getRandomTypeNum(), b = getRandomTypeNum();
// 随机生成操作符
getRandomOP();
OP op = nops.poll();
// 判断是否需要出现 a - b 且 a < b的情况
if(isNeedSwap(a, b, op)) {
Type temp = a;
a = b;
b = temp;
}
// 生成算式字符串
generateEquationStr(a, b, op, false);
// 计算答案
ans = twoNumEquationCal(a, b, op);
qStr.append(" ").append("=");
break;
}
case 3: {
// 算式2~4号
equation = random.nextInt(3)+2;
// 随机生成三个数 a b c
Type a = getRandomTypeNum(), b = getRandomTypeNum(), c = getRandomTypeNum();
// 随机生成操作符
getRandomOP();
OP firstOP = nops.poll();
OP secondOP = nops.poll();
if(equation == 2 || equation == 3) { // (1 + 2) + 3 或 1 + (2 + 3)
if(isNeedSwap(a, b, firstOP)) {
Type temp = a;
a = b;
b = temp;
}
// 生成算式字符串
generateEquationStr(a, b, firstOP, true);
// 计算中间结果 res
Type res = twoNumEquationCal(a, b, firstOP);
// 判断是否需要出现 res - c 且 res < c 的情况
boolean isSwap = isNeedSwap(res, c, secondOP);
// 判断c的类型进行相应操作
if(c.getNumType().getCode() == NumType.FRACTION.getCode()) {
Fraction cc = (Fraction) c;
if(isSwap) {
// 若需交换则将c与第三个运算符插入到算式的前面
qStr.insert(0, " ").insert(0, secondOP.getSign()).insert(0, " ");
qStr.insert(0, cc.generateStr());
ans = twoNumEquationCal(cc, res, secondOP);
} else {
// 否则将c与第三个运算符插入到算式后面
qStr.append(" ").append(secondOP.getSign()).append(" ");
qStr.append(cc.generateStr());
ans = twoNumEquationCal(res, cc, secondOP);
}
} else {
Ordinary cc = (Ordinary) c;
if(isSwap) {
qStr.insert(0, " ").insert(0, secondOP.getSign()).insert(0, " ");
qStr.insert(0, cc.getValue());
ans = twoNumEquationCal(cc, res, secondOP);
} else {
qStr.append(" ").append(secondOP.getSign()).append(" ");
qStr.append(cc.getValue());
ans = twoNumEquationCal(res, cc, secondOP);
}
}
} else if(equation == 4) { // 1 + 2 + 3
// 优先级判断,({加为1, 减为2} - 1) / 2 后为0,{乘为3, 除为4}运算后为1
if((firstOP.getCode() - 1) / 2 >= (secondOP.getCode() - 1) / 2) {
// 第一个操作符的优先级高
if(isNeedSwap(a, b, firstOP)) {
Type temp = a;
a = b;
b = temp;
}
// 生成算式字符串
generateEquationStr(a, b, firstOP, false);
Type res = twoNumEquationCal(a, b, firstOP);
boolean isSwap = isNeedSwap(res, c, secondOP);
if(c.getNumType().getCode() == NumType.FRACTION.getCode()) {
Fraction cc = (Fraction) c;
if(isSwap) {
qStr.insert(0, " ").insert(0, secondOP.getSign()).insert(0, " ");
qStr.insert(0, cc.generateStr());
ans = twoNumEquationCal(cc, res, secondOP);
} else {
qStr.append(" ").append(secondOP.getSign()).append(" ");
qStr.append(cc.generateStr());
ans = twoNumEquationCal(res, cc, secondOP);
}
} else {
Ordinary cc = (Ordinary) c;
if(isSwap) {
qStr.insert(0, " ").insert(0, secondOP.getSign()).insert(0, " ");
qStr.insert(0, cc.getValue());
ans = twoNumEquationCal(cc, res, secondOP);
} else {
qStr.append(" ").append(secondOP.getSign()).append(" ");
qStr.append(cc.getValue());
ans = twoNumEquationCal(res, cc, secondOP);
}
}
} else {
// 第二个操作符的优先级高
if(isNeedSwap(b, c, secondOP)) {
Type temp = b;
b = c;
c = temp;
}
// 生成算式字符串
generateEquationStr(b, c, secondOP, false);
Type res = twoNumEquationCal(b, c, secondOP);
boolean isSwap = isNeedSwap(a, res, firstOP);
if(a.getNumType().getCode() == NumType.FRACTION.getCode()) {
Fraction aa = (Fraction) a;
if(isSwap) {
qStr.append(" ").append(firstOP.getSign()).append(" ");
qStr.append(aa.generateStr());
ans = twoNumEquationCal(res, a, firstOP);
} else {
qStr.insert(0, " ").insert(0, firstOP.getSign()).insert(0, " ");
qStr.insert(0, aa.generateStr());
ans = twoNumEquationCal(a, res, firstOP);
}
} else {
Ordinary aa = (Ordinary) a;
if(isSwap) {
qStr.append(" ").append(firstOP.getSign()).append(" ");
qStr.append(aa.getValue());
ans = twoNumEquationCal(res, a, firstOP);
} else {
qStr.insert(0, " ").insert(0, firstOP.getSign()).insert(0, " ");
qStr.insert(0, aa.getValue());
ans = twoNumEquationCal(a, res, firstOP);
}
}
}
}
qStr.append(" ").append("=");
break;
}
case 4: {
// 算式5~7号
equation = random.nextInt(3)+5;
// 随机生成四个数
Type a = getRandomTypeNum(), b = getRandomTypeNum(), c = getRandomTypeNum(), d = getRandomTypeNum();
// 随机生成操作符
getRandomOP();
OP firstOP = nops.poll();
OP secondOP = nops.poll();
OP thirdOP = nops.poll();
if(equation == 5 || equation == 6 || equation == 7) {
// 如(1 + 2) + 3 + 4 或 1 + (2 + 3) + 4 或 1 + 2 + 3 + 4
if(isNeedSwap(a, b, firstOP)) {
Type temp = a;
a = b;
b = temp;
};
// 生成算式字符串
generateEquationStr(a, b, firstOP, true);
Type res = twoNumEquationCal(a, b, firstOP);
if((secondOP.getCode() - 1) / 2 >= (thirdOP.getCode() - 1) / 2) {
Boolean isSwap1 = isNeedSwap(res, c, secondOP);
if(c.getNumType().getCode() == NumType.FRACTION.getCode()) {
Fraction cc = (Fraction) c;
if(isSwap1) {
qStr.insert(0, " ").insert(0, secondOP.getSign()).insert(0, " ");
qStr.insert(0, cc.generateStr());
res = twoNumEquationCal(cc, res, secondOP);
} else {
qStr.append(" ").append(secondOP.getSign()).append(" ");
qStr.append(cc.generateStr());
res = twoNumEquationCal(res, cc, secondOP);
}
} else {
Ordinary cc = (Ordinary) c;
if(isSwap1) {
qStr.insert(0, " ").insert(0, secondOP.getSign()).insert(0, " ");
qStr.insert(0, cc.getValue());
res = twoNumEquationCal(cc, res, secondOP);
} else {
qStr.append(" ").append(secondOP.getSign()).append(" ");
qStr.append(cc.getValue());
res = twoNumEquationCal(res, cc, secondOP);
}
}
Boolean isSwap2 = isNeedSwap(res, d, thirdOP);
if(d.getNumType().getCode() == NumType.FRACTION.getCode()) {
Fraction dd = (Fraction) d;
if(isSwap2) {
qStr.insert(0, " ").insert(0, thirdOP.getSign()).insert(0, " ");
qStr.insert(0, dd.generateStr());
ans = twoNumEquationCal(dd, res, thirdOP);
} else {
qStr.append(" ").append(thirdOP.getSign()).append(" ");
qStr.append(dd.generateStr());
ans = twoNumEquationCal(res, dd, thirdOP);
}
} else {
Ordinary dd = (Ordinary) d;
if(isSwap2) {
qStr.insert(0," ").insert(0, thirdOP.getSign()).insert(0, " ");
qStr.insert(0, dd.getValue());
ans = twoNumEquationCal(dd, res, thirdOP);
} else {
qStr.append(" ").append(thirdOP.getSign()).append(" ");
qStr.append(dd.getValue());
ans = twoNumEquationCal(res, dd, thirdOP);
}
}
} else {
if(isNeedSwap(c, d, thirdOP)) {
Type temp = c;
c = d;
d = temp;
};
Type res1 = twoNumEquationCal(c, d, thirdOP);
Boolean needSwap = isNeedSwap(res, res1, secondOP);
if(needSwap) {
String temp = qStr.toString();
qStr = new StringBuilder();
generateEquationStr(c, d, thirdOP, false);
qStr.append(" ").append(secondOP.getSign()).append(" ");
qStr.append(temp);
ans = twoNumEquationCal(res1, res, secondOP);
} else {
qStr.append(" ").append(secondOP.getSign()).append(" ");
// 生成算式字符串
generateEquationStr(c, d, thirdOP, false);
ans = twoNumEquationCal(res, res1, secondOP);
}
}
}
qStr.append(" ").append("=");
break;
}
}
}
}
思路:由于最多只会出现4个数字的算式,则将所有情况(即数字数量,有无括号和括号出现位置)列出并作出具体处理即可(例如符号优先级,计算,字符串生成拼接),除去一个括号可以包3个数和嵌套括号的情况,一共可能生成7种算式。写完代码发现仅仅7种情况,代码冗余度就已经非常高了。
五.运行测试
一万道题目测试




全对情况

全错情况

没做的情况

部分正确







六.项目小结
郭泽杰: 一开始觉得实现一个简单的四则运算生成程序应该十分容易,通过共同讨论,形成了初步的思路,如数字类,题目类的设计,算式可能出现的所有情况列举,列举出来后发现情况个数不多,就没有进一步完善设计思路而直接开始写代码了,直到写生成算式的代码中途时才发现虽然情况少思路简单,但要用代码实现各个字符串的拼接并考虑到括号位置及两数运算是否要交换位置时,代码量很多,容易写到一半就想转换其他思路,在网上看到其他更加巧妙的思路后,代码也已经写到一半了,全部推翻从来也挺折磨人的,所以就硬着头皮把最初的思路给一步步实现出来,中间经历了长时间的debug,写出来的代码冗余度也很高。总结成一句话就是,先做好比较充分的设计,再动手写代码。通过这次作业,我认为结对设计程序的优点,可以让个人专注于自己负责的功能模块,遇到自己debug不出的错误或觉得自己的思路不够清晰时可以互相讨论,快速找出问题以及扩宽思路。
黄仁辉:首先是进行题目生成算法的讨论,随后分配任务由我写计算类和文件输入输出类,计算由于涉及到带分数,需要考虑的情况一共有四种,例如普通整数和带分数运算、两个带分数运算等,实际在写的时候细节把握的不够好,测试用例也不够详尽,debug的时候时常会对代码进行或多或少的修改,如果一开始就考虑到所有特殊情况,后期debug应该会相较来说比较轻松,而且写代码水平也不够,写出来的代码不够优雅。通过这次作业,我觉得两个人一起写一个程序,思路可以更广,程序的漏洞或者可以优化的、自己忽视的地方也可以被同伴指出,或多或少可以学习到结对同伴的长处和发现自己的短处。

浙公网安备 33010602011771号