OO第一单元总结博客
OO第一单元总结博客
一、程序结构分析
(一)第一次作业
第一次作业是多项式求导,给定符合形式化表述规则的多项式,输出符合规定格式的导数。在本次作业中,我按照形式化表述中规定的格式要求,写了四个类Term,Factor,Expression,Main。
下图为本次作业的uml图:

1.Main类
这个类的功能比较简单,就是读取输入字符串,去掉空白项,调用Expression的构造方法生成表达式,并且调用Derivative方法进行求导,最后调用toString方法输出。
2. Expression类
此类,相当于表达式类,存放着一些与表达式相关的操作方法。本人一开始用Treemap来存放表达式中的项,键值对中键表示指数,幂表示次数。通过写出识别项的正则表达式来识别每一个项。每一个项之间的关系默认为加(如果表达式符号或者连接项之间的符号为-,则把该项系数取相反数反即可。
这样做的缺点是项必须能通过因子合并化简成A * x ** B的简单形式,也就是说,因子必须为常数因子或者幂。如果因子出现奇奇怪怪的东西就需要重构,可拓展性不强。
构造表达式的时候,首先读首字符,如果为[+-]就将其标记为表达式固有符号(默认为正)。之后调用Term的构造方法获取一个Term,根据表达式固有符号调整系数。之后进入循环。循环内容是当字符串还没有扫描完毕的时候,先扫描一个[+-]并且记录,之后调用Term的构造方法获取一个Term,根据之前读到的项间符号调整系数,如果Treemap中有指数相同的项就进行合并。读完后删除系数为0的项。
求导的时候,对Treemap中每一个项进行求导,并且将新生成的Term放到新的Treemap中。
3. Term类
这一类有两个字段,一个是指数,一个是系数。这么干是因为本次任务的因子只包含常数因子或者幂,因此Term可以进行合并。
构造项的时候,首先读首字符,如果为[+-]就将其标记为项固有符号(默认为正)。之后调用Factor的构造方法来获取一个因子,并根据项固有符号调整系数。之后进入循环。循环内容是当字符串还没有扫描完毕的时候,先读一个字符,判定是不是*。若是则读入一个Factor,若否则退出循环。之后将新读入的Factor按照指数加指数,系数成系数的方式进行合并。
求导的时候,按照(A*x**B)' = A*B*x**(B-1)的方式生成新的项。
4. Factor类
这一类有两个字段,一个是指数,一个是系数。如果读到幂的话就默认系数为1,指数为相应值;读到常数因子的时候,就需要默认指数为0,系数为常数。
5. 结构分析
下图为每一个方法的度量图。
| Method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| Term.toString() | 12.0 | 8.0 | 8.0 | 8.0 |
| Term.Term(String) | 6.0 | 1.0 | 4.0 | 4.0 |
| Term.Term(BigInteger,BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
| Term.setCoe(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
| Term.getExp() | 0.0 | 1.0 | 1.0 | 1.0 |
| Term.getCoe() | 0.0 | 1.0 | 1.0 | 1.0 |
| Term.derivative() | 2.0 | 2.0 | 2.0 | 2.0 |
| Main.main(String[]) | 0.0 | 1.0 | 1.0 | 1.0 |
| Factor.getExp() | 0.0 | 1.0 | 1.0 | 1.0 |
| Factor.getCoe() | 0.0 | 1.0 | 1.0 | 1.0 |
| Factor.Factor(String) | 6.0 | 1.0 | 4.0 | 4.0 |
| Expression.toString() | 9.0 | 2.0 | 5.0 | 5.0 |
| Expression.getExpToTerm() | 0.0 | 1.0 | 1.0 | 1.0 |
| Expression.Expression(String) | 21.0 | 4.0 | 9.0 | 9.0 |
| Expression.Expression() | 0.0 | 1.0 | 1.0 | 1.0 |
| Expression.derivative() | 4.0 | 1.0 | 3.0 | 3.0 |
| Total | 60.0 | 28.0 | 44.0 | 44.0 |
| Average | 3.75 | 1.75 | 2.75 | 2.75 |
本次实验中度量标红的部分是Expression.Expression(String),和Term.toString()。前者的原因是在构造方法里面加入了大量的逻辑,后者的原因是使用了大量分支语句分类讨论进行优化。
本次设计的优点是思路清晰,一个一个将项、因子进行读入,同时方便进行格式检查(应该出现的东西没有出现就是了)。缺点是可拓展性不太强。此外还有一个事情是边生成表达式(求导)边合并同类项,直接在项的字段里改系数,容易出现隐患,此隐患在第三周作业中出现了,后文会讲。这启示我们分离各个功能,不要耦合在一起。
(二)第二次作业
第二次作业在第一次作业基础上加上了表达式因子,sin(x), cos(x)。
本次作业,我犯蠢了,不太清楚自己当时什么脑回路,竟然在因子数量增加的前提下,把Factor类干掉了。这就导致我需要在Term类里进行全部因子的协调,合并,分别进行求导等等。这导致的一个后果是Term类的总代码长度多达360行。此外还有,我在Term类里进行了大量重复的代码复制粘贴操作,导致我总是出现sin和cos互相混淆的问题,调试了好久才结束。
为了直观显示Term类多复杂,直接上图:

1. BasicMethods接口
这个接口打算定义一些基本方法的,这次实验定义了getExpressionInBrackets,相当于给定一个以左括号开头的字符串,能够输出首字符左括号与同一级右括号之间的子串。
2. Main类
和第一次作业相同。
3. Expression类
由于本次实验的因子比较复杂多样,因此无法将项简化成为系数-指数的对应关系,因此,直接用Arraylist来存储Term类。
生成Expression类的方法也和上一次实验相同,先读取表达式固有符号,之后读取一个项,进入循环。在循环中,先读取一个符号,然后调用Term的构造方法读取一个项,并且将其加入到termArraylist中(如果新加入的term与容器中原先的一个除了系数之外均相同的话,直接改系数就好了(此处合并时该系数有隐患,在后续实验中也确实因此造成了bug)。
4. Term类
说过了,我因为犯蠢将Factor类给干掉了。所以Term类将干所有和Factor相关的事情。
首先Term类新增了几个字段,比较重要的有sinList, cosList, expressionList。这三个字段都是Hashmap,键相当于表达式,值相当于指数(在sinList和cosList中键相当于x,不过本次实验我想得远了一些,就直接默认三角函数因子括号里面是表达式了)。原有字段的系数指数意义有变,系数相当于所有常数因子乘积,指数相当于所有幂的指数相加。
其次是新添了好几个与之相关的方法,比如addSinList,addCosList,addExpressionList等,表示把相应的因子加入到hashmap中(如果键相同就直接改值了)。
在toString方法中,先输出coe*x**exp,之后将每一个三角函数和表达式因子输出。在输出括号中的表达式过程中,先输出左括号,再递归调用括号内表达式的toString方法。
5. 结构分析
下图是被标红方法的度量。
| Methods | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| Term.toString() | 31.0 | 2.0 | 16.0 | 21.0 |
| Term.matchFactor(String) | 18.0 | 10.0 | 10.0 | 10.0 |
| Expression.Expression(String) | 19.0 | 1.0 | 10.0 | 10.0 |
| Expression.equals(Object) | 10.0 | 6.0 | 4.0 | 6.0 |
这是类的度量:
| class | OCavg | OCmax | WMC |
|---|---|---|---|
| Expression | 3.111111111111111 | 8.0 | 28.0 |
| Main | 1.0 | 1.0 | 1.0 |
| Term | 3.9473684210526314 | 19.0 | 75.0 |
| Total | 104.0 | ||
| Average | 3.586206896551724 | 9.333333333333334 | 34.666666666666664 |
类里被标红的有term和Expression。
本次出现标红的主要原因是Term类太过繁杂,因此需要大量分类讨论。
本次实验代码的缺点是:Term结构太过复杂,如果要加入新的因子,要改其中很多种方法。内部各种方法的调用关系即为复杂。
优点是:读入字符串生成表达式的过程不会消耗大量时间空间,同时方便进行格式检查。
(三)第三次作业
第三次作业在第二次作业的基础上加入了sin(Factor), cos(Factor)和格式检查任务。
本次作业吸取了第二次架构的教训,进行重构。在第一次作业架构的基础上加入了Factor类的子类NumFactor, CosFactor, PowerFactor, CosFactor, ExpressionFactor。将Factor类改为抽象类。主要贴合形式化表达。
读入表达式的时候与之前逻辑类似,格式检查主要判定应该读取某些东西却没有读出来,或者按照形式化表达规则读取完成后字符串还有东西。
下图是类的uml图:

相对于第二次作业改进的地方是重新将Factor抽离出来,并且按照不同因子设立不同的Factor的子类。在Term这个需要用因子的地方直接用Factor类,相当于是多态。
1. Main类
由于进行格式检查,所以就直接将原字符串导入到Expression方法中,并且捕捉IllegalArgumentException异常。
2. Expression类
与之前逻辑相同,先读取表达式固有符号(非空白字符的首字符不是正负号的话跳过这一步),之后读取一个项,进入循环。在循环中,先读取首个非空白字符,如果符号不是正负号或者没有非空白字符,就当做字符串读取完毕,跳出循环。然后调用Term的构造方法读取一个项,并且将其加入到termArraylist中。在读取过程中,捕捉生成Term中抛出的异常,并且如果在读取完毕后还有东西,也抛出异常。
此外,termArraylist内所有term关系均为加。我们需要利用项间符号和表达式固有符号修改新生成的项的系数。
如果新生成的term和容器中某一个term除系数外都相同,就改容器内term的coe字段来进行合并。
3. Term类
与之前逻辑相同,首先读首个非空白字符,如果为[+-]就将其标记为项固有符号(默认为正)。之后调用Factor的getFactor方法来获取一个因子,并根据项固有符号调整系数。如果获取因子返回null的话就抛出异常。之后进入循环。循环内容是当字符串还没有扫描完毕的时候,先读一个字符,判定是不是*。若是则读入一个Factor(若新构造的Factor为null则抛出异常),若否则退出循环。
Term类用factorArraylist来存储特殊因子(表达式因子,三角因子),coe和exp字段分别表示常数因子乘积和幂因子的指数之和。
4. Factor类
其中的getFactor方法用来生成一个因子。内部逻辑是根据不同的因子特征生成不同类型的因子,如果所遇到的字符串首端不满足所有因子的特征则返回null。
Factor的字段包括type和factorlength。type标记因子种类,factorlength标记该因子占用原字符串中子串长度,方便后续返回后继续读取字符串。
5. NumFactor, PowerFactor, CosFactor, SinFactor, ExpressionFactor类
他们是Factor类的子类,表示不同的因子,有不同的字段,并且有各自的求导方法。
6. 结构分析
下图是被标红的方法度量。
| Methods | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| Expression.equals(Object) | 10.0 | 6.0 | 4.0 | 6.0 |
| Factor.getFactor(String) | 10.0 | 6.0 | 7.0 | 8.0 |
| Factor.getSpecialFactor(String) | 9.0 | 6.0 | 3.0 | 8.0 |
| Term.addFactor(Factor) | 35.0 | 14.0 | 15.0 | 15.0 |
| Term.deleteFactorNotExist() | 16.0 | 1.0 | 8.0 | 8.0 |
| Term.sameFactorList(Term) | 4.0 | 4.0 | 2.0 | 4.0 |
| Term.Term(String) | 7.0 | 4.0 | 4.0 | 6.0 |
| Term.toString() | 21.0 | 2.0 | 13.0 | 13.0 |
可以看出,有好多种方法,我都是通过factor的种类进行分类讨论从而使结构复杂,比如:Term.addFactor(Factor), Term.deleteFactorNotExist()。因此,此处不妨提出优化,在Factor字段中加入exp,毕竟他的四个子类都有exp字段,共同提取出来可以减少复杂度,到时候改指数就不用分类讨论了。
总结一下本次实验的优缺点:
优点:
- 相对于第二次实验重构进行优化,使某一个类的逻辑那么臃肿
- 逻辑结构更加清晰
缺点:
- Factor可以在各个子类之间增加二级子类,或者抽取共同的字段,避免大量分类讨论,注意多态的使用
- 优化和求导、构造互相耦合,容易产生bug(确实产生了)。
二、bug相关
(一)第一次作业
1. 调试期间
本次作业在调试期间主要有一个bug:coe和exp的类型开小了。
2. 强测期间
本次强测没有被发现bug。
3. 互测期间
本次互测被找出了一个bug。有人利用+1-1循环数百次将我的程序进行了爆栈。那是因为我用了一个超大正则表达式来进行匹配。事实上,我从第一次实验开始就进行了逐项匹配,因此没必要进行全局匹配。此次bug出现在Expression.Expression(String)方法,确实是一个有高圈复杂度的方法。不过此bug倒是容易被定位。
(二)第二次作业
1. 调试期间
在调试期间出现的bug主要是因为代码大量重复出现导致我进行了不恰当的复制粘贴,将sin和cos进行混淆。此bug出现在term类。这也启示我们要将代码模块化,高内聚、低耦合。
2. 强测期间
本次强测没有被发现bug。
3. 互测期间
此次互测被找出来一个bug。我是用hashmap来存储表达式因子的。因此我其实支持表达式因子的幂运算。但是我输出依然支持了表达式的幂运算。此次bug出现在Term.Term(String)方法中,是一个高圈复杂度的方法,但是容易定位。
(三)第三次作业
1. 调试期间
在调试期间,之前讲过,我求导和优化耦合在一起。因此,在求导过程中,我直接修改了某些Factor对象的指数,因此,在某一项进行求导后,本来的内容也发生改变。在某些需要调用原函数的场景中就会出现bug(比如乘法的求导法则)。这个bug改正的方法是将factor进行复制。或者在求导过程中不进行优化,在求导之后进行。或者是将Factor的字段设定为final属性,不过优化会更加占空间。
此bug主要出现在Term.addFactor(Factor)方法中,是一个圈复杂度比较高的方法,而且不容易定位,需要我们格外小心。
2. 强测期间
本次强测发现一个bug。在判定指数绝对值是否越界的时候,我将指数进行强制类型转化,结果恰巧碰见了一个强制转化后在范围内的样例,没查出格式错误。这启示我们,不要随便用强制类型转化,因为强制转化可能会改变变量本身的值。
此次bug出现在Factor.getFactor(String)和Factor.getSpecialFactor(String)处,属于圈复杂度比较高的方法,但是容易定位。
3. 互测期间
本次互测没有被发现bug。
三、互测相关
三次实验中,我比较懒惰,没有使用观察法研究屋内成员的逻辑漏洞,而是使用了黑盒测试,利用评测机。
这三次实验我忘记在实现评测机的过程中加入输出字符串的格式检查,只判定了计算结果的正确性。因此,第三次作业的一个同学的bug,以及第二次作业我关于表达式支持幂运算的bug均没有被自己的评测机发现。
生成样例点中,主要方法是根据形式化表述生成大量随机样例。辅助方法是写一些有坑点的表达式因子,比如x-x, +1-1+1-1+1-sin(x)**2-cos(x)**2……
在第一次互测中,实际上有爆栈可能性,但我没有进行压力测试,导致没有用评测机查出自己的问题。
三次互测启示我们:要考虑各种作业细节;要进行格式检查和压力测试;要把代码写好看点,符合规范,要不然别人也不会愿意仔细看的。
四、心得体会
这三次作业中,第一次作业比较简单,用的类比较少;第二次作业应该是践行面向对象的好时机,但是我错误了,用了一个耦合性非常强的方法;我在第三次作业才开始注意这些问题。在写这几次作业的过程中,我也确实遇到了一些困难,走了一些弯路,踩了一些坑,不过我通过研讨课和不断的推进看见了面向对象的影子,也学习到了如何发现自己程序中的问题(观察法通常很难发现自己的问题),还学习到了深浅复制,工厂模式相关知识。
我很菜,还需要继续努力学习啊!
浙公网安备 33010602011771号