OO第一单元作业总结
第一次作业
一、基于度量分析程序结构
(右键 src->Diagrams->Show Diagrams->Java Class Diagrams)
第一次作业的思路是:先用大正则分析输入的正确性(造成爆栈bug发分析见“程序中的BUG总结一栏),后采用五种类型(coef*x, coef*x^index, x^index, x_only, coef_only)的五个小正则将表达式切割成小项并添加入Arraylist中,添加过程中进行合并,最后对各项求导并输出
2、复杂度分析
(右键 src->Analyze->Calculate Metrics, 在Metrics profile框下选择Complexity metrics)
ev(G):Essential Complexity(基本复杂度),衡量程序非结构化程度
iv(G):Module Design Complexity(模块设计复杂度),衡量模块与其他模块的调用关系
v(G):Cyclomatic Complexity(圈复杂度),衡量一个模块判断结构的复杂度
从Metrics分析可以看出,代码的结构化程度基本良好,但模块间的调用关系以及模块判断结构都不尽人意,尤其是Expression.calcAndAdd(Unit)
方法,数据表明该方法右极大改良空间。数据也说明该份代码可维护性较低。
二、程序中的BUG总结
1、大正则导致的爆栈bug
问题所在类
class Expression
特征
使用Matcher.matches()
方法,当字符串过长时,由于正则匹配默认为贪婪模式,在匹配项时发生回溯造成爆栈(正则表达式三种模式:贪婪模式、懒惰模式、独占模式)
解决方法
1)、贪婪->独占
笔者的大正则为第一项[+-]后续项*
,只需在*后加上+即可转换为独占模式,即第一项[+-]后续项*+
2)、利用尾指针的翻动查找匹配
预处理为若表达式的第一项无符号,添加一个“+”号,并声明一个int tailpointer = 0
。利用Matcher.find()
方法进行查找,若Matcher.start() != tailpointer
,说明查找过程中出现非项元素,返回false
,否则令tailpointer = Matcher.end()
。查找结束时,若tailpointer != String.length()
,说明表达式中仍有不匹配项,返回false
,否则返回true
3)、添加隔板切分项,并在添加项的过程中进行正确性匹配
预处理为判断字符串中是否存在隔板字符。利用StringBuffer
类与Matcher.appendReplacement()
, Matcher.appendTail()
方法添加隔板,并用String.split()
方法切割。具体代码如下:
Pattern checkWallP = Pattern.compile("wall"); Matcher checkWallM = checkWallP.matcher(str); if (checkWallM.find()) { System.out.print("WRONG FORMAT!"); // if there has a "wall" System.exit(0); } // wall position regex (append at the tail) Pattern pattern = Pattern.compile("[x\\d][ \t]*[+-]"); Matcher matcher = pattern.matcher(this.expression); //new total buffer "strbuffer" StringBuffer strbuffer = new StringBuffer(); while (matcher.find()) { // new temp buffer StringBuffer pointer = new StringBuffer(matcher.group()); // insert wall in temp buffer "pointer" pointer.insert(1, "wall"); // append replacement in total buffer "strbuffer" matcher.appendReplacement(strbuffer, pointer.toString()); } // append tail str in total buffer "strbuffer" matcher.appendTail(strbuffer); // split this.unitstr = strbuffer.toString().split("wall");
2、输出字符串为空时异常
问题所在类
class Unit
特征
考虑不周全从而遗漏输出为0情况
解决方法
判断输出为空时输出0
三、互测中BUG分析采用策略
由于没有掌握自动化测试的方法,在第一次互测中采用观察代码的方式寻找bug。观察代码并不是将每个人的代码从头读到尾,根据与同学的交流,能够进入互测的代码在逻辑上或许不会有太大问题,主要关注点在输入处理、正确性判断与输出处理这几个模块上,手误在正则中多打了字符、预处理使用替换存在漏洞、输出为空时抛异常(像笔者一样粗心),这些都是在观察代码的几个主要关注模块后构造测试样例便可发现的bug。
第二次作业
一、基于度量分析程序结构
1、类图
第二次作业的思路是:采用尾指针翻动查找的匹配方法进行正确性检查,之后进行项查找并构造三元组,并添加到Arraylist中,最后构造求导类进行求导。本次作业做了抽象出求导类的尝试。
2、复杂度分析
从Metrics分析可以看出,代码中大多数方法的结构化程度都良好,而对于少数几个方法如Expression.checkExpression()
, Expression.Derivate()
, Unit.printSelf()
,以及构造方法Unit.unit()
都存在许多问题,降低了代码的可维护性和美观性。笔者将对以上几个方法一一分析。
Expression.checkExpression()
方法中涉及了预处理操作(即添加前导符号),造成模块功能的不清晰。改进方法可以是将预处理分离。
Expression.Derivate()
方法中进行了合并求导后同类项并简化表达式的操作,显然属于“多管闲事”类型,功能十分不清晰且代码杂乱难看,改进方法是分离各个功能作为新的方法
Unit.printSelf()
中采用了许多if-else结构进行输出简化,造成圈复杂度的增大。改进方法可以是将每一种情况切分成方法,并在Unit.printSelf()
中调用。
Unit.unit()
构造方法中进行了合成三元组操作,导致构造方法功能不清晰。解决方法是使用工厂函数,或另声明方法进行三元组合并。
二、程序中的BUG总结
1、简化中遗漏系数判断
问题所在类
class Expression
特征
检测三元组后提取sin(x)^2+cos(x)^2
,遗漏了对系数的判断。原因是在测试时间紧张,且在测试时惯性地使用了简单的数据,没有发现bug。
解决方法
添加对系数的判断
2、前导[+-]1
对符号的影响判断错误
问题所在类
class Unit
特征
对题意理解不深,在编程时惯性地将前导+1、-1的简化误以为是任意连乘因子前可以添加的符号,造成结果的系数符号错误
解决方法
将前导[+-]1
对连乘项系数符号的影响改为对三元组前的符号的影响。
三、互测中BUG分析采用策略
笔者由于自己的疏忽,未能参与互测,故在这一栏将填写对这次疏忽造成灾难的反思。
在截止提交的前一晚本已经提交了AC的版本安心睡觉,早上起床突然想到简化表达式的一种方法,迫不及待开始动工,在8:45时正式完成commit与push,脑袋一热就点击了提交。在出现一排Wrong Anser
之后陷入一阵 沉思
事后一度思考自己的行为是否受本体意识控制,也为这次粗心懊悔很久。事后debug的过程中才发现,是非常愚蠢的问题,也就是"BUG总结"中的第一点:遗漏系数判断。思考原因后发现,是在此语句前的合并三元组的条件判断中无系数判断(因为三元组合并的就是系数),加之自己写得急,就被误导了。
于是造成这次灾难的重要原因必有:代码功能混乱。在求导里合并三元组,在合并三元组之下判断可简化性,不仅使自己维护优化的难度增大,更背离了面向对象的设计理念。
灾难事件之后,我也多次指责自己,好好的AC多漂亮,为什么要优化?性能分根本不值得去花费这些精力。思前想后,也许仅仅是因为看见输出又精简了一些心里会有一种莫名的舒畅。
这是一次愚蠢的疏忽,但转念一想,不用写互测中的BUG分析了又未尝不是一件好事呢:-)
第三次作业
一、基于度量分析程序结构
1、类图
第三次作业的思路是:同第二次作业一样采用翻动查找的模式进行正确性匹配,其中利用栈识别括号表达式并进行递归判断,之后处理表达式的方法是识别单项(此处结构是将嵌套组合项作为父类NestCombTerm
,三角函数Trigfunc
和幂函数Poly
继承嵌套组合类) ,将单项组合为乘法项MultiCombTerm
,后将乘法项组合为加法项AddCombTerm
。求导方法从Expression
类对加法项求导,加法项对每一项乘法项求导,乘法项对单项按求导规则求导,最后返回String并输出。
2、复杂度分析
从Metrics分析可以看出,大部分方法的复杂度处于安全状态,但关于格式检查、括号匹配和嵌套组合的方法十分惹眼,由于其中包含大量条件语句判断,导致圈复杂度、模块复杂度与基本复杂度都有不同程度的高涨,使得程序后期优化困难。
二、程序中的BUG总结
1、系数乘以括号表达式时遗漏系数
问题所在类
class NestCombTerm
特征
问题没有考虑周全,仅考虑了sin()
和cos()
的系数,而漏掉仅有()
的表达式系数,归根结底是由于设计层面工作不充分,没有将项的设计理清,而是在编程过程中东填西补。
解决方法
在返回"("+inside+")"
语句前添加this.coef+
三、互测中BUG分析采用策略
本次互测采用了半自动化的测评方式,输入为自己构造,评测为自动评测。
自动测评的第一步是编译打包。编写脚本,先利用javac
编译互测屋中所有成员的代码,生成.class
文件,后利用grep 'public static void main'
将主类找出并重定向生成manifest
文件,最终生成.jar
包,方便测评。
第二步是利用python的表达式计算库sympy
,编写生成答案与正确性比较的py程序参考讨论区(正确性比较可直接利用equals()
方法,若表达式等价,返回True)
第三步是编写judge.sh
脚本拉取所有人的输出与答案进行比对,这里本人对评测输出优化了外观,可以输出好看的Accept
和Wrong Anser
!(摸鱼王
if [ "$result" == "True" ]; then echo -e "\e[32;47m Accept \e[0m" # 32:green words, 47:white back else echo -e "\e[31;47m Wrong Anser \e[0m" # 31:red words fi echo # \n
效果如下:
自动测评虽方便,但也降低了看代码的积极性。直到研讨课听取优秀同袍的分享,才意识到互测的目的并不是“找到”,而是“去找”,日后还需多加注意。
对象创建模式
这三次作业都分别做了不同的尝试。
第一次作业只抽象了表达式与项,是面向过程思维向面向对象思维的一小步;
第二次作业进行了重构,将项类重构为三元组类(但起名还是Unit),并做了抽象求导类的尝试,不过总结时发现行为与特征同时抽象,显得逻辑还是有些混乱,主体架构不太明确;
第三次作业又是一次大重构,由于三元组已经不再适用,第三次作业采用了分层抽象的方式,将表达式一层层剥开并组合,在表达式的架构上思路还算比较清晰,但是表达式正确性检查与切分等模块还是显得冗乱。
总总结
总的来说,这三次作业是对面向对象设计与构造的初入门,看到了程序中的一些良性变化,也更意识到有数不清的不足之处,如复杂度、模块化、鲁棒性等等。经过这三次作业,也更意识到了“设计”的重要性,以及对代码风格的重视。