382
社区成员
写在前面:本人代码能力基础有限,逻辑思维能力不是很强,因此可能一些表述比较模糊或不够严谨,如有错误欢迎指正,本人希望从错误中学到更多。
ps: 表达式解析的三次作业采取增量开发的形式,因此只取最后一次作业的要求进行作业要求展示
基本要求:将按照形式化定义的表达式进行化简展开,包括表达式括号展开(项与表达式、表达式与表达式相乘、高次幂表达式展开)、自定义函数代入、求导计算等。
进阶要求(性能提升):
{} 表示允许存在 0 个、1 个或多个。
[] 表示允许存在 0 个或 1 个。
() 内的运算拥有更高优先级,类似数学中的括号。
| 表示在多个之中选择一个。
上述表述中使用单引号包裹的串表示字符串字面量,如 '(' 表示字符 (
1 //自定义函数个数
f(x,z)=(x+z)**2 -(x- z)**2
dx((sin((x+sin(f((2* x-x),y))*(1-sin((4*x*y))+x**2)-cos((2*x*(2*y)) )**2))))+sin(x)*cos(x)**+000
以下对三次作业作递进式分析。
前两次作业因为架构不好、易出bug,所以只简略分析结构思路,注重分析架构不断完善的过程、思路的演进、犯的一些错误,以及从bug修复角度分析不足,作为经验教训记录于此。而第三次架构较为成熟,因此将对照类图详细解释每个类以及其中属性方法的设计考虑,注重最终结果。
第一次作业难度低,但由于是白手起家,因此花费了不少时间和精力。并且,最大的敌人是死脑筋。“没有想象力是罪魁”
从头开始看作业,我认为作业可分为表达式“解析”“展开”和“输出”三个关系密切的过程。“解析”意味着以何种数据结构“存储”(或者说,“暂存”)所输入的表达式,而“展开”意味着如何进行去括号、合并同类项等计算。“输出”即是字面意义上输出展开后的表达式(但其实有很多性能优化的过程也可在输出完成)
层次分析:根据形式化定义,表达式的本质是多个项,项的本质是多个因子;而表达式又可以是因子存在于项中,由此看出“递归下降”在本次任务中的体现,引出Lexer/Parser——文法/词法解析器的方法;以及层次化解析表达式的必要性,由此引出我们课程的核心思想——面向对象的思想(将表达式、项、因子分成三层次的对象,而每个层次也可根据性质分成不同的具体对象表示)。
(有个PowerF,其实是Power Function幂函数)
由类图就可以看出,我第一次的架构其实是很乱的,尽管只有500多行的常规代码量,但是里面的逻辑关系我没有梳理的很清楚。(回过头看我还花了很多时间理解自己的代码……)
数据结构上犯的错:我那时的想法是这样的:因为作业指导书建议去用表达式树解析,于是我联想到当初做后缀表达式时用到的二叉树,结合这次乘法加减法都是二元运算符,我打算依然用二叉树解析表达式。于是我按照树的形状,每个父结点有左右子结点,结点用ExprNode这个容器表示,这个容器里有个接口ExprItem表示这个结点的实际意义。如果该ExprItem实现为AS(add/sub),则表示表达式中间的加减运算符;如果该ExprItem实现为Term,则表示项。当时我只想着搭框架,生硬地套表达式树的形式,没有想清楚这个二叉树意义何在,就这样那个星期我还没有想通建树跟另一种想法——将表达式表示成List<Term>——这种思路有什么区别。这个二叉树建起来有什么意义?因为做到表达式之间相乘的时候,肯定是List与List进行项项合并更符合直觉,所以我甚至还用递归写了树与List相互转换的函数,只为了能表现出二叉树这种形式……显然,我犯了形式大于内容的错误。
递归下降解析:递归下降的指导精神在三次作业中一直延续,不过具体实现是在不断进化的。第一次作业参考了第一次训练中课程组提供的代码。简单来说过程是这样的:
(Term 继承 了 ArrayList<Factor>)
| Term |
| ---- |
| 因子 |
| 因子 |
| ... |
“输出”时,是表达式二叉树输出的常规逻辑,中序遍历树的结点, 输出ExprNode.toString(即“容器”ExprNode)。并且当时因为没有意识到最简项类(SimpTerm)的强大功能,我直到输出环节才引入了一个临时变量ArrayList<SimpTerm>进行化简,存储化简结果。而输出的时候化简,已经迟了。
关于SimpTerm:这正是类似于大家熟知的Poly-Mono架构的雏形。当时我没研究讨论区,自己给最简项取了个名字(simplified term--SimpTerm)。这个SimpTerm(相当于Poly)恰恰就是后两次作业处理问题的关键。
表达式展开:由于在数据结构层面没有想清楚,第一次作业的表达式相乘、幂展开过程过于繁琐,始终在维护树的结构,在做深克隆、替换结点、List-Node转换等操作,此处多做失败操作的赘述意义不大。第一次作业的bug就是在这里出的锅。
正负号解析:第一次作业的正负号解析逻辑也很乱。当时把正负号分成了两类,一类是表达式、项的前导正负号(leadingAS),作为ExprNode类和Term类的属性。一类是表达式中间的运算正负号(AS),单独作为ExprItem的实现存在。事实上这样做把逻辑复杂化了。重构时将这个问题梳理清楚。
如此麻烦。幸亏第一次作业比较简单,弱测中测勉强过了;强测和互测被找出了bug。
第一次作业采用二叉树结构,并且表达式相乘时我采用的方法是,将两棵表达式树每个结点两两合并,产生新子树,并且在相乘时没有合并同类项!,是在输出时合并的,结果就是,深度为m的子树1与深度为n的子树2相乘会形成深度为m*n新子树3,在表达式的幂次稍微有点高时,这个新子树最终就很深很深很深很深……而中序遍历是递归操作,一个简单的遍历函数面对很深的树会调用自己很多次,这种调用通过函数栈实现,因此强测及互测就出现了StackOverflow Error的bug,java的函数栈爆了。
(正如StackOverflow网站名字被寄予的美好寓意,这个error给了我重构的信心)
我这次作业的bug修复只是打补丁,具体操作对于作业的进展没有任何实质上的意义,单纯为了补分,这里也不做记录。真正意义上的bug修复从以下的重构开始。
在做HW2之前,我花了一段时间对HW1进行重构,理清了思路,最终形成了相对稳定好用的、逻辑清晰的、十分有利于后续迭代开发的新一套架构,沿用到了后两次作业中。
刚刚的HW1中我总结了:
以下我的重构逐一攻破了这些问题。
(HW1重构后的类图与HW2相似,可以参考 HW2类图)
重构的最大改变来自思想转变,我意识到,树的本质就是组织数据的形式。而之前具体意义上类似于List<Factor>的想法,其实就类似于在抽象意义上的多叉树!数据结构为解决问题服务,需要理解问题的需求,再选择出最合适的数据结构。我由此意识到,这个问题在我的直觉上更适合用多叉树的形式去解析表达式(我本人没想出二叉树对解决这个问题有什么用,以及如何用二叉树比较优雅地解决这个问题。如有好的想法欢迎在评论区加以指导!)。于是我推倒重来,将所有要素从命名到实现进行结点化,梳理出了一个结构更清晰的表达式多叉树。
摆脱了二叉树以及所谓“容器”以后,AddNode意义变成了连加,直接管理一个表达式(因子)整体。表达式本质就是连加;MultNode的意义变成了连乘,直接管理一个项整体。项的本质就是连乘。
其中根据类图可知,从表达式到项,再到因子,都摆脱了“容器”的束缚,去实现了ExprItem表达式成员接口,其本身作为表达式树的一员存在(ExprItem表示表达式的所有成员)。ExprNode也实现了ExprItem,配备ArrayList<Expritem>(即子结点是一个个表达式成员),在此基础上赋予了抽象类实现--表达式成员管理者的身份,不再以“容器”的形式具体存在,而成了AddNode、MultNode们的灵魂。在这种结构中,所有的叶节点都是常数因子和变量因子,上面的组织结点均由ExprNode实现。(而所谓ExprNode,其实是最终结构中抽象出的“算子”雏形,仅仅是雏形)
这个结构相比于HW1的结构,更符合直觉,也更有利于之后操作的进行了。
不过这里依然犯了两个错误,直到HW3才改过来:
以上数据结构的构建也伴随着我对SimpTerm(simplified term 最简化了的项,即Poly单项式)的再思考。最终,结合SimpTerms类的实现,我把SimpTerm的地位从一个临时变量提升到了ExprNode的属性的地位。
相信了解过这次OO作业的童鞋都或多或少知道,表达式解析问题最终可以归结为很多最简项、单项式Poly--ax^b^y^c^z^d^的相加,在我的作业中我把这个东西命名为了SimpTerm。
public class SimpTerm{
int xnum; //x的次数,无x则为0,yz等同
int ynum;
int znum;
BigInteger coef; //系数
//...
}
相加后的结果形成多项式Mono,我将Mono实现为一个继承了ArrayList<SimpTerm>的SimpTerms类。每个ExprNode都具有一个SimpTerms属性。相应的,ExprNode有一个collectSimpTerms的抽象方法来从子节点中提取出自己的SimpTerms,由AddNode、MultNode具体实现。
public class SimpTerms extends ArrayList<SimpTerm>{
public SimpTerms mult(SimpTerms o){
//实现SimpTerms与SimpTerms相乘...
}
@Override
public boolean add(SimpTerm o){
//加入一个SimpTerm...
}
@Override
public boolean addAll(Collection<? extends SimpTerm> c){
//将另一个SimpTerms所有SimpTerm整合进自身...
}
}
最终,表达式展开和合并同类项均归结到了SimpTerm和SimpTerms的操作中。
处理流程:
处理时,由AddNode和MultNode的collectSimpTerms方法交错进行,体现了面向对象的特点。下面的分析过程也有点面向对象。
MultNode:collectSimpTerms():开始时,在递归下降的结构中,首先得到的是parser为我们parse的各个不同的因子,在我的数据结构中,我把它们先放进了MultNode的children子节点列表中。当一个项的所有因子解析完毕并放进了MultNode后,MultNode调用其实现的ExprNode的collectSimpTerms抽象方法。
在MultNode中还有tmpSimpTerm和tmpExpr两个属性。
SimpTerm tmpSimpterm;
SimpTerms tmpExpr;
遍历子节点时,遇到常数和幂函数直接整合进tmpSimpTerm;而遇到表达式AddNode时,如果tmpExpr为null(之前未遇到过表达式),则直接让tmpExpr指向AddNode的SimpTerms,否则将新的AddNode中的SimpTerms和暂存的tmpExpr进行相乘(表达式与表达式的相乘,调用tmpExpr.mult()方法),结束后,将tmpSimpTerm和tmpExpr做项与表达式的相乘,得到最终该MultNode的SimpTerm。在具体实现方面还有一些细节的处理,此处不做赘述。
AddNode:collectSimpTerms() AddNode下接MultNode,它所做的就是简单的将所有自己所有MultNode的SimpTerms合并到一块(多个List双遍历),形成一个新的完整的SimpTerms放进自身。在此期间完成合并同类项的操作。为了合并同类项,我重写了SimpTerms的add和addAll方法,不重复添加相同的SimpTerm。同时,也需要重写SimpTerm的equals方法。这个equals需要分为带系数和不带系数的两种,分别用于项项相加和后面要用到的三角函数因子判断。在此基础上的合并同类项就比较简单了,具体实现不做赘述。
AddNode:powerExpand() 我们的表达式还可能有高次幂,需要幂展开。在这个架构中,只需要将AddNode执行完collectSimpTerms以后,自己深克隆出一份自己的SimpTerms
SimpTerms cloned = this.getSimpTerms().deepClone();
然后将this.simpTerms
和cloned.simpTerms做幂次次数的表达式与表达式相乘操作即可。
需要注意的一点是,在做各种相乘操作时要判断是否需要深克隆的问题,涉及许多细节。(当然,很多拿不准的地方是可以无脑深克隆的,反正java有其垃圾回收机制……)
由此,我们就完成了作业的==核心任务==,针不戳。
由上面的分析也可以看到,我将AS类换为了仅代表连加的AddNode类。那么重构以后减号去哪里了呢?我将之前减号的静态存储结构转化为了变号这一动态分析。
根据作业要求,表达式的前导正负号代表着第一项的正负,因此一个表达式如果是“负”的,那么就让表达式的第一项变号(方法swicthAS())。项的前导正负号则意味着项内每个因子都要变号(所有因子乘-1),因此遍历项(即MultNode)的属性SimpTerms内的所有SimpTerm,让他们的系数coef变号即可。如此就完成了减号的处理。
对于加号,事实上不用处理,表达式中的所有加号的意义都蕴含在了AddNode结点或每个SimpTerm的coef中。(至于指数前面的那个+嘛,太微不足道了,直接忽略=.=)
重构以后我们在解析过程中就化简完成了,不存在输出才化简导致爆长度爆栈等错误。重写SimpTerm:toString,输出时遍历头AddNode的SimpTerms中的SimpTerms,依次输出即可。
ps.关于正负号有个可以小小优化的地方,即SimpTerms中可以按照SimpTerm的coef从大到小进行排序,就可能可以优化一个长度——当第一个SimpTerm系数小于0的情况。
重构完成,我当时重构完真是激动不已,对HW2信心满满,然而上面说过的错误或多或少导致了HW2不尽人意的结果。
↑ 有些令人迷惑的名句引用=.=。其实想表达的意思是,在思维上把抽象的顶层设计做好了以后,才能在具体写代码时从容不迫,甚至不拘小节,因为好的架构避免很多不好架构隐藏的不起眼的bug。这一思想从重建后的HW2尤其是bug部分充分体现了出来。
三角函数:三角函数因子可以分为两部分,三角标识符(sin/cos)以及内部因子。
自定义函数:本质上是参数替换,即函数定义式中的形参xyz替换为函数调用中的因子们。这种对应关系我们很容易联想到HashMap,我也是用这个容器实现的。
三角函数处理:这个比较简单,在SimpTerm中新增关于三角函数的List存储各个三角函数即可。
出于合并同类项的需要,以及封装性的考虑,SimpTerm中的三角函数List我采用内部类来实现。
private class TrigFNodes extends ArrayList<TrigFNode>
(内部类这个问题其实我不太吃的准。它的好处是维护了外部类的封装性,但是坏处是增加了外部类的复杂度,我的TrigFNodes在外部类文件中占了60行代码。若外部类所需的功能不多、单独写一个文件不爽的内部类更多,代码量会更大。我仍不清楚如何定位内部类的作用,欢迎在评论区指点迷津)
TrigFNodes继承了ArrayList<TrigFNode>,主要是为了重写ArrayList中的add和addAll这两个方法,以达到判断相等,继而合并同类项的作用。(还有一个深克隆方法,是为了SimpTerm的深克隆而实现的。)
接下来聚焦三角函数因子本身(TrigFNode类)。除了体现本身的属性之外,为了合并同类项和项项相乘,需要重写三角函数因子的equals方法。而这进一步引发了一个需求:需要实现因子的equals方法。因为之前合并同类项的相等判断是在SimpTerm层次上实现的,只需要判断系数和各个变量的指数;而三角函数就需要同时判断三角函数名以及内部因子了。
还有一个注意点,三角函数应该有两个"equals"方法,一个equals()带指数,用于合并同类项。另一个我命名为varEquals(),不带指数,用于项项相乘。
还有一个小问题。
这时候,当成表达式解析的优势体现出来了:判断因子相等时只需判断内部表达式的Simpterms是否相等即可,不需要重写其他因子的equals方法。
并且我尝试过当成表达式解析。为啥我还是选择因子解析呢?其实可能是出现了一些失误,当时在哪里卡住了我已经忘记了。。。下面分析一下这两个方法吧。
emm,当时是因为形式化定义的限制,如果三角函数内是表达式外边就得有两层括号,sin(x+y)? 不行! sin((x+y))? 行! 如果要提升性能把sin((x))这种不必要的括号去掉,拆括号的操作就需要增加能否拆括号判断,即判断三角函数的表达式是否只含有一个常数或变量因子。这个可以给三角函数加一个boolean属性,在解析该三角函数的时候分析保存起来,输出时看一下这个属性即可。
而在作业要求中,当成表达式解析和当成因子解析,两者的不同恰恰就在于拆括号这个操作。表达式解析是上述操作;因子解析则需要
这样三步走。逻辑似乎更加复杂。所以正在写文档的我也不太想得起来当时为啥选择按因子解析了。我只记得一个原因是,尊重并遵守形式化定义中,保持三角函数内部是一个“因子”…… 乐。总之这两种方法都是适用的,不知道大佬们对这个问题有啥独到见解不。
自定义函数实现:我采取了类似于工厂的方法实现自定义函数(Func类工厂)。
Func工厂的自定义函数制作流程是:
HashMap<Character, Func> funcMap
。之后,常规用parseExpr读入表达式,存进刚刚新建的Func对象,称为模板表达式ExprNode template
。同时也要保存模板表达式相应的形参有序序列ArrayList<Character> virSeq
。类似于支持重载的函数需要函数名和形参变量共同定义一个函数。ArrayList<ExprItem> actSeq
),根据顺序与工厂中已保存的形参有序序列一一对应,得到一个HashMap<Character, ExprItem> valueMap
。之后在工厂的模板表达式上做参数替换得到新的表达式,这样自定义函数的调用就完成了,得到了替换以后的表达式。不过在实现时遇到了挫折——没有想清楚SimpTerms、Children与ExprNode的关联关系。
我用UML类图的关系表述表达这个思路错误:开始我只设想Children与ExprNode是组合关系(一种强关联关系),两者的生命周期是相同的,没有利用模板表达式的SimpTerm,而必须要通过子节点替换以后父节点的collectSimpTerms才能在父节点中形成ExprNode的SimpTerm,如此自下而上一层层地向上收集。
在这种思路指导下我竟然折腾了五六小时还没写好。因为这样就需要从最底层开始搭建新的表达式,由此需要递归遍历整个表达式多叉树,以及为每个成员写对应的深克隆。而递归遍历与自下而上搭建表达式树、边解析边计算这两个过程是同时进行的,两者通过函数结合起来有些困难;并且要替换的是原树中的幂函数,此处指数的处理也有些麻烦……当时遇到了很多问题,此处不再赘述。
后来我转变了想法,注意到Children对象和ExprNode的功能实际上只存在一种弱关联的关系,ExprNode脱离了Children也能正常实现其功能,它的物理意义是通过SimpTerms体现的,也就是说,是SimpTerms与ExprNode形成组合关系,而非Children。参数替换依赖物理意义才能实现。
在这种思路指导下,我为SimpTerm类和AddNode类写了nodeChange方法。
SimpTerm类的这个方法中,只要根据template的xnum, ynum, znum就可以确定各个实参因子的个数。并且一个SimpTerm用一个MultNode组织,再配合AddNode的功能就可以新建出一个参数替换后的自定义函数表达式,最后把这个表达式填入所解析到的因子位置即可。而不管是template还是最后我们得到的表达式,真正有用的只是它的SimpTerms,所有Children都是用完即弃,存储可有可无的。返回值为这个MultNode。
AddNode类的nodeChange方法,就是遍历SimpTerms调用每个SimpTerm的nodeChange,收集MultNode为己所用。由此实现了参数替换。
自定义函数中的三角函数:如果三角函数内存的是因子,就需要分类型写nodeChange方法(三角函数嵌套则涉及递归),如果存的是表达式,那么就可以直接调用上面写的SimpTerm类的nodeChange方法。(所以如果再给我一次机会,我会再尝试一下按照表达式形式存储三角函数因子的做法,当时究竟出什么错了呢……)
综上,HW2的两个新增需求的实现完成了。
可以吸取的一个教训是,要正视我们所建立的每个对象、对象中的每个属性的物理意义,并分析好对象与对象,对象与自己属性的关联强度究竟是怎样的,这对于我们实现功能、完善结构等都是很重要的
一共产生了5个bug,我提交了5次合并修复,并且前三个bug一共只修了4行,足以看出我的粗心,以及架构的不够完备。
总的来说,HW2体现的是抽象、思想层面我考虑不周,导致了写代码时的挫折以及最终的一些小bug。因此,只有大节不亏,才有小节不拘的余地啊。
HW3的架构最终让自己心满意足,但还是稍有遗憾。
面对求导,我最终下了小重构的决心:从物理意义上分清因子(Interface FactorNode)和算子(abstract class ExprNode),摒弃只有数据结构形式意义的ExprItem接口。因子重内容。算子重计算,可以操作SimpTerms。这样,NumberNode、PowerFNode、TrigFNode实现FactorNode接口,MultNode和DrvNode(求导算子)实现ExprNode抽象类,AddNode两者都实现。同时,因为摒弃了ExprItem,我将三种算子的子节点属性从ExprNode分离出来放入子类,具体化了:AddNode with ArrayList<ExprNode>, MultNode with ArrayList<FactorNode>, DrvNode with AddNode(单元运算符)。这样一来,结构就清晰明了了,并且增强了程序的可扩展性。
从结构不断重构的过程中,我体会到上课时老师所讲的物理意义分析对于抽象是多么地重要。千万不能形式重于内容!!!否则就会有我架构中ExprItem这种不伦不类的抽象出现。
还有一个小重构可以提一嘴,因为HW3施加充裕,为了提升可扩展性,我将SimpTerm中的xnum, ynum, znum变成了用HashMap组织的变量集合,这个集合我也用了一个内部类BaseNums表示,并实现了一些方法。
private class BaseNums extends HashMap<Character, Integer>
在我的架构下,求导操作的实现就轻松写意了。DrvNode也实现了collectSimpTerms方法,拿它的AddNode子节点中的SimpTerms作分析。
我的操作比较直接,像解析表达式一样,回到原始的算子收集因子过程。SimpTerm实现了求导方法,表现为乘法法则productsRule()
。用MultNode组织,对于其中幂函数xyz, 如x^2^, 就给MultNode下接一个“2”的NumberNode,一个x**1的PowerFNode。而对于三角函数,则用到链式法则。三角函数求导有三步:dx(sin(f(x))**n), dx(sin(f(x))), dx(f(x)),也将每个求导结果放进相应的MultNode中。这就又引发一个需求,由于三角函数按因子存储,所以还要给每个因子实现求导方法。(如果按表达式存储或许就没有这么麻烦?没试过。)其中还有不涉及求导的参数,这些处理就不赘述,实现的方法有很多种。
互测强测都没出bug,耶耶耶!
到此,我的迭代开发之旅就结束了。接下来就到了下一重头戏,结合类图对最终我的结构进行分析。
主要分析见类图。这个类图类似于流程图的从上而下的关系,我认为是因为,一个程序是同时具有对象和过程的“二态性”的,只不过在面向对象的程序中,对象作为了程序的主要矛盾而存在,但过程并没有消失,而是作为次要矛盾而存在。
对象的体现就在于程序各个类的组织;而在我的架构中,过程主要隐藏在算子的collectSimpTerms
方法上,因子->算子就是我架构的主要过程,因此在类图上就体现为,上方的因子FactorNode
最终归结到下方的算子ExprNode
上。
上一节迭代开发的历程叙述中,我已尽力在说明各个类的设计思路和原因,此处再逐类分析或许显得重复啰嗦,因此我就串一下每个类,总结我的最终设计(不涉及内部类):
主程序MainClass
调用Lexer
文法解析输入的表达式字符串,并由Parser
利用Lexer
逐类词法解析。解析表达式AddNode
时,先解析项,解析出各个因子FactorNode
NumberNode
PowerFNode
TrigFNode
(可能涉及递归)AddNode
(可能涉及递归)AddNode
'DrvNode
求导其中表达式后得到的多项式SimpTerms转换而成的AddNode
''(可能涉及递归)将各个因子FactorNode
以一个或多个MultNode
组织,计算出多个最简项SimpTerm
并合并得出最终的多项式SimpTerms
,由SimpTerms
赋予MultNode
算子多项式的物理意义。最后,用AddNode
组织所有的算子ExprNode
,合并所有多项式SimpTerms
得到最终的最简表达式AddNode
。这个过程若发生在自定义函数定义过程,则将所得表达式放入函数名对应的Func
工厂作为模板;若发生解析待展开的表达式过程,则直接输出所得AddNode
的SimpTerms
物理意义,得到最终答案,问题解决。
点评讲求简明扼要
优点:
缺点:
仅就最终架构作度量分析,未分析内部类,将内部类放进外部类的规模和各项指标中去了(不知道该怎么分析...)
类名 | 属性个数 | 方法个数 | 类总代码规模(lines) |
---|---|---|---|
AddNode | 2 | 15 | 115 |
DrvNode | 2 | 2 | 14 |
ExprNode | 1 | 6 | 24 |
FactorNode<<接口>> | 0 | 5 | 9 |
Func | 2 | 3 | 31 |
MultNode | 3 | 12 | 69 |
NumberNode | 1 | 9 | 43 |
PowerFNode | 2 | 11 | 73 |
SimpTerm | 3 | 35 | 373 |
SimpTerms | 0 | 7 | 92 |
TrigFNode | 5 | 17 | 100 |
Lexer | 3 | 7 | 60 |
MainClass | 0 | 1 | 20 |
Parser | 4 | 10 | 153 |
从此处可以看出SimpTerm类的规模确实过于庞大,功能需要精简.其他类的长度和复杂度感觉还在可以接受的范围内。
方法分析时我才注意自己的构造函数写的很多、不太规范,有些类的构造函数重载了很多种,这应该避免。
getter和setter逻辑太简单,也不进行分析
方法(去掉了构造器、getter和setter) | 方法规模(lines) | 控制分支数目 |
---|---|---|
Lexer.getExp() | 9 | 1 |
Lexer.getNumber() | 7 | 1 |
Lexer.getPowerF() | 1 | 0 |
Lexer.hasExp() | 10 | 3 |
Lexer.next() | 10 | 3 |
Lexer.peek() | 1 | 0 |
MainClass.main(String[]) | 13 | 1 |
Parser.getLeadingAS() | 7 | 1 |
Parser.parseExpr() | 27 | 6 |
Parser.parseFactor() | 30 | 8 |
Parser.parseFunc() | 11 | 1 |
Parser.parseFuncDef(String) | 11 | 2 |
Parser.parseTerm() | 14 | 2 |
Parser.parseTrig() | 13 | 2 |
element.AddNode.addChild(ExprNode) | 1 | 0 |
element.AddNode.addIntoMult(MultNode) | 1 | 0 |
element.AddNode.collectSimpTerms() | 8 | 3 |
element.AddNode.containBase(Character) | 11 | 4 |
element.AddNode.deepClone() | 3 | 0 |
element.AddNode.derivated(char) | 3 | 0 |
element.AddNode.equals(Object) | 5 | 1 |
element.AddNode.nodeChange(...) | 12 | 3 |
element.AddNode.powerExpand() | 11 | 3 |
element.AddNode.rmBracket() | 9 | 3 |
element.AddNode.toSimpFactorInTrig(...) | 1 | 0 |
element.DrvNode.collectSimpTerms() | 1 | 0 |
element.ExprNode.toAddNode() | 4 | 0 |
element.ExprNode.toString() | 1 | 0 |
element.Func.createValueMap(...) | 10 | 3 |
element.Func.funcToAddNode(...) | 2 | 0 |
element.MultNode.addChild(FactorNode) | 1 | 0 |
element.MultNode.addExpr(ExprNode) | 5 | 2 |
element.MultNode.addNumber(NumberNode) | 1 | 0 |
element.MultNode.addPF(PowerFNode) | 2 | 0 |
element.MultNode.addTrig(TrigFNode) | 1 | 0 |
element.MultNode.collectSimpTerms() | 8 | 3 |
element.MultNode.deepClone() | 3 | 0 |
element.MultNode.equals(Object) | 3 | 0 |
element.MultNode.switchAS() | 3 | 1 |
element.NumberNode.addIntoMult(MultNode) | 1 | 0 |
element.NumberNode.containBase(Character) | 1 | 0 |
element.NumberNode.deepClone() | 1 | 0 |
element.NumberNode.derivated(char) | 2 | 0 |
element.NumberNode.equals(Object) | 4 | 1 |
element.NumberNode.toSimpFactorInTrig(...) | 1 | 0 |
element.NumberNode.toString() | 1 | 0 |
element.PowerFNode.addIntoMult(MultNode) | 1 | 0 |
element.PowerFNode.containBase(Character) | 1 | 0 |
element.PowerFNode.deepClone() | 1 | 0 |
element.PowerFNode.derivated(char) | 8 | 1 |
element.PowerFNode.equals(Object) | 5 | 1 |
element.PowerFNode.pow(int) | 1 | 0 |
element.PowerFNode.toSimpFactorInTrig(...) | 10 | 1 |
element.PowerFNode.toString() | 7 | 3 |
element.SimpTerm.BaseNums.expAdd(char, int) | 1 | 0 |
element.SimpTerm.BaseNums.expAddAll(BaseNums) | 3 | 0 |
element.SimpTerm.BaseNums.noBase() | 1 | 0 |
element.SimpTerm.BaseNums.onlyKey() | 14 | 4 |
element.SimpTerm.TrigFNodes.add(TrigFNode) | 22 | 6 |
element.SimpTerm.TrigFNodes.addAll(...) | 11 | 4 |
element.SimpTerm.TrigFNodes.deepClone() | 8 | 2 |
element.SimpTerm.TrigFNodes.equals(TrigFNodes) | 5 | 1 |
element.SimpTerm.addTrig(TrigFNode) | 1 | 0 |
element.SimpTerm.coefAdd(SimpTerm) | 1 | 0 |
element.SimpTerm.coefIsPositive() | 1 | 0 |
element.SimpTerm.coefMult(BigInteger) | 1 | 0 |
element.SimpTerm.coefNegated() | 1 | 0 |
element.SimpTerm.compareTo(SimpTerm) | 9 | 4 |
element.SimpTerm.containBase(Character) | 11 | 2 |
element.SimpTerm.deepClone() | 1 | 0 |
element.SimpTerm.derivated(char) | 20 | 8 |
element.SimpTerm.equals(Object) | 5 | 1 |
element.SimpTerm.factorExchange(...) | 10 | 2 |
element.SimpTerm.isConst() | 1 | 0 |
element.SimpTerm.isZero() | 1 | 0 |
element.SimpTerm.mult(SimpTerm) | 8 | 0 |
element.SimpTerm.pfExchange(...) | 12 | 3 |
element.SimpTerm.powerFAppended(StringBuilder) | 40 | 14 |
element.SimpTerm.productsRule(char) | 35 | 5 |
element.SimpTerm.putPF(PowerFNode) | 1 | 0 |
element.SimpTerm.rmBracket() | 9 | 3 |
element.SimpTerm.toString() | 11 | 3 |
element.SimpTerm.trigAppended(StringBuilder) | 10 | 2 |
element.SimpTerm.varEquals(Object) | 3 | 0 |
element.SimpTerms.add(SimpTerm) | 18 | 7 |
element.SimpTerms.addAll(...) | 6 | 2 |
element.SimpTerms.deepClone() | 6 | 1 |
element.SimpTerms.derivated(SimpTerms, char) | 4 | 2 |
element.SimpTerms.equals(SimpTerms) | 12 | 4 |
element.SimpTerms.mult(SimpTerms) | 8 | 2 |
element.SimpTerms.toString() | 15 | 5 |
element.TrigFNode.addIntoMult(MultNode) | 7 | 2 |
element.TrigFNode.containBase(Character) | 1 | 0 |
element.TrigFNode.deepClone() | 1 | 0 |
element.TrigFNode.derivated(char) | 12 | 2 |
element.TrigFNode.equals(Object) | 5 | 1 |
element.TrigFNode.factorExchange(...) | 1 | 0 |
element.TrigFNode.isSin() | 1 | 0 |
element.TrigFNode.mult(TrigFNode) | 1 | 0 |
element.TrigFNode.toSimpFactorInTrig(...) | 1 | 0 |
element.TrigFNode.toString() | 10 | 4 |
element.TrigFNode.varEqual(TrigFNode) | 1 | 0 |
(手动分析,感觉很累,不知道有没有什么可以自动分析的工具,或者说是要自己写工具,还是我对要求理解错了……)
CK:
Class | CBO | DIT | LCOM | NOC | RFC | WMC |
---|---|---|---|---|---|---|
Lexer | 3 | 1 | 1 | 0 | 17 | 12 |
MainClass | 3 | 1 | 1 | 0 | 11 | 2 |
Parser | 10 | 1 | 1 | 0 | 49 | 28 |
element.AddNode | 11 | 2 | 3 | 0 | 42 | 27 |
element.DrvNode | 4 | 2 | 1 | 0 | 5 | 2 |
element.ExprNode | 8 | 1 | 3 | 3 | 10 | 5 |
element.FactorNode | 5 | |||||
element.Func | 4 | 1 | 1 | 0 | 13 | 6 |
element.MultNode | 9 | 2 | 4 | 0 | 30 | 17 |
element.NumberNode | 8 | 1 | 4 | 0 | 14 | 10 |
element.PowerFNode | 7 | 1 | 2 | 0 | 28 | 16 |
element.SimpTerm | 10 | 1 | 1 | 0 | 85 | 70 |
element.SimpTerm.BaseNums | 1 | 3 | 3 | 0 | 9 | 8 |
element.SimpTerm.TrigFNodes | 3 | 4 | 1 | 0 | 17 | 18 |
element.SimpTerms | 5 | 4 | 1 | 0 | 26 | 27 |
element.TrigFNode | 8 | 1 | 2 | 0 | 35 | 26 |
(这些统计指标我有些还搞不懂其具体意义,所以只能凭自己理解,要是有教程就好了额)
Parser、AddNode的圈复杂度大,感觉比较正常,因为与所有算子和因子都有关。
还是SimpTerm的问题,复杂性有些过大了。我认为还是需要关注其物理意义即可,有关计算意义的方法或许可以放进算子类中。
MOOD:
Project | AHF | AIF | CF | MHF | MIF | PF |
---|---|---|---|---|---|---|
project | 92.86% | 39.13% | 64.84% | 12.71% | 9.68% | 114.29% |
MOOD的这个分析感觉还比较有意义?
Method.Complexity:
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
element.SimpTerm.powerFAppended(StringBuilder) | 21 | 1 | 14 | 15 |
element.SimpTerms.add(SimpTerm) | 18 | 6 | 7 | 7 |
element.SimpTerm.TrigFNodes.add(TrigFNode) | 11 | 6 | 8 | 11 |
element.SimpTerm.TrigFNodes.equals(TrigFNodes) | 10 | 5 | 4 | 5 |
element.SimpTerms.equals(SimpTerms) | 10 | 5 | 4 | 5 |
element.SimpTerm.rmBracket() | 9 | 1 | 9 | 9 |
element.AddNode.containBase(Character) | 7 | 4 | 3 | 4 |
element.SimpTerm.BaseNums.onlyKey() | 7 | 4 | 2 | 4 |
摘了一些复杂度出问题的方法,确实体现出SimpTerm的输出复杂度太高了。。。然后因为合并同类项搞的复杂度有点超,因为用到很多循环和if-equals判断,这里边有很多方法是在判断。判断逻辑的优化也是我需要注意的。
Class.Complexity:
Class | OCavg | OCmax | WMC |
---|---|---|---|
Lexer | 1.71 | 3 | 12 |
MainClass | 2 | 2 | 2 |
Parser | 2.8 | 8 | 28 |
element.AddNode | 1.8 | 4 | 27 |
element.DrvNode | 1 | 1 | 2 |
element.ExprNode | 1 | 1 | 5 |
element.Func | 2 | 3 | 6 |
element.MultNode | 1.42 | 4 | 17 |
element.NumberNode | 1.11 | 2 | 10 |
element.PowerFNode | 1.45 | 3 | 16 |
element.SimpTerm | 2.69 | 14 | 70 |
element.SimpTerm.BaseNums | 1.6 | 4 | 8 |
element.SimpTerm.TrigFNodes | 4.5 | 7 | 18 |
element.SimpTerms | 3.86 | 6 | 27 |
element.TrigFNode | 1.53 | 6 | 26 |
类复杂度除了SimpTerm的WMC其他问题也不大。SimpTerm的方法复杂度高。
TrigFNodes SimpTerms继承了ArrayList类,因此OCavg超了很多,不过应该不用太担心。
Dependency:
Class | Cyclic | Dcy | Dcy* | Dpt | Dpt* | PDcy | PDpt |
---|---|---|---|---|---|---|---|
Lexer | 0 | 1 | 12 | 2 | 2 | 1 | 1 |
MainClass | 0 | 3 | 16 | 0 | 0 | 2 | 0 |
Parser | 0 | 9 | 15 | 1 | 1 | 2 | 1 |
element.AddNode | 10 | 7 | 11 | 7 | 14 | 1 | 2 |
element.DrvNode | 10 | 3 | 11 | 2 | 14 | 1 | 2 |
element.ExprNode | 10 | 2 | 11 | 7 | 14 | 1 | 2 |
element.Func | 0 | 3 | 13 | 1 | 2 | 1 | 1 |
element.MultNode | 10 | 7 | 11 | 7 | 14 | 1 | 2 |
element.NumberNode | 10 | 2 | 11 | 7 | 14 | 1 | 2 |
element.PowerFNode | 10 | 5 | 11 | 3 | 14 | 1 | 2 |
element.SimpTerm | 10 | 10 | 11 | 4 | 14 | 1 | 1 |
element.SimpTerm.BaseNums | 0 | 0 | 0 | 1 | 15 | 0 | 1 |
element.SimpTerm.TrigFNodes | 10 | 3 | 11 | 1 | 14 | 1 | 1 |
element.SimpTerms | 10 | 1 | 11 | 5 | 14 | 1 | 1 |
element.TrigFNode | 10 | 5 | 11 | 4 | 14 | 1 | 2 |
element.FactorNode | 10 | 1 | 11 | 9 | 14 | 1 | 2 |
Average | 6.875 | 3.875 | 11.0625 | 3.8125 | 10.875 | 1.0625 | 1.4375 |
依赖性我也不太会看,是否意味着类之间的耦合度呢?看着,一般的类设计的依赖性还比较平均,不过SimpTerm的Dcy指标超了平均比较多,这个类设计或许确实还有很大优化空间。
终于到心得体会了。
这个图的线弯弯的,是因为这是美人鱼(mermaid)帮我画的(先谢谢她)。然后不知道这种形式到底好不好。用代码构造图,好处是能帮你自动排版,坏处是因此不能手动排版。不过自己附加逻辑信息和用逻辑关系(关联、聚合、组合等)联结各个类的操作还是可以照常进行的。为了图的清晰度我存成HTML格式了,如果不太合适下次还是换成StarUML,做图片。
在做UML类图的时候我甚至用java结合正则表达式写了个工具逆向生成类图中的类,减少一点手动写mermaid代码麻烦,感觉还有点用。如果这样设计类图可以的话我就继续开发下去……这次算是一种尝试吧。
-
HTML的类图不能显示,我就发图片了,不好意思,有点糊。
HW1:
HW2:
HW3: