这个作业属于哪个课程 | 软件工程 |
---|---|
这个作业要求在哪里 | 结对项目 |
这个作业的目标 | 实现一个自动生成小学四则运算题目的命令行程序 |
项目成员
- 车峤锐 3122004471
- 黄梓洋 3122004481
Github 项目地址: 项目链接
2. PSP表格(计划时间)
PSP表格(个人软件过程)
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 20 | 30 |
Development | 开发 | ||
· Analysis | · 需求分析(包括学习新技术) | 40 | 45 |
· Design Spec | · 生成设计文档 | 30 | 35 |
· Design Review | · 设计复审(和同事审核设计文档) | 20 | 25 |
· Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 15 | 15 |
· Design | · 具体设计 | 45 | 60 |
· Coding | · 具体编码 | 120 | 150 |
· Code Review | · 代码复审 | 30 | 35 |
· Test | · 测试(自测,修改代码,提交修改) | 60 | 70 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 20 | 25 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 20 | 25 |
合计 | 430 | 525 |
3. 效能分析
在程序的改进过程中,通过加入性能分析工具 performanceProfiler.js
来跟踪各个关键函数的执行时间,找出性能瓶颈并进行优化。
通过多次分析,确定了 generateExpression
和 evaluateExpression
是两个主要的性能瓶颈,因此对这些函数进行了优化。
主要性能瓶颈
-
表达式的生成:
generateExpression
函数负责生成随机的四则运算表达式。由于需要在生成过程中避免负数、假分数等无效表达式,导致了多次重新生成的操作。
-
表达式的评估:
evaluateExpression
函数在计算表达式的过程中,特别是涉及到分数运算时,耗时较长。旧版本中,表达式的求值和真分数转换逻辑复杂,影响了整体性能。
改进思路
-
性能跟踪:
- 通过
performanceProfiler
工具,在generateExpression
和evaluateExpression
函数的开头和结束处分别加入profiler.start()
和profiler.end()
,跟踪这些函数的执行时间。借助该工具,我们能够了解哪些部分消耗了最多的时间。
- 通过
-
优化表达式生成:
- 改进了
generateExpression
函数,简化了对减法、除法等情况的检查。例如,直接通过交换操作数的方式避免负数结果,而非重新生成表达式。此外,确保分母不为0,减少了无效除法的发生。
- 改进了
-
优化分数计算:
- 优化了
evaluateExpression
中的分数计算和化简逻辑,减少了不必要的分数化简步骤。对于简单的分数和整数运算,通过直接计算而非复杂的通分逻辑来提升速度。
- 优化了
具体改进代码
generateExpression
函数:
function generateExpression(maxOperators, range) {
profiler.start('generateExpression'); // 开始计时
let expr = generateRandomNumberOrFraction(range).toString();
let operatorCount = Math.floor(Math.random() * (maxOperators - 1)) + 2; // 至少一个运算符
for (let i = 0; i < operatorCount; i++) {
let operator = getRandomOperator(range);
let nextNum = generateRandomNumberOrFraction(range).toString();
// 确保减法不会产生负数
if (operator === '-' && (expr instanceof Fraction ? expr.numerator < nextNum : expr < nextNum)) {
[expr, nextNum] = [nextNum, expr];
}
// 确保除数不为0,避免生成无效除法
if (operator === '/' && ((expr instanceof Fraction ? expr.numerator <= nextNum : expr <= nextNum) || nextNum === '0')) {
i--; // 重新生成
continue;
}
// 避免负数结果,去掉预计算步骤
if (evaluateExpression(`(${expr} ${operator} ${nextNum})`) < 0) {
i--;
continue;
}
expr = `(${expr} ${operator} ${nextNum})`;
}
profiler.end('generateExpression'); // 结束计时
return expr;
}
evaluateExpression
函数:
function evaluateExpression(expr) {
profiler.start('evaluateExpression'); // 开始计时
if (expr.includes('/')) {
let result = eval(handleFraction(expr));
if (Number.isInteger(result)) {
profiler.end('evaluateExpression'); // 结束计时
return result;
} else {
const fra = math.evaluate(handleFraction(expr));
const expr1 = `${fra.n}/${fra.d}`;
profiler.end('evaluateExpression'); // 结束计时
return genTFra(expr1);
}
} else {
profiler.end('evaluateExpression'); // 结束计时
return eval(handleFraction(expr));
}
}
性能分析工具
使用 performanceProfiler.js
跟踪各个函数的执行时间:
class PerformanceProfiler {
constructor() {
this.timings = {};
this.startTimes = {};
}
start(label) {
if (!this.timings[label]) {
this.timings[label] = [];
}
this.startTimes[label] = Date.now();
}
end(label) {
const endTime = Date.now();
const startTime = this.startTimes[label];
if (!startTime) return;
const timeTaken = endTime - startTime;
this.timings[label].push(timeTaken);
}
printSummary() {
const summary = Object.entries(this.timings).map(([label, times]) => {
const total = times.reduce((acc, time) => acc + time, 0);
const average = total / times.length;
return { label, total, average };
});
summary.sort((a, b) => b.total - a.total);
console.log('Performance Summary:');
summary.forEach(({ label, total, average }) => {
console.log(`Function: ${label}, Total Time: ${total.toFixed(2)}ms, Average Time: ${average.toFixed(2)}ms`);
});
}
}
module.exports = new PerformanceProfiler();
性能测试与结果
PS E:\vscode_program\3122004481\homework2> node Myapp.js -n 100 -r 100
Performance Summary:
Function: generateProblems, Total Time: 117.00ms, Average Time: 117.00ms
Function: evaluateExpression, Total Time: 111.00ms, Average Time: 0.31ms
Function: generateExpression, Total Time: 92.00ms, Average Time: 0.92ms
性能跟踪结果:
generateProblems
:总执行时间为 117.00ms,问题生成时间为 117.00ms。evaluateExpression
:总执行时间为 111.00ms,平均每次表达式评估时间为 0.31ms。generateExpression
:总执行时间为 92.00ms,平均每次表达式生成时间为 0.92ms。
4. PSP 表格(实际时间)
PSP2.1 | Personal Software Process Stages | 实际耗时(分钟) |
---|---|---|
问题定位与分析 | 问题定位与分析 | 30 |
优化表达式生成逻辑 | 优化表达式生成逻辑 | 40 |
优化分数运算 | 优化分数运算 | 30 |
性能测试与验证 | 性能测试与验证 | 25 |
合计 | 125 |
设计实现过程
在本项目中,目标是开发一个能自动生成小学四则运算题目的程序。该程序的设计围绕着生成随机的四则运算题目、计算答案、并对题目和答案进行验证。所以我们将代码设计成多个模块,并引入性能分析工具来确保代码的运行效率。
代码组织结构
项目包含以下几个主要模块和文件:
-
Myapp.js
:- 这是主程序文件,包含生成随机运算表达式、计算表达式结果、以及保存题目和答案的逻辑。
-
performanceProfiler.js
:- 用于性能分析,跟踪各个关键函数的执行时间,帮助定位性能瓶颈并优化代码。
-
Myapp.test.js
:- 包含测试逻辑,用于验证程序的正确性。
-
package.json
和package-lock.json
:- 包含项目的依赖项配置和版本控制。
主要类与函数
-
Fraction
类:- 用于表示分数,并实现分数的加、减、乘、除以及简化操作。该类还支持将假分数转换为真分数的功能。
主要方法:
constructor(numerator, denominator)
:构造函数,创建分数对象并进行化简。simplify()
:简化分数。toString()
:将分数转换为字符串表示。add()
,subtract()
,multiply()
,divide()
:分别实现分数的四则运算。
-
generateRandomNumberOrFraction()
函数:- 生成一个随机自然数或真分数。根据随机数确定生成的是自然数还是分数。
-
handleCovToProperFraction()
函数:- 将生成的假分数转换为真分数。如果分子大于分母,则将其转换为整数加分数的形式。
-
getRandomOperator()
函数:- 随机生成四则运算符
+
,-
,*
,/
。
- 随机生成四则运算符
-
generateExpression()
函数:- 生成一个包含随机自然数、分数和运算符的算术表达式。该函数通过递归或循环生成包含指定运算符个数的表达式。
- 在生成过程中,对减法和除法进行特殊处理,确保结果为非负数,且除法不会导致除数为零。
-
evaluateExpression()
函数:- 计算生成的表达式结果。该函数会处理分数,并确保运算结果以正确的分数或自然数形式返回。
-
performanceProfiler
:- 用于性能跟踪的类,记录每个函数的执行时间,帮助确定最耗时的部分。
类与函数的关系
-
generateExpression()
函数依赖于以下函数和类:generateRandomNumberOrFraction()
:生成表达式中的随机数或分数。getRandomOperator()
:生成运算符。evaluateExpression()
:计算表达式结果以检测负数或无效表达式。Fraction
类:生成表达式中的分数,并进行分数运算。
-
evaluateExpression()
函数依赖于:mathjs
库和Fraction
类:用于处理分数和算术运算。performanceProfiler
:跟踪表达式评估的执行时间。
流程图
为了清晰展示表达式生成和评估过程,以下是 generateExpression()
和 evaluateExpression()
函数的简化流程图:
代码说明
1. 分数处理类:Fraction
class Fraction {
constructor(numerator, denominator) {
this.numerator = numerator;
this.denominator = denominator;
this.simplify();
}
simplify() {
const gcd = (a, b) => b ? gcd(b, a % b) : a;
const common = gcd(this.numerator, this.denominator);
this.numerator = this.numerator / common;
this.denominator = this.denominator / common;
}
toString() {
if (this.denominator === 1) {
return this.numerator.toString();
}
return `${this.numerator}/${this.denominator}`;
}
static add(f1, f2) {
let num = f1.numerator * f2.denominator + f2.numerator * f1.denominator;
let denom = f1.denominator * f2.denominator;
return new Fraction(num, denom).simplify();
}
static subtract(f1, f2) {
let num = f1.numerator * f2.denominator - f2.numerator * f1.denominator;
let denom = f1.denominator * f2.denominator;
return new Fraction(num, denom).simplify();
}
static multiply(f1, f2) {
let num = f1.numerator * f2.numerator;
let denom = f1.denominator * f2.denominator;
return new Fraction(num, denom).simplify();
}
static divide(f1, f2) {
let num = f1.numerator * f2.denominator;
let denom = f1.denominator * f2.numerator;
return new Fraction(num, denom).simplify();
}
}
说明:
- 功能:该类用于表示分数及其加、减、乘、除的操作。分数对象通过
simplify()
方法进行化简,并且能以字符串形式输出(如"1/2"
)。 - 设计思路:分数运算是生成题目的核心部分。此类提供了分数的标准化操作,确保生成的分数题目始终以最简形式显示。此外,分数的加、减、乘、除操作都得到了有效的支持。
- 注释:构造函数通过传入的分子、分母进行分数初始化,并调用
simplify()
方法进行简化。
2. 生成随机数或分数:generateRandomNumberOrFraction
function generateRandomNumberOrFraction(range) {
let isFraction = Math.random() > 0.5;
if (range === 1) {
isFraction = false;
} // 如果范围为 1,只能生成自然数
if (isFraction) {
let numerator = Math.floor(Math.random() * range) + 1;
let denominator;
do {
denominator = Math.floor(Math.random() * range) + 1;
} while (denominator === numerator);
return handleCovToProperFraction(new Fraction(numerator, denominator));
} else {
return Math.floor(Math.random() * range);
}
}
说明:
- 功能:随机生成一个自然数或真分数。通过
Math.random()
确定是否生成分数,如果是分数则调用Fraction
类。 - 设计思路:为了保证题目的多样性,此函数生成的数值有 50% 的概率是自然数,另 50% 的概率是分数。对于分数,分母和分子不相等,且通过处理将假分数转换为真分数或带分数。
- 注释:当
range
为 1 时,仅生成自然数;否则,根据生成的随机数决定返回自然数或分数。
3. 生成随机表达式:generateExpression
function generateExpression(maxOperators, range) {
profiler.start('generateExpression'); // 开始性能分析计时
let expr = generateRandomNumberOrFraction(range).toString();
let operatorCount = Math.floor(Math.random() * (maxOperators - 1)) + 2; // 至少一个运算符
for (let i = 0; i < operatorCount; i++) {
let operator = getRandomOperator(range);
let nextNum = generateRandomNumberOrFraction(range).toString();
// 确保减法不会产生负数
if (operator === '-' && (expr instanceof Fraction ? expr.numerator < nextNum : expr < nextNum)) {
[expr, nextNum] = [nextNum, expr];
}
// 确保除数不为0,避免无效除法
if (operator === '/' && ((expr instanceof Fraction ? expr.numerator <= nextNum : expr <= nextNum) || nextNum === '0')) {
i--; // 重新生成
continue;
}
// 表达式复杂性预检,防止负数生成
if (evaluateExpression(`(${expr} ${operator} ${nextNum})`) < 0) {
i--; // 重新生成
continue;
}
expr = `(${expr} ${operator} ${nextNum})`;
}
profiler.end('generateExpression'); // 结束计时
return expr;
}
说明:
- 功能:生成随机的四则运算表达式。根据传入的运算符数量和数值范围,递归生成算式,并确保生成的表达式不会出现负数或除以 0 的情况。
- 设计思路:通过循环生成多个操作数和运算符,并依次添加到表达式中。为了防止无效表达式(如负数或除数为零),在生成时进行了相关校验。
- 注释:使用性能分析工具
profiler
来跟踪函数的执行时间,从而优化函数性能。
4. 表达式结果计算:evaluateExpression
function evaluateExpression(expr) {
profiler.start('evaluateExpression'); // 开始计时
if (expr.includes('/')) {
let result = eval(handleFraction(expr));
if (Number.isInteger(result)) {
profiler.end('evaluateExpression'); // 结束计时
return result;
} else {
const fra = math.evaluate(handleFraction(expr));
const expr1 = `${fra.n}/${fra.d}`;
profiler.end('evaluateExpression'); // 结束计时
return genTFra(expr1);
}
} else {
profiler.end('evaluateExpression'); // 结束计时
return eval(handleFraction(expr));
}
}
说明:
- 功能:对生成的表达式进行计算,处理分数和自然数的运算。该函数能够处理包含分数的表达式,并返回正确的结果格式。
- 设计思路:根据表达式是否包含分数,分别处理。使用
mathjs
库计算分数的结果,并返回标准化的分数格式。 - 注释:通过
profiler
跟踪表达式评估时间,以便进行性能优化。
测试代码分析和解释
该测试文件 Myapp.test.js
主要测试了 generateRandomNumberOrFraction
函数的各种情况,确保生成的自然数和分数符合要求。以下是每个测试用例的详细说明:
1. 测试用例:range
为 1 时返回自然数
test('range 为 1 时返回自然数', () => {
const result = generateRandomNumberOrFraction(1);
expect(Number.isInteger(result)).toBe(true);
});
目的:当 range
为 1 时,函数应该返回自然数,不生成分数。
解释:该测试通过 Number.isInteger()
方法检查生成的结果是否为整数,确保在最小范围内只生成自然数。
2. 测试用例:range
大于 1 时返回自然数或真分数
test('range 大于 1 时返回自然数或真分数', () => {
const result = generateRandomNumberOrFraction(10);
if (typeof result === 'number') {
expect(Number.isInteger(result)).toBe(true);
} else {
const [integerPart, numerator, denominator] = result.split(/['/]/).map(Number);
expect(numerator).toBeLessThan(denominator);
}
});
目的:确保当 range
大于 1 时,程序可以生成自然数或真分数。
解释:通过检查返回的结果,如果是数字则验证其为整数,如果是字符串或分数,确保生成的分子小于分母,符合真分数的定义。
3. 测试用例:返回的分数是一个真分数
test('返回的分数是一个真分数', () => {
const result = generateRandomNumberOrFraction(10);
if (result instanceof Fraction) {
expect(result.numerator).toBeLessThan(result.denominator);
}
});
目的:确保生成的分数是一个真分数。
解释:该测试使用 Fraction
类对象来验证生成的分数是否是一个真分数,即分子小于分母。
4. 测试用例:生成的自然数在范围内
test('生成的自然数在范围内', () => {
const range = 10;
const result = generateRandomNumberOrFraction(range);
if (typeof result === 'number') {
expect(result).toBeGreaterThanOrEqual(1);
expect(result).toBeLessThanOrEqual(range);
}
});
目的:确保生成的自然数在指定范围内。
解释:通过检查生成的自然数,确保其不小于 1 且不超过指定的 range
值。
5. 测试用例:生成的分数分子和分母在范围内
test('生成的分数分子和分母在范围内', () => {
const range = 10;
const result = generateRandomNumberOrFraction(range);
if (result instanceof Fraction) {
expect(result.numerator).toBeGreaterThanOrEqual(1);
expect(result.numerator).toBeLessThanOrEqual(range);
expect(result.denominator).toBeGreaterThanOrEqual(1);
expect(result.denominator).toBeLessThanOrEqual(range);
}
});
目的:确保生成的分数的分子和分母都在指定范围内。
解释:通过对生成的分数对象进行检查,确保分子和分母都在 1
到 range
范围内。
6. 测试用例:生成的假分数被正确处理为真分数
test('生成的假分数被正确处理为真分数', () => {
const range = 10;
const result = generateRandomNumberOrFraction(range);
if (typeof result === 'string') {
const [integerPart, numerator, denominator] = result.split(/['/]/).map(Number);
expect(numerator).toBeLessThan(denominator);
}
});
目的:确保生成的假分数能够正确处理为真分数。
解释:验证假分数是否被转换为标准的真分数形式,避免生成形如 3/3
或类似的不合法分数。
7. 测试用例:生成的自然数和分数是正数
test('生成的自然数和分数是正数', () => {
const range = 10;
const result = generateRandomNumberOrFraction(range);
if (typeof result === 'number') {
expect(result).toBeGreaterThan(0);
} else if (result instanceof Fraction) {
expect(result.numerator).toBeGreaterThan(0);
expect(result.denominator).toBeGreaterThan(0);
}
});
目的:确保生成的自然数和分数都是正数。
解释:检查生成的自然数和分数是否都为正数,符合四则运算要求。
8. 测试用例:生成的分数不等于 1
test('生成的分数不等于1', () => {
const range = 10;
const result = generateRandomNumberOrFraction(range);
if (result instanceof Fraction) {
expect(result.numerator).not.toBe(result.denominator);
}
});
目的:确保生成的分数不简化为 1。
解释:验证分数的分子和分母不相等,避免生成无意义的 1/1
分数。
9. 测试用例:生成的自然数不等于 0
test('生成的自然数不等于0', () => {
const range = 10;
const result = generateRandomNumberOrFraction(range);
if (typeof result === 'number') {
expect(result).not.toBe(0);
}
});
目的:确保生成的自然数不为 0。
解释:该测试确保生成的自然数始终大于 0,符合生成四则运算的要求。
10. 测试用例:生成的分数分母不等于 0
test('生成的分数分母不等于0', () => {
const range = 10;
const result = generateRandomNumberOrFraction(range);
if (result instanceof Fraction) {
expect(result.denominator).not.toBe(0);
}
});
目的:确保生成的分数的分母不为 0。
解释:验证生成的分数分母合法,不为零,从而避免除以零的情况。
结论
通过这些测试用例,程序被验证能够生成正确的自然数和分数,且生成的表达式符合四则运算的规则:
- 数字和分数生成都在指定的范围内。
- 假分数能够正确转换为真分数。
- 不会出现无效的零分母或负数。
- 测试用例全面涵盖了程序的边界情况和正常情况,确保生成的四则运算表达式是有效的。
项目小结
在本次四则运算生成器项目中,我们通过结对编程的方式完成了程序的设计、实现、测试和优化。项目的目标是实现一个可以自动生成小学四则运算题目的程序,涵盖自然数、真分数,确保题目的合法性并防止生成无效题目。我们通过多次讨论和协作,顺利完成了项目任务,同时在过程中收获了宝贵的经验。
成败得失
成功之处
-
合理分工,效率提升:
- 我们在项目初期进行了明确的分工,一人负责核心算法的实现,另一人则负责测试和性能优化。这种分工使我们可以专注于各自的部分,并在合并代码时有很好的衔接。
-
测试覆盖全面:
- 项目中我们编写了全面的测试用例,涵盖了自然数、分数、负数、除零等边界条件。通过这些测试,我们确保了生成的题目和答案都是正确且符合规范的。
-
性能优化显著:
- 在项目后期,我们通过引入性能分析工具
profiler
,跟踪了关键函数的执行时间,并针对性能瓶颈进行了优化。最终,我们的程序在生成大量题目时仍能保持较高的效率。
- 在项目后期,我们通过引入性能分析工具
遇到的挑战与解决
-
分数处理的复杂性:
- 在项目的实现过程中,处理分数的逻辑成为了一大挑战,尤其是在假分数和真分数的转换,以及加减乘除的运算过程中容易出错。为了解决这一问题,我们通过创建
Fraction
类来处理分数运算,并且对生成的假分数进行自动转换,确保结果的正确性。
- 在项目的实现过程中,处理分数的逻辑成为了一大挑战,尤其是在假分数和真分数的转换,以及加减乘除的运算过程中容易出错。为了解决这一问题,我们通过创建
-
表达式合法性检查:
- 确保生成的表达式没有负数、除零、非法分数等情况是另一个难点。我们通过在生成表达式时加入合法性检查(如判断减法结果是否为负、除数是否为零等),解决了这一问题。
-
沟通与协调:
- 在项目的前期,我们在沟通上出现了一些问题,例如各自的代码风格不一致,导致代码合并时遇到了些许冲突。我们及时沟通,明确了编码规范,并通过版本控制系统(如 Git)来管理我们的代码修改,顺利解决了这些问题。
经验分享
-
结对编程的优势:
- 结对编程让我们能够在项目的开发过程中相互支持和纠正。当一人遇到问题时,另一人能够及时帮助解决。同时,结对编程还带来了更多的讨论机会,让我们在设计上避免了许多不必要的错误。
-
注重测试的重要性:
- 本项目中,测试驱动开发(TDD)使我们能够及时发现问题并进行修复。在编写完关键功能后,立刻编写相应的测试用例,确保每一部分都能正确运行。这种方式帮助我们减少了在项目后期调试时遇到的困难。
-
性能优化的实践:
- 通过使用性能分析工具,我们了解了如何定位代码的性能瓶颈,并通过简化逻辑和避免重复计算,显著提高了程序的效率。这是我们在编程中的一项重要收获,帮助我们提升了代码质量。
结对感受与建议
- 彼此闪光点:
一个在代码逻辑的优化和性能分析上展现了很强的能力,能够快速定位问题并提出有效的解决方案。
一个比较擅长测试用例的编写和代码的模块化设计。 - 建议与提升:
- 我们在项目初期的沟通还可以更加高效。由于前期讨论不够充分,导致分工不明确,浪费了一些时间。在后续的项目中,可以在一开始就充分讨论并明确任务分配,以提升整体开发效率。