结对编程作业——四则运算
PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 10 |
· Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | 1495 | 1540 |
· Analysis | · 需求分析 (包括学习新技术) | 2h * 60 | 3h * 60 |
· Design Spec | · 生成设计文档 | 30 | 20 |
· Design Review | · 设计复审 (和同事审核设计文档) | 5 | 5 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 5 |
· Design | · 具体设计 | 30 | 30 |
· Coding | · 具体编码 | 12h * 60 | 10h * 60 |
· Code Review | · 代码复审 | 30 | 40 |
· Test | · 测试(自我测试,修改代码,提交修改) | 10h * 60 | 11h * 60 |
Reporting | 报告 | 70 | 60 |
· Test Report | · 测试报告 | 30 | 30 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 20 |
合计 | 1555 | 1620 |
项目要求
1.Calc()
2.Setting()
3.Generate()
编程思路
首先,考虑到Calc是一个相对独立的功能,首先实现了该函数,主要思路是栈操作实现四则的后缀计算。随后考虑Generate函数,随机生成表达式实现方式是随机生成操作数和运算符,然后在一定限制下随机插入括号,主要难度在于判断题目不能重复,讨论之后决定使用表达式二叉树,验证是否重复的操作就变成了交换左右子树以及二叉树判断是否相同。同时最后的表达式从树中输出,这样就可以避免出现多余的括号。建立二叉树结构并判断是否产生负数时,已经完成了表达式的计算,Calc似乎并无必要。但之后又发现有输出分数结果的要求,就重新修改了Calc函数,使分数成为可选功能。最后,我们采用了读取xml文件的方式完成setting,也留了不使用xml的同功能setting作为选择。
实现结构
一.总体结构
总体结构包括两个类,fomularNode和fomularCore,前者是表达式树的节点,包括必要的成员信息,后者是主要模块,public函数包括Calc Generate setting等主要函数。
1 class fomularNode 2 { 3 public: 4 int value; 5 bool chFlag;//if true,value is ascii 6 fomularNode* lchild; 7 fomularNode* rchild; 8 fomularNode() :value(0),chFlag(false),lchild(NULL),rchild(NULL){} 9 fomularNode(int val, bool flag, fomularNode* lch=NULL, fomularNode* rch=NULL) 10 { 11 value = val; 12 chFlag = flag; 13 lchild = lch; 14 rchild = rch; 15 } 16 }; 17 18 class fomularCore 19 { 20 private: 21 vector<fomularNode*> fomulars; 22 vector<char> ops = {'+','-','*','/','^','(',')'};//all ops 需要保持最后两个是括号! 23 vector<string> finalRes;//最终结果,和Generate返回值一一对应 24 int maxopNum = 5;//每个表达式中运算符个数 25 int range = 100;//操作数数的上限 26 int precise = 2;//输出精度(最大为6) 27 int fomuNum;//表达式个数 28 int MaxRange = 100000;//运算中出现的最大数 29 bool fractionflag = true;//是否进行分数运算 30 double result[MAX_FOMU_NUM];//原始字符串运算结果 31 bool okFlag[MAX_FOMU_NUM];//判断原始字符串是否符合要求 32 public: 33 //省略 34 }
二.实现函数
这里给出了fomularCore类中的主要函数
主要函数 | 依赖函数 | 输入 | 返回值 | 功能 |
Calc | string inputFomu | string | 计算输入string的值,返回string形式结果 | |
findMultiple | string instr | int | 获得输入表达式的通分分分母 | |
gcd | int a,int b | int | 返回最大公约数,用于将分数化为最简 | |
arthimetic | string fomu | double | 计算表达式的浮点结果 | |
Generate | string word | bool | 根据任务规定,判断是否是单词 | |
geneExp | int num | vetor<string> | 返回指定数目的原始随机表达式 | |
toPostTree | vector<string> | bool | 建立二叉树 | |
toJudgeTree | 全局变量 | bool | 判断表达式是否符合要求 | |
fomuToStr | vector<string。 | vector<string> | 从二叉树输出string | |
getRes | void | vector<string> | 获得表达式计算结果 | |
settingXml | string path | bool | 读取xml文件并更改参数 | |
ReadXml | string path,...... | bool | ||
setting | int foN,int maxopN,int MaxR,vector<char> op,bool fraction,int preci | bool | 直接更改参数 |
三.具体代码
1 COREDLL_API string Calc(string inputFomu) 2 { 3 int multi; 4 int tp; 5 string tpFomu; 6 long res; 7 multi=findMultiple(inputFomu); 8 9 if (fractionflag&&multi != 1&&!withDot(inputFomu))//有浮点'.'就认为不是分数运算 10 { 11 tpFomu.append(to_string(multi)); 12 tpFomu.append("*("); 13 tpFomu.append(inputFomu).push_back(')'); 14 res = int(arthimetic(tpFomu)); 15 tp = gcd(res, multi); 16 tpFomu = to_string(int(res/tp)); 17 if(multi/tp!=1) 18 tpFomu.append("/").append(to_string(multi/tp)); 19 return tpFomu; 20 } 21 else 22 { 23 //这里似乎有概率访问越界 24 tpFomu=to_string(arthimetic(inputFomu)); 25 for (int i = 0; i < 6 - precise; i++) 26 tpFomu.pop_back(); 27 return tpFomu; 28 } 29 } 30 31 COREDLL_API vector<string> Generate() 32 { 33 34 vector<string> rawFomu; 35 vector<fomularNode*> judgedFomu; 36 vector<string> finalFomu; 37 38 39 rawFomu = geneExp(3*fomuNum);//3是可选参数,保证能选出符合要求个数的表达式 40 41 toPostTree(rawFomu);//建树 42 toJudgeTree();//判断是否符合要求 43 44 for (size_t i = 0; i < fomulars.size(); i++) 45 { 46 if (okFlag[i] == true) 47 judgedFomu.push_back(fomulars[i]);//选出合适的树 48 } 49 50 51 finalFomu=fomusToStr(judgedFomu);//树转表达式,去除多余括号 52 53 for (size_t i = 0; i < finalFomu.size(); i++) 54 { 55 //cout << finalFomu[i] << '='; 56 //cout << Calc(finalFomu[i]) << endl;//测试输出 57 finalRes.push_back(Calc(finalFomu[i])); 58 } 59 60 return finalFomu; 61 } 62 63 COREDLL_API vector<string> getRes() 64 { 65 return finalRes; 66 } 67 68 COREDLL_API bool settingXml(string path) 69 { 70 //xml方式setting 71 string tpop; 72 ReadXml(path, fomuNum, maxopNum, range, tpop, fractionflag, precise); 73 for (size_t i = 0; i < ops.size(); i++) 74 { 75 ops.push_back(tpop[i]); 76 } 77 return true; 78 } 79 80 COREDLL_API bool setting(int foN,int maxopN,int MaxR,string op,bool fraction,int preci) 81 { 82 //非xml方式的setting 83 fomuNum = foN; 84 maxopNum = maxopN; 85 range = MaxR; 86 ops.clear(); 87 for (size_t i = 0; i < op.size(); i++) 88 { 89 ops.push_back(op[i]); 90 } 91 fractionflag = fraction; 92 precise = preci; 93 return true; 94 }
四.Bug及修复
1.检查树的函数调用结果不正确
检查发现是判断二叉树是否相同的子函数的变量名写错了,左子树 lch 和右子树 rch 使用错误,再次阅读时也难以发现,说明一个好的变量名的重要性==
3.双方代码融合出现问题
由于编程平台有区别,linux内核系统下的cpp文件直接添加进工程会出现错误,代码没有问题,但是运行结果不可预测。。。推测是编译器问题,直接复制代码到另一cpp文件解决(这个不是很懂,有更好解决方案欢迎告知
4.加入乘方运算符后越界
之前对数的限制是利用对操作数的直接限制,加入乘方后,很容易随机生成幂次很高的表达式,或者多次乘方运算叠加,很容易变量溢出,最终导致转成字符串输出时出现异常。考虑到使用情景,过高次幂表达式口算有很大难度,生成时指数最大为5。同时验证表达式时,不仅不能为负,单词运算不能超过一定范围。
阶段划分
第一阶段 | 第二阶段 | 第三阶段 | 第四阶段 | |
驾驶员 | 马睿淳 | 马睿淳 | 陈灿 | 陈灿 |
领航员 | 陈灿 | 陈灿 | 马睿淳 | 马睿淳 |
第一阶段 - 把计算的功能封装起来,通过测试程序和API 接口测试简单的加减乘除功能,并能看到代码覆盖率
经历过数据结构课上的大作业,很容易实现了四则运算两个操作数的运算,这一阶段主要是马作为驾驶员,陈作为领航员,一边进行讨论,一边编写程序。我们在过程中采用的思路是:随机生成字符串再判断。并且在工程的过程中不断增加了测试集的数量,每实现一个新的功能的,都要做回归测试,保证了以前运行正确的例子继续是正确的,这主要是考虑到加入的新模块可能会影响旧模块的功能。路径覆盖率 ,判定覆盖率 , 语句覆盖率都达到了较高的水平。
第二阶段 - 完成类的主要逻辑,封装生成表达式模块
本阶段仍然主要由马作为驾驶员,陈作为领航员,我们采用的数据结构是二叉树,为了达到“即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目”这句话的要求,我们采用了递归交换左右子树并且比较的方法。此外,我们基本完成了generate部分类的逻辑,封装生成了表达式模块,这里使用了单一职责原则,一个类只负责一项功能。过程中我们加了范围限制,防止乘方结果过大。
第三阶段 - 通过测试程序和API 接口测试其对于各种参数的支持,并能看到代码覆盖率。
增加了Setting() 函数,提供一个统一接口给用户输入参数,同时我们要让模块支持这样的参数,并且还要保证原来的各个测试用例继续正确地工作。这一阶段主要是陈作为驾驶员,马作为领航员。由于参数较多,考虑用 XML 来传递这些参数。下载了XML对应的6个文件,加入工程中。假定UI组按照Core组的文件读取方式,将用户的要求写入对应的XML文件,我们从XML文件读取相应信息,传入我们的计算模块。同时我们保留了不使用Setting()的接口,以便能与更多UI组对接上。达到了很高的路径覆盖率 ,判定覆盖率等。
第四阶段 - 界面模块,测试模块和核心模块的松耦合。
我们至此完成了core组所有的功能,封装形成dll,交给UI组调试。 在这里UI组和Core组的划分,本身就是MVC原则的体现。Core组主要负责程序核心(Model),UI 组的工作主要是用户交互(Controller)和数据显示(View)。当然,Core和UI组的功能划分也没有这么详细,具体情况具体分析,主要还是看编程者的思路。
个人作业和结对编程的区别?
这次作业最大的收获就是明白了结对编程的意义,有一个好的队友真地很重要。在和队友结对编程的过程中,能看到对方的优点,同时看到自己的缺点,队伍里两个人由此都能得到巨大的进步。从开发层次来说,编写的代码同时经过2个人的眼睛,设计质量和代码质量都有了很大的提高,同时代码也更加规范了,因为谁也用不惯对方的代码风格,最后更多地偏向于书上提供的规范。同时,在这次结对编程中,认为马同学解决问题的能力很强,在难以描述的bug面前,总能在很短时间内解决问题,给我们的结对编程工作带来更大的信心以及更高的满足感。同时我们不断地编程过程,就是不断的复审过程,因为我们的一举一动都在对方的视线之内,在这种督促的压力下,我们的工作显得更加认真,并且我们要提高自己能力不让对方小看。结对编程使这次作业开发流程显得更加规范。
走上工作岗位之后,是否会采用结对编程?
走上工作岗位之后,很大概率不会采用结对编程。当然,培训新人的时候可能会考虑。因为结对编程特别适合于知识的分享和传递,特别适合于帮助开发者快速熟悉自己所不熟悉的领域。更多情况下是两个人水平相似,因此结对编程有浪费时间的嫌疑。另外结对编程的双方如果性格不合(这点概率是很大的),可能陷入很多的争吵,而导致进度停滞不前。而且结对要求结对的双方都要保证有充沛的精力,因为很容易疲劳。最后,结对编程不能完全替代代码评审,虽然结对编程对代码的审核程度比代码评审更加细致,但两个结对的人有一定的思维趋同性,从而忽略同样的问题。
今后的作业如何从结对编程,个人作业中获取经验?
这两次作业给以后开发流程有很多可以借鉴的地方,其中个人感觉最重要的一点是要跑在时间前面,因为我(陈灿)第一次个人作业是在截至时间前一分钟提交的。把事情留在最后,的确能提高时间的效率,但是这是在拿自己project的质量冒险,因为在软件开发的过程中,可能会有各种各样的bug跳出来,而且在太大压力下编写的程序很大概率会有一些潜在的bug。在接下工程的那天开始,就得有一个详细的计划,规定XX天之前必须完成什么阶段。而且还要有足够的时间余量,来应对突发情况,这才是一个合格的程序员应该具备的素养。
课程建议
我们认为软件工程是非常能锻炼我们coding能力的一门课,之前C语言和数据结构更多是教给我们编写一个简单的模块的能力,而软件工程则侧重于把这些模块结合起来,而且会使用到一些在实际编程中很实用的东西,比如单元测试,看热行等等,相关技巧在其他课程是学不到的。但是,这门课程也存在一些问题,比如布置编程作业时要求不是很明确,导致学生们走了不少弯路(后来咨询过老师,表示这是个新题目,所以助教和老师也是在摸索中前进)。另外,就是希望老师能在布置作业之前,把涉及到一些语言知识点大概提一下,让学生们知道要去学什么。这门课的主体学生是信息学院英才班的学生,英才班是由电子所搞起来的,所以信院从一开始就是把这些学生往ee方向培养的,cs的课程接触地比较少,所以编程水平没有老师们想象中那么高。C++中的一些概念是没有接触过的,如果不加提醒,学生们很大程度上是会按照C的编程思想写,这样会走很多弯路。