ccqr

导航

结对项目

这个作业属于哪个课程 软件工程
这个作业要求在哪里 结对项目
这个作业的目标 实现一个自动生成小学四则运算题目的命令行程序

项目成员

  • 车峤锐 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 来跟踪各个关键函数的执行时间,找出性能瓶颈并进行优化。
通过多次分析,确定了 generateExpressionevaluateExpression 是两个主要的性能瓶颈,因此对这些函数进行了优化。

主要性能瓶颈

  1. 表达式的生成

    • generateExpression 函数负责生成随机的四则运算表达式。由于需要在生成过程中避免负数、假分数等无效表达式,导致了多次重新生成的操作。
  2. 表达式的评估

    • evaluateExpression 函数在计算表达式的过程中,特别是涉及到分数运算时,耗时较长。旧版本中,表达式的求值和真分数转换逻辑复杂,影响了整体性能。

改进思路

  1. 性能跟踪

    • 通过 performanceProfiler 工具,在 generateExpressionevaluateExpression 函数的开头和结束处分别加入 profiler.start()profiler.end(),跟踪这些函数的执行时间。借助该工具,我们能够了解哪些部分消耗了最多的时间。
  2. 优化表达式生成

    • 改进了 generateExpression 函数,简化了对减法、除法等情况的检查。例如,直接通过交换操作数的方式避免负数结果,而非重新生成表达式。此外,确保分母不为0,减少了无效除法的发生。
  3. 优化分数计算

    • 优化了 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

设计实现过程

在本项目中,目标是开发一个能自动生成小学四则运算题目的程序。该程序的设计围绕着生成随机的四则运算题目、计算答案、并对题目和答案进行验证。所以我们将代码设计成多个模块,并引入性能分析工具来确保代码的运行效率。


代码组织结构

项目包含以下几个主要模块和文件:

  1. Myapp.js

    • 这是主程序文件,包含生成随机运算表达式、计算表达式结果、以及保存题目和答案的逻辑。
  2. performanceProfiler.js

    • 用于性能分析,跟踪各个关键函数的执行时间,帮助定位性能瓶颈并优化代码。
  3. Myapp.test.js

    • 包含测试逻辑,用于验证程序的正确性。
  4. package.jsonpackage-lock.json

    • 包含项目的依赖项配置和版本控制。

主要类与函数

  1. Fraction

    • 用于表示分数,并实现分数的加、减、乘、除以及简化操作。该类还支持将假分数转换为真分数的功能。

    主要方法

    • constructor(numerator, denominator):构造函数,创建分数对象并进行化简。
    • simplify():简化分数。
    • toString():将分数转换为字符串表示。
    • add(), subtract(), multiply(), divide():分别实现分数的四则运算。
  2. generateRandomNumberOrFraction() 函数

    • 生成一个随机自然数或真分数。根据随机数确定生成的是自然数还是分数。
  3. handleCovToProperFraction() 函数

    • 将生成的假分数转换为真分数。如果分子大于分母,则将其转换为整数加分数的形式。
  4. getRandomOperator() 函数

    • 随机生成四则运算符 +, -, *, /
  5. generateExpression() 函数

    • 生成一个包含随机自然数、分数和运算符的算术表达式。该函数通过递归或循环生成包含指定运算符个数的表达式。
    • 在生成过程中,对减法和除法进行特殊处理,确保结果为非负数,且除法不会导致除数为零。
  6. evaluateExpression() 函数

    • 计算生成的表达式结果。该函数会处理分数,并确保运算结果以正确的分数或自然数形式返回。
  7. performanceProfiler

    • 用于性能跟踪的类,记录每个函数的执行时间,帮助确定最耗时的部分。

类与函数的关系

  1. generateExpression() 函数依赖于以下函数和类

    • generateRandomNumberOrFraction():生成表达式中的随机数或分数。
    • getRandomOperator():生成运算符。
    • evaluateExpression():计算表达式结果以检测负数或无效表达式。
    • Fraction 类:生成表达式中的分数,并进行分数运算。
  2. 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);
    }
});

目的:确保生成的分数的分子和分母都在指定范围内。
解释:通过对生成的分数对象进行检查,确保分子和分母都在 1range 范围内。


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。
解释:验证生成的分数分母合法,不为零,从而避免除以零的情况。


结论

通过这些测试用例,程序被验证能够生成正确的自然数和分数,且生成的表达式符合四则运算的规则:

  • 数字和分数生成都在指定的范围内。
  • 假分数能够正确转换为真分数。
  • 不会出现无效的零分母或负数。
  • 测试用例全面涵盖了程序的边界情况和正常情况,确保生成的四则运算表达式是有效的。

项目小结

在本次四则运算生成器项目中,我们通过结对编程的方式完成了程序的设计、实现、测试和优化。项目的目标是实现一个可以自动生成小学四则运算题目的程序,涵盖自然数、真分数,确保题目的合法性并防止生成无效题目。我们通过多次讨论和协作,顺利完成了项目任务,同时在过程中收获了宝贵的经验。


成败得失

成功之处

  1. 合理分工,效率提升

    • 我们在项目初期进行了明确的分工,一人负责核心算法的实现,另一人则负责测试和性能优化。这种分工使我们可以专注于各自的部分,并在合并代码时有很好的衔接。
  2. 测试覆盖全面

    • 项目中我们编写了全面的测试用例,涵盖了自然数、分数、负数、除零等边界条件。通过这些测试,我们确保了生成的题目和答案都是正确且符合规范的。
  3. 性能优化显著

    • 在项目后期,我们通过引入性能分析工具 profiler,跟踪了关键函数的执行时间,并针对性能瓶颈进行了优化。最终,我们的程序在生成大量题目时仍能保持较高的效率。

遇到的挑战与解决

  1. 分数处理的复杂性

    • 在项目的实现过程中,处理分数的逻辑成为了一大挑战,尤其是在假分数和真分数的转换,以及加减乘除的运算过程中容易出错。为了解决这一问题,我们通过创建 Fraction 类来处理分数运算,并且对生成的假分数进行自动转换,确保结果的正确性。
  2. 表达式合法性检查

    • 确保生成的表达式没有负数、除零、非法分数等情况是另一个难点。我们通过在生成表达式时加入合法性检查(如判断减法结果是否为负、除数是否为零等),解决了这一问题。
  3. 沟通与协调

    • 在项目的前期,我们在沟通上出现了一些问题,例如各自的代码风格不一致,导致代码合并时遇到了些许冲突。我们及时沟通,明确了编码规范,并通过版本控制系统(如 Git)来管理我们的代码修改,顺利解决了这些问题。

经验分享

  1. 结对编程的优势

    • 结对编程让我们能够在项目的开发过程中相互支持和纠正。当一人遇到问题时,另一人能够及时帮助解决。同时,结对编程还带来了更多的讨论机会,让我们在设计上避免了许多不必要的错误。
  2. 注重测试的重要性

    • 本项目中,测试驱动开发(TDD)使我们能够及时发现问题并进行修复。在编写完关键功能后,立刻编写相应的测试用例,确保每一部分都能正确运行。这种方式帮助我们减少了在项目后期调试时遇到的困难。
  3. 性能优化的实践

    • 通过使用性能分析工具,我们了解了如何定位代码的性能瓶颈,并通过简化逻辑和避免重复计算,显著提高了程序的效率。这是我们在编程中的一项重要收获,帮助我们提升了代码质量。

结对感受与建议

  • 彼此闪光点
    一个在代码逻辑的优化和性能分析上展现了很强的能力,能够快速定位问题并提出有效的解决方案。
    一个比较擅长测试用例的编写和代码的模块化设计。
  • 建议与提升
    • 我们在项目初期的沟通还可以更加高效。由于前期讨论不够充分,导致分工不明确,浪费了一些时间。在后续的项目中,可以在一开始就充分讨论并明确任务分配,以提升整体开发效率。

posted on 2024-09-28 17:57  xc1l  阅读(12)  评论(0编辑  收藏  举报