集大软件工程15级结对编程week1

0. 团队成员

孙志威20152112307AgtEurekaaa
孙慧君201521123098野原泽君野原泽君

1. 需求分析:针对现有代码的改进分析,新开发功能的分析。

1. 题目需求:

  • 原题要求:

    • 写一个能自动生成小学四则运算题目的命令行 “软件”:

      • 除了整数以外,还要支持真分数的四则运算,真分数的运算,例如:1/6 + 1/8 = 7/24;
      • 运算符为 +, −, ×, ÷;
      • 并且要求能处理用户的输入,并判断对错,打分统计正确率
      • 要求能处理用户输入的真分数, 如 1/2, 5/12 等;
      • 使用 -n 参数控制生成题目的个数,例如执行下面命令将生成10个题目 :Myapp.exe -n 10。
    • 把这个程序做成GUI:
      • 记录用户的对错总数,程序退出再启动的时候,能把以前的对错数量保存并在此基础上增量计算;
      • 有计时功能,能显示用户开始答题后的消耗时间;
      • 界面支持中文简体/中文繁体/英语,用户可以选择一种。
    • 两个任务:
      • 把计算模块提取出来,单独创建一个类。
      • 针对提取出来的计算类的接口函数做单元测试。
  • 功能改进与扩展:
    • 增加括号操作符;
    • 减少重复题目;
    • 增加一个运算符(乘方):用符号 ^ 表示乘方,例如:4 ^ 2=16;
    • 回归测试:在开发新功能时避免损坏旧的功能,以确保新的功能不与原有功能冲突,在确认修改的功能正确之后再嵌入代码;
    • 效能分析。

2. 代码分析

  • 获取代码

  • 分析结构
    • 类之间关系

      比较码云的链接中给出的代码只有两个类:

      TrivialCalculatorTrivialCalculatorTest

    • 类图

      集大软件工程15级结对编程week1-LMLPHP

    • 逻辑分析:该项目一共只有4个函数,分别为

      • gcd 求出最大公约数
      • cal 实现两个数字的加减乘除计算
      • zfcal 实现两个分数的加减乘除
      • calinput 处理输入的字符串

        基本上都是逻辑清晰的分支结构,所以没有“逻辑泥球”
    • 测试用例

      集大软件工程15级结对编程week1-LMLPHP集大软件工程15级结对编程week1-LMLPHP

      测试用例完整覆盖了所有函数的各个分支

    • 分析现有代码需要改进的地方:

      1. 没有实现真分数和整数的同时存在:使用随机实现真分数和整数的可能同时出现。
      2. 没有实现表达式中数字个数的随机:利用宏定义统一设置表达式数字个数的最多个数,从2到设置的最多位数随机取一个数,决定表达式的数字长度。

        【表达式的生成利用循环“随机符号+随机数字”,最终去除第一位符号来得到】
      3. 码云里的代码不能运行;
      4. GUI界面有待改善;
      5. 代码可读性不强,缺少相应的注释。
      • 新开功能的分析:
      1. 添加括号:

        • 表达式中数字个数大于2时,可随机添加括号;
        • 左括号的添加位置可以为表达式前,或各个运算符的后面(除了最后一个运算符),右括号的添加位置可以为表达式的最后,或与左括号相隔一个符号开始到表达式最后的每个运算符的左边。
      2. 减少重复题目:
        • 将表达式分解为多个“符号+数字”组合的数组,第一位数字前符号为“+”;
        • 对组合数组进行排序,并放入二叉树,左子节点为该父节点组合在数组中的下一位,右子节点为在数组中拥有相同上几位组合的同位的不同组合;
        • 每生成一个表达式就v在该二叉树内检索插入,已存在则剔除重新生成。(直说好难理解,下面会用图解释)
      3. 添加乘方:
        • 为了减小使用者和计算机内部的计算量,将最多只添加一个二次幂或三次幂;
        • 遍历表达式字符串,得到运算符的位置,并在这些位置和表达式的末尾中随机抽取一个位置,在其右边添加"^2"或 "^3"。

2. 程序设计:针对新开发功能做设计,建议使用思维导图。

  • 程序总体设计

    集大软件工程15级结对编程week1-LMLPHP

  • 所使用的二叉树算法介绍

    集大软件工程15级结对编程week1-LMLPHP

集大软件工程15级结对编程week1-LMLPHP

  • 程序主要部分介绍(C++,C++/QT,Python,C#)

    • 生成表达式(C++):采用随机产生字符拼接成字符串的方式生成
    • 加工表达式(C++)
      • 对生成的表达式进行合法性检查
      • 在合理位置添加括号、 幂运算等操作
      • 将表达式分割为以“符号+数字”的更小元素
      • 将表达式以“元素”为单元进行统一排序
    • 避免表达式广义重复(C++)
      • 构建了一个二叉树结构用来解析排序后的表达式是否重复
      • 二叉树将添加并维护存在过的所有表达式
    • 解析表达式(python)
      • 使用C++调用python接口
      • 使用python的eval函数动态解析表达式

        所以就不用正常的中缀-后缀 + 栈解析的做法了(py大法好)
    • 前端显示(C++/QT)
      • 用QT简单的做了个显示表达式、能检测输入框答案是否正确的GroupBox
      • 然后运行时在MainWindows里new几个Box就酱
    • 单元测试部分(C#)
      • 单元测试用的是MS测试框架
      • 测试项目不需要与正常代码放在同一项目下,‘引用’功能和测试资源管理器都十分好用
  • 各新增功能演示

    • 支持括号

      集大软件工程15级结对编程week1-LMLPHP

    • 表达式不重复

      表达式重复的几率为0,具体算法见二叉树算法处

    • 集大软件工程15级结对编程week1-LMLPHP

      集大软件工程15级结对编程week1-LMLPHP

    • 乘方运算

      集大软件工程15级结对编程week1-LMLPHP

  • 测试分析结果

    • 回归测试:

      测试方法大纲

      集大软件工程15级结对编程week1-LMLPHP

      部分测试用例

      集大软件工程15级结对编程week1-LMLPHP

    • 代码覆盖率测试

      集大软件工程15级结对编程week1-LMLPHP

    • 效能分析

      集大软件工程15级结对编程week1-LMLPHP

    • 消耗最大模块

      • 发现占用绝大部分时间的都不是用户的函数

        基本都是系统调用
      • 但是在用户代码中有一个 Evaluator::initPython方法被调用了124次,实际上这里是可以优化的
      • 优化建议:将Evaluator的一些需要全局经常调用的方法设置为static的类方法,一旦init后就不需要再调用了,可以大大减少该函数的调用次数

3. 代码展示:展示每个功能的核心代码。

  • 字符串生成类

    class ExpGenerator
    {
    public:
    ExpGenerator();
    ~ExpGenerator();
    int getGcd(int x, int y);
    string generateRawExp();
    vector<int> findOperator(string exp);
    string addBracket(string exp);
    string addPower(string exp);
    vector<string> apartExp(string exp);
    vector<string> sortExp(vector<string> items);
    vector<string> generate(int amount,
    vector<double>&results);
    };
    • 生成字符串
      string ExpGenerator::generateRawExp()
      {
      //产生初始表达式
      string exp = "";
      char ch[5] = { '+','-','*','~','^' };
      int maxNum; if (NUMBER < 2)
      {
      //表达式位数小于2则报错
      exp = "ERROR!";
      return exp;
      }
      else if (NUMBER == 2)
      maxNum = 2;
      else
      maxNum = rand() % NUMBER + 2; for (int i = 0; i < maxNum; i++)
      {
      //以(符号+数字)*maxNum形成第一位为符号的“伪表达式” //随机生成符号
      char c = ch[rand() % 4];
      exp.push_back(c); //随机生成数字
      stringstream ss;
      string s;
      int flag = rand() % 10;
      if (flag != 0)
      //生成整数(90%)
      ss << rand() % MAX + 1;
      else
      {
      //生成分数(10%)
      int mumNum = rand() % MAX + 2;
      int sonNum = rand() % (mumNum - 1) + 1;
      sonNum /= getGcd(mumNum, sonNum);
      mumNum /= getGcd(mumNum, sonNum);
      ss << sonNum << "/" << mumNum;
      }
      ss >> s;
      exp += s;
      } //截去第一位符号,生成初始表达式
      exp = exp.substr(1, exp.length());
      ////cout << exp << endl;
      return exp;
      }
    • 拆分字符串并排序
      vector<string>ExpGenerator::apartExp(string exp)
      {
      //遍历初始表达式,以“符号+数字”的结构分割返回
      vector<string> items;
      int begin = 0;
      int end;
      unsigned int i;
      string str;
      exp = "+" + exp;
      char c;
      for (i = 0; i < exp.length(); i++)
      {
      c = exp.at(i);
      if ((c == '+' || c == '-' || c == '*' || c == '~')
      && (i != 0))
      {
      end = i;
      str = exp.substr(begin, end - begin);
      items.push_back(str);
      begin = i;
      }
      }
      str = exp.substr(begin, exp.length() - begin);
      items.push_back(str); return items;
      } vector<string> ExpGenerator::sortExp(vector<string> items)
      {
      //将分割后的表达式项进行排序,用于二叉树的形成
      sort(items.begin(), items.end());
      return items;
      }
  • 二叉树相关

    class UniqueBinaryTree
    {
    public:
    UniqueBinaryTree();
    ~UniqueBinaryTree(); // check if the tree contains the expression
    // if contains return true
    // else return false
    // and add the unique new nodes to the tree
    bool add(vector<string>expressionVec, double answer);
    // clear the tree but not destoryed it
    void clear();
    // get amount of the nodes
    int nodeAmount();
    // get amount of all the resultsNodes
    int resultsAmount();
    // amount of both result node and the mormal node
    int amount();
    // Debug: show contents to the console
    void showLayer();
    private:
    // add the result node to the last node (levelNode)
    // return true if new result added
    // which represent that the new expression
    // has the same expression(without brackets) but
    // different answers
    bool mountAnswerToNode(PNode levelNode, double answer);
    int countAnswerNode(PNode levelNode);
    void clearAnswerNode(PNode levelNode); Node * head = NULL;
    };
    • add函数
      bool UniqueBinaryTree::add(vector<string> expressionVec , double answer)
      {
      // treat empty string as true/contains and ignore it
      if (expressionVec.empty())
      return true; int length = expressionVec.size();
      PNode curNode = this->head->right; // compare each node from the first
      PNode fatherNode = this->head;
      string currentStr; int index = 0; // the current index of stringVector
      bool isContained = false;
      while (curNode != NULL)
      { currentStr = expressionVec.at(index);
      // record the father node to support the insertion later
      fatherNode = curNode;
      if (curNode->data == currentStr)
      {
      curNode = curNode->left;
      // jump to the next string
      index++;
      // break when the whole expression finish
      if (index >= length)
      break;
      }
      else
      {
      curNode = curNode->right;
      }
      }
      //if not then it's an new expression
      // mount it to the tree
      // begin from the last part they're same
      // the current node is the last node matches part of the new expression
      bool first = true;
      // if it reached the end of the expression in the last loop
      // will skip it
      while (index < length)
      {
      string curStr = expressionVec.at(index); PNode newNode = new Node();
      if (first)
      {
      // mount the first newNode as the rightChild
      fatherNode->right = newNode;
      first = false;
      }
      else
      {
      fatherNode->left = newNode;
      } newNode->data = curStr;
      newNode->left = NULL;
      newNode->right = NULL;
      newNode->answer = NULL;
      fatherNode = newNode;
      index++;
      }
      // now the new expression has been added to the tree // add the answer of the expression as well
      // and check if the answer is inside the tree
      // if it's inside it return false as well
      PNode lastNode = fatherNode;
      bool newAnswerAdded = this->mountAnswerToNode(lastNode,answer);
      // new answer node is unique , and been added to the tree
      return !newAnswerAdded;
      }
  • 具体代码见码云Agt-TrivialCalculator


4. 程序运行:程序运行及每个功能的使用截图。

  • 以下图片分别演示了

    1. 随机生成字符串
    2. 表达式的加减乘除
    3. 表达式的括号运算符以及幂运算
    4. 用户的输入和结果正确反馈
    5. 表达式的不重复
    6. 表达式的正确解析
    7. etc...

      集大软件工程15级结对编程week1-LMLPHP

5. 结对编程记录

集大软件工程15级结对编程week1-LMLPHP

  • 码云记录

    集大软件工程15级结对编程week1-LMLPHP

  • 编码规范文档

    集大软件工程15级结对编程week1-LMLPHP

  • PSP表格

Planning计划2030
Estimate明确需求和其他相关因素,估计每个阶段的时间成本1030
Development开发413940
Analysis需求分析 (包括学习新技术)2060
Design Spec生成设计文档6060
Design Review设计复审6030
Coding Standard代码规范310
Design具体设计3060
Coding具体编码120400
Code Review代码复审60200
Test测试(自我测试,修改代码,提交修改60120
Reporting报告60120
·测试报告1020
·计算工作量3030
·并提出过程改进计划6060

6. 小结感受:结对编程真的能够带来1+1>2的效果吗?通过这次结对编程,请谈谈你的感受和体会。

我认为1+2是大于2的,结对编程还是有很多收获的。

例如在考虑问题的解决方案的时候,一个人自己思考的时候很容易被自己过去的经验所影响,容易被自己的思维定势束缚住。而在结对编程的时候,由于一边写一边审,互相之间会交流为什么用这种算法,算法有什么优缺点等,能更好地找到更佳的解决方案,所以结对编程也还是有好处的。

队友的感想见博客野原泽君

05-08 08:13