OO第一单元:表达式解析

朱宇-21373074 2023-03-19 19:13:11

OO第一单元:表达式解析

写在前面:本人代码能力基础有限,逻辑思维能力不是很强,因此可能一些表述比较模糊或不够严谨,如有错误欢迎指正,本人希望从错误中学到更多。

作业要求

ps: 表达式解析的三次作业采取增量开发的形式,因此只取最后一次作业的要求进行作业要求展示

基本要求:将按照形式化定义的表达式进行化简展开,包括表达式括号展开(项与表达式、表达式与表达式相乘、高次幂表达式展开)、自定义函数代入、求导计算等。
进阶要求(性能提升):

  • 合并同类项
  • 三角函数化简(诱导公式(最简单的sin(0)、cos(0)),sin(x)**2+cos(x)**2等)
  • ...

形式化定义

基本量

  • 表达式 → 空白项 [加减 空白项] 项 空白项 | 表达式 加减 空白项 项 空白项
  • 项 → [加减 空白项] 因子 | 项 空白项 '*' 空白项 因子
  • 因子 → 变量因子 | 常数因子 | 表达式因子|求导因子
  • 变量因子 → 幂函数 | 三角函数 | 自定义函数调用
  • 常数因子 → 带符号的整数
  • 表达式因子 → '(' 表达式 ')' [空白项 指数]
  • 幂函数 → 自变量 [空白项 指数]
  • 自变量 → 'x' | 'y' | 'z'
  • 三角函数 → 'sin' 空白项 '(' 空白项 因子 空白项 ')' [空白项 指数] | 'cos' 空白项 '(' 空白项 因子 空白项 ')' [空白项 指数]
  • 指数 → '**' 空白项 ['+'] 允许前导零的整数 (注:指数一定不是负数)
  • 带符号的整数 → [加减] 允许前导零的整数
  • 允许前导零的整数 → ('0'|'1'|'2'|…|'9'){'0'|'1'|'2'|…|'9'}
  • 空白项 → {空白字符}
  • 空白字符 → (空格) | \t
  • 加减 → '+' | '-'

自定义函数相关

  • 自定义函数定义 → 自定义函数名 空白项 '(' 空白项 自变量 空白项 [',' 空白项 自变量 空白项 [',' 空白项 自变量 空白项]] ')' 空白项 '=' 空白项 函数表达式
  • 自定义函数调用 → 自定义函数名 空白项 '(' 空白项 因子 空白项 [',' 空白项 因子 空白项 [',' 空白项 因子 空白项]] ')'
  • 自定义函数名 → 'f' | 'g' | 'h'
  • 函数表达式 → 表达式 (注:本次作业函数表达式中可以调用其他自定义函数,但保证不会出现递归调用的情况)

求导算子相关

  • 求导因子 → 求导算子 空白项 '(' 空白项 表达式 空白项 ')'
  • 求导算子 → 'dx' |'dy' |'dz'

特殊符号含义

{} 表示允许存在 0 个、1 个或多个。
[] 表示允许存在 0 个或 1 个。
() 内的运算拥有更高优先级,类似数学中的括号。
| 表示在多个之中选择一个。
上述表述中使用单引号包裹的串表示字符串字面量,如 '(' 表示字符 (

例子(出自HW3强测)

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修复角度分析不足,作为经验教训记录于此。而第三次架构较为成熟,因此将对照类图详细解释每个类以及其中属性方法的设计考虑,注重最终结果

HW1--无三角函数、自定义函数、求导

第一次作业难度低,但由于是白手起家,因此花费了不少时间和精力。并且,最大的敌人是死脑筋。“没有想象力是罪魁”

HW1分析

从头开始看作业,我认为作业可分为表达式“解析”“展开”和“输出”三个关系密切的过程。“解析”意味着以何种数据结构“存储”(或者说,“暂存”)所输入的表达式,而“展开”意味着如何进行去括号、合并同类项等计算。“输出”即是字面意义上输出展开后的表达式(但其实有很多性能优化的过程也可在输出完成)

层次分析:根据形式化定义,表达式的本质是多个项,项的本质是多个因子;而表达式又可以是因子存在于项中,由此看出“递归下降”在本次任务中的体现,引出Lexer/Parser——文法/词法解析器的方法;以及层次化解析表达式的必要性,由此引出我们课程的核心思想——面向对象的思想(将表达式、项、因子分成三层次的对象,而每个层次也可根据性质分成不同的具体对象表示)。

img

架构

HW1CD.html 16.44K

(有个PowerF,其实是Power Function幂函数)
由类图就可以看出,我第一次的架构其实是很乱的,尽管只有500多行的常规代码量,但是里面的逻辑关系我没有梳理的很清楚。(回过头看我还花了很多时间理解自己的代码……)

数据结构上犯的错:我那时的想法是这样的:因为作业指导书建议去用表达式树解析,于是我联想到当初做后缀表达式时用到的二叉树,结合这次乘法加减法都是二元运算符,我打算依然用二叉树解析表达式。于是我按照树的形状,每个父结点有左右子结点,结点用ExprNode这个容器表示,这个容器里有个接口ExprItem表示这个结点的实际意义。如果该ExprItem实现为AS(add/sub),则表示表达式中间的加减运算符;如果该ExprItem实现为Term,则表示项。当时我只想着搭框架,生硬地套表达式树的形式,没有想清楚这个二叉树意义何在,就这样那个星期我还没有想通建树跟另一种想法——将表达式表示成List<Term>——这种思路有什么区别。这个二叉树建起来有什么意义?因为做到表达式之间相乘的时候,肯定是List与List进行项项合并更符合直觉,所以我甚至还用递归写了树与List相互转换的函数,只为了能表现出二叉树这种形式……显然,我犯了形式大于内容的错误。

递归下降解析:递归下降的指导精神在三次作业中一直延续,不过具体实现是在不断进化的。第一次作业参考了第一次训练中课程组提供的代码。简单来说过程是这样的:

  • 表达式由parseExpr解析,即解析其中的各个项,以+/-(AS)间隔出各个项;(下图每个方框都是容器ExprNode)

img

  • 项由parseTerm解析,以*间隔分隔出各个因子;
  • 因子由parseFactor解析,以因子首字符特征([0-9]常数还是[xyz]幂函数还是'('表达式)提取因子放到Term中。

(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。

HW1 bug修复

第一次作业采用二叉树结构,并且表达式相乘时我采用的方法是,将两棵表达式树每个结点两两合并,产生新子树,并且在相乘时没有合并同类项!,是在输出时合并的,结果就是,深度为m的子树1与深度为n的子树2相乘会形成深度为m*n新子树3,在表达式的幂次稍微有点高时,这个新子树最终就很深很深很深很深……而中序遍历是递归操作,一个简单的遍历函数面对很深的树会调用自己很多次,这种调用通过函数栈实现,因此强测及互测就出现了StackOverflow Error的bug,java的函数栈爆了。

(正如StackOverflow网站名字被寄予的美好寓意,这个error给了我重构的信心)

我这次作业的bug修复只是打补丁,具体操作对于作业的进展没有任何实质上的意义,单纯为了补分,这里也不做记录。真正意义上的bug修复从以下的重构开始。

HW1.5--重构

在做HW2之前,我花了一段时间对HW1进行重构,理清了思路,最终形成了相对稳定好用的、逻辑清晰的、十分有利于后续迭代开发的新一套架构,沿用到了后两次作业中。

刚刚的HW1中我总结了:

  1. 数据结构上的错误
  2. 表达式展开及合并同类项的繁琐
  3. 正负号解析的逻辑不清
  4. 输出时才化简导致的致命bug

以下我的重构逐一攻破了这些问题。

(HW1重构后的类图与HW2相似,可以参考 HW2类图

数据结构的改变(核心)

重构的最大改变来自思想转变,我意识到,树的本质就是组织数据的形式。而之前具体意义上类似于List<Factor>的想法,其实就类似于在抽象意义上的多叉树!数据结构为解决问题服务,需要理解问题的需求,再选择出最合适的数据结构。我由此意识到,这个问题在我的直觉上更适合用多叉树的形式去解析表达式(我本人没想出二叉树对解决这个问题有什么用,以及如何用二叉树比较优雅地解决这个问题。如有好的想法欢迎在评论区加以指导!)。于是我推倒重来,将所有要素从命名到实现进行结点化,梳理出了一个结构更清晰的表达式多叉树

img

摆脱了二叉树以及所谓“容器”以后,AddNode意义变成了连加,直接管理一个表达式(因子)整体。表达式本质就是连加;MultNode的意义变成了连乘,直接管理一个项整体。项的本质就是连乘。

其中根据类图可知,从表达式到项,再到因子,都摆脱了“容器”的束缚,去实现了ExprItem表达式成员接口,其本身作为表达式树的一员存在(ExprItem表示表达式的所有成员)。ExprNode也实现了ExprItem,配备ArrayList<Expritem>(即子结点是一个个表达式成员),在此基础上赋予了抽象类实现--表达式成员管理者的身份,不再以“容器”的形式具体存在,而成了AddNode、MultNode们的灵魂。在这种结构中,所有的叶节点都是常数因子和变量因子,上面的组织结点均由ExprNode实现。(而所谓ExprNode,其实是最终结构中抽象出的“算子”雏形,仅仅是雏形)

这个结构相比于HW1的结构,更符合直觉,也更有利于之后操作的进行了。

不过这里依然犯了两个错误,直到HW3才改过来:

  1. 参照类图可知,ExprNode的逻辑不明。我在这时依然将ExprNode实现了ExprItem接口(即算子与因子在一个抽象层次上,而因子没有单独的抽象层次),事实上这对层次化管理、以及之后三角函数的解析等均有不利之处。这样的抽象实现抽象,看起来不妥当,导致了逻辑混乱。而且这样一来MultNode就处于一个尴尬的位置:因子有些应当共同实现的方法,那么MultNode该怎么处理呢?没有抽象出Factor接口,导致抽象逻辑不清楚,继而导致了HW2的一处粗心bug。以及,因为ExprItem的出现,它与Factor的重合度比较大,所我这里放弃了Factor因子接口。仔细想,这同样是一个很不好的处理。在HW3中我把表达式成员拆分成了算子和因子,摒弃了ExprItem这一不好的抽象,解决了这一问题,这样也利于程序的可扩展性。
  2. 上了课我才知道抽象类在java中并不是很适用,似乎拆分成一个普通类和一个接口更符合面向对象的思维方法。这里我为了抽象方法以及包含一些共有的属性,就将ExprNode定义成了一个抽象类。在以后的作业中我会注意这类问题。

表达式展开及合并同类项--封神SimpTerm

以上数据结构的构建也伴随着我对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不尽人意的结果。

HW2--新增三角函数、自定义函数

写在前面:大节不亏,才能小节不拘

↑ 有些令人迷惑的名句引用=.=。其实想表达的意思是,在思维上把抽象的顶层设计做好了以后,才能在具体写代码时从容不迫,甚至不拘小节,因为好的架构避免很多不好架构隐藏的不起眼的bug。这一思想从重建后的HW2尤其是bug部分充分体现了出来。

HW2分析

三角函数:三角函数因子可以分为两部分,三角标识符(sin/cos)以及内部因子。

  • 因为只有两个三角标识符,所以我直接设boolean量区分了,方便一些(后面求导的时候变名也稍稍方便一些)。为了可扩展性,其实可以为其定义成Enum枚举类
  • 形式化定义中三角函数内部就是层次化后的因子。但是关于内部因子的处理其实有两种方式:当成因子解析(parseFactor)和当成表达式解析(parseExpr),其实这两种方法区别不大,正确性都可以保证;但是在我的架构下,当成表达式解析不太有利于提升性能,因此我选择了当成因子解析的方法。
  • 对了,还有指数,不过这个设一个int属性表示即可,没什么难的。

自定义函数:本质上是参数替换,即函数定义式中的形参xyz替换为函数调用中的因子们。这种对应关系我们很容易联想到HashMap,我也是用这个容器实现的。

HW2架构及实现

HW2cd.html 17.35K

三角函数处理:这个比较简单,在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属性,在解析该三角函数的时候分析保存起来,输出时看一下这个属性即可。

而在作业要求中,当成表达式解析和当成因子解析,两者的不同恰恰就在于拆括号这个操作。表达式解析是上述操作;因子解析则需要

  1. 对表达式进行拆括号判断
  2. 如果能拆括号则进行向因子等价转换
  3. 输出时instanceof类型判断,如果是表达式则多加一层括号

这样三步走。逻辑似乎更加复杂。所以正在写文档的我也不太想得起来当时为啥选择按因子解析了。我只记得一个原因是,尊重并遵守形式化定义中,保持三角函数内部是一个“因子”…… 乐。总之这两种方法都是适用的,不知道大佬们对这个问题有啥独到见解不。


自定义函数实现:我采取了类似于工厂的方法实现自定义函数(Func类工厂)。

Func工厂的自定义函数制作流程是:

  • 自定义函数定义:首先在Parser类中先构建Func类,将函数名与其工厂建立一个对应关系,放进统管自定义函数的HashMap<Character, Func> funcMap。之后,常规用parseExpr读入表达式,存进刚刚新建的Func对象,称为模板表达式ExprNode template。同时也要保存模板表达式相应的形参有序序列ArrayList<Character> virSeq类似于支持重载的函数需要函数名和形参变量共同定义一个函数
  • 自定义函数调用:先用函数名找到对应的Func工厂,然后向工厂传入实参有序序列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的两个新增需求的实现完成了。


可以吸取的一个教训是,要正视我们所建立的每个对象、对象中的每个属性的物理意义,并分析好对象与对象,对象与自己属性的关联强度究竟是怎样的,这对于我们实现功能、完善结构等都是很重要的

HW2 bug修复

一共产生了5个bug,我提交了5次合并修复,并且前三个bug一共只修了4行,足以看出我的粗心,以及架构的不够完备。

  1. 第一个bug来源于,自定义函数方法转变后没有做好善后工作。在替换过程调用的nodeChange函数中我写了一个验证AddNode的children结点们是否为空的判断条件,以进行debug测试,当其为空时输出“nodechangeerror”字符串并结束程序,否则正常运行。根据上面的原理分析,并不是所有合法的AddNode都有Children,代表物理意义、应该不为空的是它的SimpTerms而不是Children。
  2. 第二个bug是我没有建立关于因子的接口造成的不良后果。由于因子抽象没做好,在对自定义函数中三角函数的因子进行参数替换的时候用的是判断因子类型(多层if(node instanceof ...))从而分别返回值的做法。我忘记了判断三角函数因子为三角函数的情况。这就导致三角函数因子为三角函数时报错了。如果做好了因子抽象,在每个因子内实现参数替换方法,那么就不容易出现这种遗漏性粗心错误。
  3. 第三个bug来自于三角函数的输出没有考虑指数特判,sin(0)**0的情况输出了0而不是1。纯粗心。
  4. 第四个bug是,自定义函数替换中处理三角函数中的幂函数时忘记考虑指数了,导致三角函数因子的参数只替换了一次。纯粗心。
  5. 第五个bug是AddNode的重复幂展开,一点代码设计的失误,不涉及架构,也不做赘述。

总的来说,HW2体现的是抽象、思想层面我考虑不周,导致了写代码时的挫折以及最终的一些小bug。因此,只有大节不亏,才有小节不拘的余地啊。

HW3--新增求导

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>

HW3分析

在我的架构下,求导操作的实现就轻松写意了。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中。这就又引发一个需求,由于三角函数按因子存储,所以还要给每个因子实现求导方法。(如果按表达式存储或许就没有这么麻烦?没试过。)其中还有不涉及求导的参数,这些处理就不赘述,实现的方法有很多种。

HW3 bug修复

互测强测都没出bug,耶耶耶!


到此,我的迭代开发之旅就结束了。接下来就到了下一重头戏,结合类图对最终我的结构进行分析。

程序结构分析

HW3cd.html 18.58K

主要分析见类图。这个类图类似于流程图的从上而下的关系,我认为是因为,一个程序是同时具有对象过程的“二态性”的,只不过在面向对象的程序中,对象作为了程序的主要矛盾而存在,但过程并没有消失,而是作为次要矛盾而存在。

对象的体现就在于程序各个类的组织;而在我的架构中,过程主要隐藏在算子的collectSimpTerms方法上,因子->算子就是我架构的主要过程,因此在类图上就体现为,上方的因子FactorNode最终归结到下方的算子ExprNode上。

类设计考虑分析

结构串联

上一节迭代开发的历程叙述中,我已尽力在说明各个类的设计思路和原因,此处再逐类分析或许显得重复啰嗦,因此我就串一下每个类,总结我的最终设计(不涉及内部类):

主程序MainClass调用Lexer文法解析输入的表达式字符串,并由Parser利用Lexer逐类词法解析。解析表达式AddNode时,先解析项,解析出各个因子FactorNode

  1. 常数因子NumberNode
  2. 幂函数因子PowerFNode
  3. 三角函数因子TrigFNode(可能涉及递归)
  4. 表达式因子AddNode(可能涉及递归)
  5. 已定义过的、经自定义函数工厂Func加工制造而成的自定义调用表达式因子AddNode'
  6. 由求导算子DrvNode求导其中表达式后得到的多项式SimpTerms转换而成的AddNode''(可能涉及递归)

将各个因子FactorNode以一个或多个MultNode组织,计算出多个最简项SimpTerm并合并得出最终的多项式SimpTerms,由SimpTerms赋予MultNode算子多项式的物理意义。最后,用AddNode组织所有的算子ExprNode,合并所有多项式SimpTerms得到最终的最简表达式AddNode。这个过程若发生在自定义函数定义过程,则将所得表达式放入函数名对应的Func工厂作为模板;若发生解析待展开的表达式过程,则直接输出所得AddNodeSimpTerms物理意义,得到最终答案,问题解决。

优缺点点评

点评讲求简明扼要

优点:

  1. 抽象层次较清晰,可扩展性较强,表达式问题若要新增,大多分为新增计算和新增因子,对应了算子因子的抽象层次。
  2. 递归下降,适应了表达式层层嵌套的特性,将复杂问题的层次梳理地很清晰。
  3. 采用最简项SimpTerm架构,利于合并同类项等性能提升操作,以及简化整体的输出逻辑。

缺点:

  1. 出于性能考虑,SimpTerm的字符串输出逻辑较为复杂,不够简练。
  2. 由于封神SimpTerm,其功能逻辑也较为复杂,可能需要考虑功能分散。

度量分析

仅就最终架构作度量分析,未分析内部类,将内部类放进外部类的规模和各项指标中去了(不知道该怎么分析...)

类分析

类名属性个数方法个数类总代码规模(lines)
AddNode215115
DrvNode2214
ExprNode1624
FactorNode<<接口>>059
Func2331
MultNode31269
NumberNode1943
PowerFNode21173
SimpTerm335373
SimpTerms0792
TrigFNode517100
Lexer3760
MainClass0120
Parser410153

从此处可以看出SimpTerm类的规模确实过于庞大,功能需要精简.其他类的长度和复杂度感觉还在可以接受的范围内。

方法分析

方法分析时我才注意自己的构造函数写的很多、不太规范,有些类的构造函数重载了很多种,这应该避免。
getter和setter逻辑太简单,也不进行分析

方法(去掉了构造器、getter和setter)方法规模(lines)控制分支数目
Lexer.getExp()91
Lexer.getNumber()71
Lexer.getPowerF()10
Lexer.hasExp()103
Lexer.next()103
Lexer.peek()10
MainClass.main(String[])131
Parser.getLeadingAS()71
Parser.parseExpr()276
Parser.parseFactor()308
Parser.parseFunc()111
Parser.parseFuncDef(String)112
Parser.parseTerm()142
Parser.parseTrig()132
element.AddNode.addChild(ExprNode)10
element.AddNode.addIntoMult(MultNode)10
element.AddNode.collectSimpTerms()83
element.AddNode.containBase(Character)114
element.AddNode.deepClone()30
element.AddNode.derivated(char)30
element.AddNode.equals(Object)51
element.AddNode.nodeChange(...)123
element.AddNode.powerExpand()113
element.AddNode.rmBracket()93
element.AddNode.toSimpFactorInTrig(...)10
element.DrvNode.collectSimpTerms()10
element.ExprNode.toAddNode()40
element.ExprNode.toString()10
element.Func.createValueMap(...)103
element.Func.funcToAddNode(...)20
element.MultNode.addChild(FactorNode)10
element.MultNode.addExpr(ExprNode)52
element.MultNode.addNumber(NumberNode)10
element.MultNode.addPF(PowerFNode)20
element.MultNode.addTrig(TrigFNode)10
element.MultNode.collectSimpTerms()83
element.MultNode.deepClone()30
element.MultNode.equals(Object)30
element.MultNode.switchAS()31
element.NumberNode.addIntoMult(MultNode)10
element.NumberNode.containBase(Character)10
element.NumberNode.deepClone()10
element.NumberNode.derivated(char)20
element.NumberNode.equals(Object)41
element.NumberNode.toSimpFactorInTrig(...)10
element.NumberNode.toString()10
element.PowerFNode.addIntoMult(MultNode)10
element.PowerFNode.containBase(Character)10
element.PowerFNode.deepClone()10
element.PowerFNode.derivated(char)81
element.PowerFNode.equals(Object)51
element.PowerFNode.pow(int)10
element.PowerFNode.toSimpFactorInTrig(...)101
element.PowerFNode.toString()73
element.SimpTerm.BaseNums.expAdd(char, int)10
element.SimpTerm.BaseNums.expAddAll(BaseNums)30
element.SimpTerm.BaseNums.noBase()10
element.SimpTerm.BaseNums.onlyKey()144
element.SimpTerm.TrigFNodes.add(TrigFNode)226
element.SimpTerm.TrigFNodes.addAll(...)114
element.SimpTerm.TrigFNodes.deepClone()82
element.SimpTerm.TrigFNodes.equals(TrigFNodes)51
element.SimpTerm.addTrig(TrigFNode)10
element.SimpTerm.coefAdd(SimpTerm)10
element.SimpTerm.coefIsPositive()10
element.SimpTerm.coefMult(BigInteger)10
element.SimpTerm.coefNegated()10
element.SimpTerm.compareTo(SimpTerm)94
element.SimpTerm.containBase(Character)112
element.SimpTerm.deepClone()10
element.SimpTerm.derivated(char)208
element.SimpTerm.equals(Object)51
element.SimpTerm.factorExchange(...)102
element.SimpTerm.isConst()10
element.SimpTerm.isZero()10
element.SimpTerm.mult(SimpTerm)80
element.SimpTerm.pfExchange(...)123
element.SimpTerm.powerFAppended(StringBuilder)4014
element.SimpTerm.productsRule(char)355
element.SimpTerm.putPF(PowerFNode)10
element.SimpTerm.rmBracket()93
element.SimpTerm.toString()113
element.SimpTerm.trigAppended(StringBuilder)102
element.SimpTerm.varEquals(Object)30
element.SimpTerms.add(SimpTerm)187
element.SimpTerms.addAll(...)62
element.SimpTerms.deepClone()61
element.SimpTerms.derivated(SimpTerms, char)42
element.SimpTerms.equals(SimpTerms)124
element.SimpTerms.mult(SimpTerms)82
element.SimpTerms.toString()155
element.TrigFNode.addIntoMult(MultNode)72
element.TrigFNode.containBase(Character)10
element.TrigFNode.deepClone()10
element.TrigFNode.derivated(char)122
element.TrigFNode.equals(Object)51
element.TrigFNode.factorExchange(...)10
element.TrigFNode.isSin()10
element.TrigFNode.mult(TrigFNode)10
element.TrigFNode.toSimpFactorInTrig(...)10
element.TrigFNode.toString()104
element.TrigFNode.varEqual(TrigFNode)10

(手动分析,感觉很累,不知道有没有什么可以自动分析的工具,或者说是要自己写工具,还是我对要求理解错了……)

  • parseFactor的复杂度比较高、分支较多,可能也不太有办法精简?
  • element.SimpTerm.powerFAppended这个方法是输出最简项中变量因子的方法,为了优化写了很多特判,逻辑比较复杂,因为没有想出很好的输出方法……值得在思考思考,或许可以新建一个专门用于输出的类?
  • element.SimpTerm.productsRule指单项式求导的乘法法则,代码量也有些偏高了。关于求导的一些方法应该放进求导算子类中,因为这个类相比于其他算子类,有些过于简单了。
  • element.SimpTerms.add为了合并同类项用了很多循环和判断,似乎也不太好优化?

类的内聚和相互间的耦合情况

CK:

ClassCBODITLCOMNOCRFCWMC
Lexer31101712
MainClass3110112
Parser101104928
element.AddNode112304227
element.DrvNode421052
element.ExprNode8133105
element.FactorNode5
element.Func4110136
element.MultNode92403017
element.NumberNode81401410
element.PowerFNode71202816
element.SimpTerm101108570
element.SimpTerm.BaseNums133098
element.SimpTerm.TrigFNodes34101718
element.SimpTerms54102627
element.TrigFNode81203526

(这些统计指标我有些还搞不懂其具体意义,所以只能凭自己理解,要是有教程就好了额)

Parser、AddNode的圈复杂度大,感觉比较正常,因为与所有算子和因子都有关。
还是SimpTerm的问题,复杂性有些过大了。我认为还是需要关注其物理意义即可,有关计算意义的方法或许可以放进算子类中。

MOOD:

ProjectAHFAIFCFMHFMIFPF
project92.86%39.13%64.84%12.71%9.68%114.29%

MOOD的这个分析感觉还比较有意义?

  • AHF表明属性封装性确实还可以,通常都设的私有属性
  • CF或许表明类之间的耦合程度处于适中的状态?
  • MHF表明方法的封装性不高,确实是有很多公用方法,而我定义的很多公有方法却应该是私有的,这个以后需要注意私有方法和公有方法区别开。
  • PF这么高,不清楚到底为啥。Essentially it reports on the probability that a given method will be overridden in a subclass.

Method.Complexity:

MethodCogCev(G)iv(G)v(G)
element.SimpTerm.powerFAppended(StringBuilder)2111415
element.SimpTerms.add(SimpTerm)18677
element.SimpTerm.TrigFNodes.add(TrigFNode)116811
element.SimpTerm.TrigFNodes.equals(TrigFNodes)10545
element.SimpTerms.equals(SimpTerms)10545
element.SimpTerm.rmBracket()9199
element.AddNode.containBase(Character)7434
element.SimpTerm.BaseNums.onlyKey()7424

摘了一些复杂度出问题的方法,确实体现出SimpTerm的输出复杂度太高了。。。然后因为合并同类项搞的复杂度有点超,因为用到很多循环和if-equals判断,这里边有很多方法是在判断。判断逻辑的优化也是我需要注意的。

Class.Complexity:

ClassOCavgOCmaxWMC
Lexer1.71312
MainClass222
Parser2.8828
element.AddNode1.8427
element.DrvNode112
element.ExprNode115
element.Func236
element.MultNode1.42417
element.NumberNode1.11210
element.PowerFNode1.45316
element.SimpTerm2.691470
element.SimpTerm.BaseNums1.648
element.SimpTerm.TrigFNodes4.5718
element.SimpTerms3.86627
element.TrigFNode1.53626

类复杂度除了SimpTerm的WMC其他问题也不大。SimpTerm的方法复杂度高。
TrigFNodes SimpTerms继承了ArrayList类,因此OCavg超了很多,不过应该不用太担心。

Dependency:

ClassCyclicDcyDcy*DptDpt*PDcyPDpt
Lexer01122211
MainClass03160020
Parser09151121
element.AddNode1071171412
element.DrvNode1031121412
element.ExprNode1021171412
element.Func03131211
element.MultNode1071171412
element.NumberNode1021171412
element.PowerFNode1051131412
element.SimpTerm10101141411
element.SimpTerm.BaseNums00011501
element.SimpTerm.TrigFNodes1031111411
element.SimpTerms1011151411
element.TrigFNode1051141412
element.FactorNode1011191412
Average6.8753.87511.06253.812510.8751.06251.4375

依赖性我也不太会看,是否意味着类之间的耦合度呢?看着,一般的类设计的依赖性还比较平均,不过SimpTerm的Dcy指标超了平均比较多,这个类设计或许确实还有很大优化空间。

心得体会

终于到心得体会了。

  • 写总结就有心得体会——写的太多了。我感觉把过多的细节写上去了,本来是想作为经验教训以警后人,但后来觉得,有些错误是自己的死脑筋之类的毛病而导致的,没什么普适性。然后这篇文档也写了很长时间,以后应该写得更简练一点,一些自己以为有用的东西,或许在别人看来并不是很重要。写文档不应该只从自己的角度出发,而应该联想读者的感受,去思考读者想要从你的文档中得到什么,毕竟文档不只是为自己看的。这次写完了就这样吧,以后要更注意。
  • 写作业的过程中,我没有跟别人讨论过架构的问题,讨论区有看但是也没怎么参考,所以整个单元作业做下来有闭门造车的感觉。结果是发现自己的想法还是和别人的有不谋而合之处。不过中间确实走了很多弯路,多花了很多不必要的时间。也是值得的吧,我发现了很多我思维方式方面的漏洞。但是我还是认为工作中与他人的讨论肯定是有必要的,只不过我暂时难以找到一种渠道与别人沟通。(本人太社恐了hhh)研讨课确实是提供了这么一个平台,让我得以短暂输出一些自己的观点,沟通是能让自己少走很多弯路,开阔自己的思维。而不沟通的结果要么就是死路和破防,要么就是让自己的单兵能力在时间的积累中越来越强。
  • 快来不及提交作业了,先不想了……这也体现一点,写文档不要写的太多,读者也没有耐心看这么长的东西。

关于UML类图

这个图的线弯弯的,是因为这是美人鱼(mermaid)帮我画的(先谢谢她)。然后不知道这种形式到底好不好。用代码构造图,好处是能帮你自动排版,坏处是因此不能手动排版。不过自己附加逻辑信息和用逻辑关系(关联、聚合、组合等)联结各个类的操作还是可以照常进行的。为了图的清晰度我存成HTML格式了,如果不太合适下次还是换成StarUML,做图片。

在做UML类图的时候我甚至用java结合正则表达式写了个工具逆向生成类图中的类,减少一点手动写mermaid代码麻烦,感觉还有点用。如果这样设计类图可以的话我就继续开发下去……这次算是一种尝试吧。

-

...全文
38 1 打赏 收藏 举报
写回复
1 条回复
切换为时间正序
请发表友善的回复…
发表回复
  • 打赏
  • 举报
回复

HTML的类图不能显示,我就发图片了,不好意思,有点糊。
HW1:

img

HW2:

img

HW3:

img

发帖
2023年北航面向对象设计与构造

382

社区成员

2023年北京航空航天大学《面向对象设计与构造》课程博客
java 高校 北京·海淀区
社区管理员
  • 被Taylor淹没的一条鱼
  • 柠栀_Gin
加入社区
帖子事件
创建了帖子
2023-03-19 19:13
社区公告
暂无公告