软件工程结对作业
软件工程结对作业
——四则运算
School of information,USTC
A同学:李浩冉 PB16061409
B同学:曾子轩 PB16061449
一、任务流程(作业要求:http://www.cnblogs.com/silent-zlv/p/8684979.html)(GitHub:https://github.com/TrinidadZZX/GroupWork)
(1)项目摘要:写一个根据设定要求能自动生成小学四则运算题目并进行计算的DLL库。
(2)开发计划:
1)讨论流程:按照用户的需求,我们打算将流程设计为:设定属性->生成表达式、计算结果->UI调用。
2)分配任务:经过对用户需求的分析,以及对任务量的权衡,我们将核心算法分为两个部分中缀表达式生成、表达式计算,并将它们合并起来写为一个函数作为对UI的接口。又因为用户的需求最初是偏、难、怪的,所以将最难的中缀表达式生成部分和相对容易的完善代码框架、外部接口、表达式计算分为两个任务交给A、B两个同学。
3)分析实现方法:
在生成部分采用随机函数,根据整数,分数与小数其特点生成字符串,并返回。
在计算部分采用先将表达式转化为逆波兰式,然后再利用栈结构进行计算。
因为两个部分没有结构上的重合,更深层次地是为了更快地完成开发,所以约定好以字符串作为两者的接口后便开始了并行开发。
(3)时间规划:
A同学:
流程规划 |
计划用时 |
实际用时 |
|
前期讨论 |
分析,讨论任务实现流程 |
30min |
1h |
分析,讨论任务难点并分配任务 |
30min |
||
分析代码耦合性,讨论参数传递 |
15min |
||
前期准备 |
查阅随机函数,学习代码规范 |
1h |
1h |
框架设计 |
学习代码框架,确定抽象函数接口 |
30min |
1h |
具体实现 |
用随机函数产生随机数(保证随机性) 写出产生整数式子的函数 类比写出分数,小数(结构统一) 局部优化(式中不出现负数,整数操作符两边加空格) |
1h 3h 2h 4h |
5h |
整合 |
与B同学的程序进行整合修改 |
2h |
1h |
B同学:
(4)代码架构和设计:
在多人任务中宜采用面向对象的语言,本例中采用的C++促使开发者以更明晰的结构思考代码接口,对后期的测试、维护很有帮助(事实上Java在测试方面更胜一筹,然而最初任务中规定以头文件的形式与UI进行交互,所以也就选择了与C兼容的C++)
代码风格展现:(OprationClass.h)
private: //B同学: int numOfQuestion; //题目的数量 int numOfOperand; //操作数的数量 double rangeOfQuestion; //题目中的数值范围 //运算符的种类 bool addition; bool subtraction; bool multiplication; bool division; bool power; bool brackets; int precision; //精度 bool properFraction; //是否支持真分数 bool decimalFraction; //是否支持小数 //都不支持说明支持整数 //计算整形、小数形式的算式 static ReversePolishType ReversePolishNotation(string Input)throw(...); //计算整形、小数形式的算式:根据string生成逆序波兰式 static double CalReversePolishNotation(ReversePolishType expression)throw(...); //计算整形、小数形式的算式:根据逆波兰式计算结果 //计算分数形式的算式 static RPT_FractionType RPN_FractionType(string Input)throw(...); //计算分数形式的算式:根据string生成逆序波兰式 static fractionType CRPN_FractionType(RPT_FractionType expression)throw(...); //计算分数形式的算式:根据逆波兰式计算结果 //分数形式的加、减、乘、除、乘方 static fractionType fraAdd(fractionType a, fractionType b)throw(...); static fractionType fraSub(fractionType a, fractionType b)throw(...); static fractionType fraMul(fractionType a, fractionType b)throw(...); static fractionType fraDiv(fractionType a, fractionType b)throw(...); static fractionType fraPow(fractionType a, int b)throw(...); //A同学: string generate()throw(...); string generate1()throw(...); string generate2()throw(...); string generate3()throw(...); string generate0()throw(...);
public: OperationClass(); //无参构造方法 //提供给UI的属性、方法 //对象的属性 vector<string> expression; //存储生成的表达式 vector<string> answer; //存储表达式的计算结果 void Setting(int numOfOperand, int numOfQuestion, double rangeOfQuestion, bool addition, bool subtraction, bool multiplication, bool division, bool power, bool brackets, int precision, bool properFraction, bool decimalFraction);/*设置属性:操作数数量, 题目数量,数值范围,加法?减法?乘法?除法?乘方?(真分数?or小数?最后两项只可选其一,否则抛出异常)*/ void GenerateAndCalc(); void Generate(); //生成表达式,并存储在string当中 void CalcAllExpression(); //对题目表达式依次进行计算,将结果string数组返回 //获得对象的属性 int getNumOfQuestion(); //获取题目数量设定值 //B同学提供给A同学的方法 //计算表达式expression //返回double类型的答案 double CalcDouble(string expression)throw(...); //整数/小数形式表达式:对输入表达式字符串进行运算,然后以数值形式返回 double CalcFrationDouble(string expression)throw(...); //分数形式表达式:对输入表达式字符串进行运算,然后以数值形式返回 //返回string类型的答案 string Calc(string expression)throw(...); //整数/小数形式表达式:对输入表达式字符串进行运算,然后以数值形式返回 string CalcFration(string expression)throw(...); //分数形式表达式:对输入表达式字符串进行运算,然后以数值形式返回
(5)调试与优化
A同学:
1)调试:
由来:在表达式生成的过程中出现了多余的括号(在一个数字的两端有括号)
发现:发现bug 后,我首先,在函数里找到生成括号的两个部分,(brackets==1即有括号时,与减法),由于算法所以brackets==1时不出生成多余的括号,所以只有可能是减法部分出错。
减法思路,如果做左操作数(可以是数字也可能是式子)比又操作数小,交换左右操作数并给现在的右操作数加上括号,当时没有考虑到,右操作数可能只是一个数字,所以加上括号后就产生了多余的括号。
解决:方法很简单,交换后的右操作数是数字只有可能是生成的第一个操作上数时,所以加上if语句判断当前是否为第一个操作数,如果是不加括号交换。
思考:bug 很好查出来,但是这很好的说明了在写代码时的考虑不周,幸亏进行了近千词的生成,否则根本无法察觉。
2)优化:
其实在第一个版本也就是光生成表达式没有加括号和对表达式中出现负数的处理,分析表示整体费时很少,但是加上哪些功能后,加上了一堆判断函数,和一堆操作同时减法又调用了B同学的计算函数,耗时呈指数形式增加,由于B同学熟练使用c++,于是我请教他用string的方法处理字符串,将我最初的一段字符串处理改为string,发现时间减少了,但是由于时间关系,没有将全部代码改成string型。
B同学:
1)调试:一个算法逻辑上的小BUG,一个机器精度上的大BUG
算法逻辑(20min解决):
由来:在进行分式的运算的时候,需要对分式进行通分,而在求取最大公约数的过程中:
//求取最大公因数 inline int commonDivisor(int a, int b) {//最大公约数 int temp1, temp2, temp3; if (a1 >= b1) { temp1 = a1; temp2 = b1; } else { temp1 = b1; temp2 = a1; } //通过比较对num1和num2赋值,便于计算 while (temp2>0) { temp3 = temp1 % temp2; temp1 = temp2; temp2 = temp3; } //辗转相除,num2=0时,num1=最大公因数 return temp1; }
可见如果输入的值存在负数,那么算出的结果将是0,这也是前期总是出现除零错误的原因。
发现:在出现除零错误后,我在程序中加入断点,观察对什么样式的表达式会出现错误,然后选取其中之一代入运行,结果发现在求取最大公约数的函数中发生错误。
解决:很简单,只需要在函数的开头加入取绝对值的语句即可。
思考:发现、解决一个BUG很简单,但是一个BUG的解决往往会带来一系列的问题,因为这引出了一整类的你没有考虑到的问题,需要对调用函数甚至整个流程进行重新考虑。这一次比较幸运的是只需要在调用函数中增加一点判断,然而当调用函数不止一个或者是在整个流程出现漏洞的时候,这个问题就很严重了。所以BUG的发现是越早越好,这也是说BUG在越早的时间被考虑到,它所带来的负面效应则越小。
机器精度(4h解决):
由来:随机生成的表达式在计算过程中可能存在溢出现象,而溢出后将出现一系列奇怪的错误。
发现:尽管算法在逻辑上并没有问题(对于操作数较小的算式在有限次测试中没有发生问题),但是操作数一大,各种问题都涌现出来。
解决:实际上到现在都难以理解为什么溢出后在计算中会出现除零错误,尽管我在表达式中声明了对各种异常的接收,但是程序仍然对一些异常(例如除零)没有进行处理,没有办法:一方面我在各个底层的计算函数的声明上加入了throw(...)允许抛出各类异常,然后触发异常后重新生成算式,另一方面对操作数的大小进行了限制。
思考:我现在意识到了机器精度所引发的问题,尤其是int型很容易触发(至少在本例中int比float型因数值范围更容易触发异常),以后一方面我在数据类型的选择上将通过宏定义进行限定,另一方面我会在计算机领域更深入地探讨这个问题。现在虽然逻辑上自己能做到几百行只出现一两个BUG,但是能无法解决因为精度而引发的一系列BUG。
2)优化:
判断:这次作业中,我采用了很多C++固有的数据类型,例如string、stack。虽然这简化了编程,但是在优化上能做的却非常少,因为这相当于你站在别人搭的戏台上表演,然而最后你需要对戏台进行检查、优化,这对我而言很难做到。另一方面本例的核心函数(重要又耗时间)集中在A同学的生成表达式函数中,而他的函数又调用了我写的函数,能做的优化也十分有限。
思考:针对以上所遇到的问题,以后在对效率有苛刻要求的地方,我尽量采用面向过程的C语言进行编写,这样更容易地控制底层实现,甚至可以修改汇编指令,以达到更优的效率。
二
对于个人作业来说代码是给自己看的,只要自己懂就行了,但是如果要和别人结对编程,一定要先进行代码规范,这样子别人不至于完全看不懂你的代码,而且事先要搞清楚自己的函数名,要返回什么,返回什么类型,像这次变成我就做的不太好,由于用c 语言比较多,我产生的函数返回的是char 型指针,而B同学需要的是string类型的一条字符串,我以为这两者是互通的,结果发现并不是,最后费了一些时间将我的代码更改为返回值为string 的函数,才将代码融合成功
结对编程我认为是必须的,除非能强到自己一个人开发一个项目,当然这是不可能的,起码对于我来说。所以对于我来说,掌握代码规范是一件非常重要的事情,因为不可能让团队里的其他人来配合我一个人,让他们跟上我的习惯,所以我尽可能的要去和其他人一致,而一般的好的程序员,它们的程序都是相当规范的,所以说我平日里写代码,要尽量规范自己。