面向对象第一单元总结
第一单元的编程练习目前告一段落,初次接触面向对象的编程模式总体来说有一定的挑战性。本单元要求实现基础的表达式求导,第一次主要针对简单的幂函数表达式进行解析并求导,第二次加入了三角函数,第三次需要实现三角函数和表达式嵌套并且在求导前要对表达式的合法性进行检查。在三次作业中,难度递增,总体来讲第二次作业是质变,第三次作业是量变,也正因为如此,笔者在第二次作业中进行过一次代码重构,前后两次代码结构差异较大。下面的程序结构分析将主要分为两个部分着重分析介绍重构前后程序的差异...
程序结构分析(基于度量)
第一次作业
第一次作业在入手之前没有做出过多的考虑,因此结构较为简单,没有富余出空间来进行后续的需求补充,思路也是顺承C语言的过程性处理方式的思维。首先展示程序代码中类的依赖结构,如下图。
不难看出程序中定义了四个类,逐层向下依赖,其中核心的两个类分别是MainClass.java
和Polynomial.java
。第一次作业的主要实现思想是利用正则表达式拆分各个项,放进ArrayList
中然后将各个项作为一个单位进行求导,通过toString()
进行输出。
MainClass.java
的方法由上图所示,main函数是主函数,实现主要的处理逻辑调用,handleRawData
是用来处理输入的字符串,其中在函数参数中printPolynomial的参数依赖了Polynomial类,主函数中同样依赖了该类。
Polynomial.java
的方法由上图所示,除了基本的getter&setter函数之外还有一些其它逻辑函数,其中包括对toString(), equals(Object), hashCode()
的重写,还有进行指数、系数的运算,对相同项进行检查、对多项式进行优化等等。由于该类属于一个单位类,在此次程序的代码结构中处于较底层,所以该类对其它构造的类没有依赖。
Mycomparetor.java
类实现了Comparator
接口,仅仅重写了compare
方法,用于对两个项相等进行判断。
下面进行Metrics分析,首先对一下表格中的数据进行注释。
- ev(G)基本复杂度是用来衡量程序非结构化程度的。
- Iv(G)模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。
- v(G)是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数。
- CogC是认知复杂度
- LOC: Line of Code
- NCLOC:Non-Commented Line Of Code
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
mainpackage.MainClass.generateRegex() | 0 | 1 | 1 | 1 |
mainpackage.MainClass.getPoly(String) | 1 | 1 | 2 | 2 |
mainpackage.MainClass.handleRawData(String) | 0 | 1 | 1 | 1 |
mainpackage.MainClass.main(String[]) | 3 | 1 | 3 | 3 |
mainpackage.MainClass.printPolynomial(HashSet |
7 | 1 | 5 | 5 |
mainpackage.MyComparetor.compare(Object,Object) | 5 | 4 | 3 | 5 |
mainpackage.Polynomial.Polynomial(String) | 17 | 1 | 6 | 9 |
mainpackage.Polynomial.Polynomial(int,BigInteger,BigInteger) | 0 | 1 | 1 | 1 |
mainpackage.Polynomial.calculateCoe(String) | 5 | 3 | 5 | 5 |
mainpackage.Polynomial.calculateExp(String) | 4 | 1 | 4 | 4 |
mainpackage.Polynomial.checkSamePoly(HashSet |
3 | 3 | 3 | 3 |
mainpackage.Polynomial.equals(Object) | 3 | 3 | 2 | 4 |
mainpackage.Polynomial.getCoe() | 0 | 1 | 1 | 1 |
mainpackage.Polynomial.getDif() | 4 | 1 | 2 | 3 |
mainpackage.Polynomial.getExp() | 0 | 1 | 1 | 1 |
mainpackage.Polynomial.getSign() | 0 | 1 | 1 | 1 |
mainpackage.Polynomial.hashCode() | 0 | 1 | 1 | 1 |
mainpackage.Polynomial.mergePolynomial(Polynomial,Polynomial) | 1 | 1 | 1 | 2 |
mainpackage.Polynomial.removeFrontSign(String) | 10 | 1 | 6 | 9 |
mainpackage.Polynomial.removeZeroPoly(HashSet |
0 | 1 | 1 | 1 |
mainpackage.Polynomial.setCoe(BigInteger) | 0 | 1 | 1 | 1 |
mainpackage.Polynomial.setExp(BigInteger) | 0 | 1 | 1 | 1 |
mainpackage.Polynomial.setSign(int) | 0 | 1 | 1 | 1 |
mainpackage.Polynomial.toString() | 12 | 1 | 9 | 10 |
null.test(Polynomial) | 0 | n/a | n/a | n/a |
上述表格是对该程序各个函数的复杂度进行分析。下面分别阐述分析得到的结果。
- CogC
- Cognitive Complexity:翻译成中文是认知复杂度,它将一段代码被阅读和理解时的复杂程度,估算成一个具体数字。
- 出现"break"中止了线性的代码阅读理解,如出现循环、条件、try-catch、switch-case、一串的and or操作符、递归,以及jump to label:代码因此更复杂。
- 多层嵌套结构:代码因此更复杂。
通过观察表格,对比两个CogC较高的函数mainpackage.Polynomial.toString(), mainpackage.Polynomial.Polynomial(String)
和CogC较低的函数mainpackage.Polynomial.mergePolynomial(Polynomial,Polynomial), mainpackage.MainClass.getPoly(String)
,容易发现前者在函数中运用了较多的if
语句嵌套以及循环语句和判断语句的嵌套,而后者的可读性较高,嵌套较少。
-
ev(G)
- Essential Complexity,基本复杂度是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。
-
iv(G)
- Module Design Complesity模块设计复杂度,即该模块和其他模块的调用关系,软件模块设计复杂度高意味着模块耦合度高,这将导致模块难以隔离、维护和复用。
第二、三次作业
这两次作业是对第一次作业的重构重写,这两次作业中需要实现对三角函数的求导、括号内表达式递归嵌套以及表达式合法性检查。程序结构如下图。
下图为建立表达式中项单元的继承关系。
其中Polyfunc定义了表达式中项单元可能含有的基本属性。
继承自Polyfunc的类中含有常数类,幂函数类、项类、三角函数类,其中三角函数类内部还有可能包含一个项(即三角函数括号内的内容),如下图所示。
在判断表达式合法性的时候会用到的类为HandleLayer.java Layer.java
,采用逐层判断的方法,按括号的深浅规定层级,每一个层级只包含一个括号级别,递归地对每个括号中的内容进行逐层判断,每一层都运用正则表达式去判断是否合法,即可完成判断。
上图展示了类内的属性,HandleLayer中包含了正则表达式的String值,Layer包含了每一层的递归判断。
整体的程序中类的依赖关系如下图:
其中的Stack.java
类是实现了一个简单的栈,用于中缀表达式的表达式树的构建。
HandleLayer.java Layer.java
上文已经提到过,用于对表达式按括号级别分层级递归进行表达式合法性的检查。
Polyfunc.java
同样上文提及,是用来储存并表达表达式项的类,继承自它的子类有Item.java Powfun.java Const.java Trifun.java
分别代表项、幂函数、常数、三角函数。
MainClass.java
是主函数入口,其中定义了许多static类型的面向过程的函数,方便每一次作业进行个性化的调整。
下面对程序各个类以及各个函数度量分析
首先以各个类为单位进行分析。
- OCavg代表类的方法的平均循环复杂度。
- OCmax代表类的方法的最高循环复杂度。
- WMC代表类的总循环复杂度。
Class | OCavg | OCmax | WMC |
---|---|---|---|
mainpackage.Const | 2 | 3 | 6 |
mainpackage.HandleLayer | 3 | 5 | 9 |
mainpackage.Item | 5.4 | 12 | 27 |
mainpackage.Layer | 3.25 | 10 | 13 |
mainpackage.MainClass | 4.2 | 10 | 63 |
mainpackage.Polyfunc | 1.91 | 11 | 44 |
mainpackage.Powfun | 3 | 4 | 9 |
mainpackage.Stack | 1.2 | 2 | 6 |
mainpackage.Trifun | 2.71 | 6 | 19 |
可以从如上表格中看出类的平均循环复杂度最高的是Item类,最低的是Stack类,总循环复杂度最高的是MainClass类。
不难分析Stack类的循环复杂度低是因为类的逻辑及其简单,只需实现出栈、入栈、栈空等类型的判断即可,而Item类以及MainClass类需要实现复杂了逻辑功能显然复杂度较高。
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
mainpackage.MainClass.getInitialPoly(String) | 25 | 1 | 6 | 14 |
mainpackage.MainClass.getMatchString(String) | 5 | 1 | 6 | 6 |
mainpackage.MainClass.getPolyTree(String,boolean) | 14 | 5 | 9 | 10 |
mainpackage.MainClass.getRestData(String) | 1 | 1 | 2 | 2 |
mainpackage.MainClass.handleWhenOp(String,Stack,Stack,boolean) | 13 | 2 | 9 | 10 |
mainpackage.MainClass.main(String[]) | 2 | 1 | 2 | 2 |
mainpackage.MainClass.polyIsNum(String) | 2 | 1 | 3 | 4 |
mainpackage.MainClass.pullTrisExp(String) | 1 | 1 | 2 | 2 |
mainpackage.MainClass.pushMi(String,Stack) | 1 | 1 | 2 | 2 |
mainpackage.MainClass.pushNum(String,Stack) | 1 | 1 | 1 | 2 |
mainpackage.MainClass.pushOpNum(String,Stack,String) | 3 | 1 | 4 | 4 |
mainpackage.MainClass.pushTri(String,Stack,String) | 0 | 1 | 1 | 1 |
mainpackage.MainClass.readTrisChildPoly(String) | 0 | 1 | 1 | 1 |
mainpackage.MainClass.simplifyFrontSign(String) | 11 | 1 | 11 | 11 |
mainpackage.MainClass.simplifyPoly(Polyfunc) | 40 | 5 | 10 | 12 |
具体查看MainClass类的函数,可以看到复杂度较高的函数是simplifyPoly() handleWhenOp() getInitialPoly() getPolyTree()
而对应的函数目的是优化表达式、对输入表达式进行处理、构造表达式树。
可见优化表达式对复杂度有较大的占用。
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
mainpackage.Item.Item(int,BigInteger,BigInteger,BigInteger,BigInteger) | 10 | 1 | 2 | 7 |
mainpackage.Item.addition(Polyfunc,Polyfunc) | 3 | 1 | 2 | 3 |
mainpackage.Item.equals(Object) | 4 | 3 | 4 | 6 |
mainpackage.Item.multiply(Polyfunc,Polyfunc) | 4 | 1 | 3 | 4 |
mainpackage.Item.toString() | 18 | 2 | 10 | 12 |
此处具体查看了Item类,toString()
函数的复杂度较高,说明将一个项转化成为一个合法的字符串逻辑较为复杂,需要将逻辑进一步进行拆分。
分析自己程序的bug
通过三次作业的bug修复,笔者也找到了一点点较快debug的方法。
首先,遇到一个引发输出错误的bug不能立即去进入程序进行调试,若有更多的测试样例,应当一并输入,查看引发错误的样例,将多项引发错误的样例排列在一起纵向对比找出相似的点,缩小bug查找范围。
若样例测试不够充分,应当对单一的测试样例进行逐层的缩小,把较为复杂的表达式按照“二分法”进行缩小,多次测试,缩小引发错误点的样例的构造长度,从而缩小bug查找的范围。
在多次公测互测中,bug的查找及修复记录如下所示
>>>>>> BUG >>>>>>
in Item.java -> public String toString()
ERROR: "-sin(x)" is a wrong format
try this "-sin(x)sin(x)(cos(x)-+x**-2)(-+(cos(x)+-x))"*
in MainClass.java -> public static void handleWhenOp()
ERROR: in the judgement of poping opStack
try this "x-2(-+sin(x))-(x+1)"*
in MainClass.java -> public static String simplifyFrontSign()
ERROR: When you simplifyFrontSign you also change the sign in the middle or end
try this "3(--5cos(x)+-1cos(x))"*
in MainClass.java -> public static void handleWhenOp()
ERROR: judge the !opStack.isEmpty() first or the process will be null pointer
try strong-data-11
in Polyfunc.java -> public Polyfunc removeZeroPoly()
ERROR: try "1-0"<<<<<< BUG <<<<<<
对用查看上述的表格
in Item.java -> public String toString()
in MainClass.java -> public static void handleWhenOp()
in MainClass.java -> public static String simplifyFrontSign()
in MainClass.java -> public static void handleWhenOp()
in Polyfunc.java -> public Polyfunc removeZeroPoly()
圈复杂度分别为12,10,11,10,15均处在很高的水平,从某种程度上反映出函数圈复杂度越高,该函数出错的可能性也越高。
发现他人程序bug采用的策略
经过三次互测,hack他人程序的测试样例的构造方法主要由笔者总结为一下三种:
- 随机法
- 随即构造或者使用自己编写的测评机随机构造测试样例来进行大规模大批量测试
- 复杂法
- 通过增加表达式中的一个项的长度和多样性,使得在一个项中可能出现多种出错机会
- 增加表达式的长度和多样性,使得在一个表达式中出现多种出错机会
- 增加嵌套深度,考验程序的鲁棒性
- 分析法
- 通过阅读他人的程序代码及程序架构分析出对方容易出错的点进行针对性测试
- 通过仔细阅读指导书,分析可能存在的极端测试样例进行构造
其中结合被测程序的代码设计结构来设计测试样例的思路已在上述分析法中给出。
综合上述三种测试方法,性价比较高的方法为复杂法,能够在较短时间内构造出多种错误测试样例。
重构经历总结
本次设计主要是在第二次作业进行了一次代码的重构。
重构前:使用正则表达式顺序处理下来,只关注了实现的过程和结果,没有关注程序的可扩展性。
重构后:使用了类间的继承,将多个类和函数解耦合,努力实现低耦合、高内聚,并将程序模块化处理,有利于程序的扩展性。
重构前后的类图如下。
可以看出,重构前的代码逻辑较为简单,关注点较为单一,程序的扩展性不高,一个类中往往高度聚合了各种方法,使得一个类于该程序高度相关,可移植性很低。
重构之后,各类之间的层次性较为明显,各个类各司其职,继承关系的类属于一个单元,模块化的思想提高了程序各个模块的可移植性,降低了各个模块之间的聚合度。
心得体会
通过本次的作业设计,能够对面向对象的思想有一个整体上的,较为清晰的认识,从陌生到认知再到熟悉是一个循序渐进的过程,在这样一个过程期间还要不断地进行练习,不断地试错并改正,不断地学习借鉴他人的编程思想。OO作业的难度相对较大,这对于我来说是一项不小的任务,在编程练习的过程中曾一度不知该如何下手,一方面需要自己努力的去思考,调用自己曾经学过的思想、另一方面也要多阅读指导书的提示部分、多关注讨论区使得我们能够更清楚的了解题目,对题目有更清晰的认识。在编程之前首先要搞清楚需求是什么,紧接着最重要的是能够选择一种合适的方法去处理题目,往往架构要比写代码重要得多!!!在架构思想完成之后,代码的编写和bug的调试就会轻松很多。