概述
OO 第一单元作业围绕着符合 BNF 形式化表示的表达式的解析与化简展开,通过三次迭代开发的作业来考察我们的层次化设计能力和面向对象程序设计中有关接口、多态等特性的运用。我也会通过对三次作业进行要求的分析和架构设计来对我这三周的 OO 作业作一个总结。
第一次作业
要求与分析
读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的多变量表达式,输出恒等变形展开所有括号后的表达式,按照本次作业描述,最终输出的表达式中不应该含有任何的 “(” 和 “)” 。
从题目以及 Training-1 中给出的解析表达式的提示中,我大致明晰了解析表达式的几个层次。一个表达式 (Expr)是由若干 “+” 或 “-” 连接的项(Term)组成,而项则是由若干 “*” 连接的因子(Factor)组成。我们可以得出一个表达式的框架。
在这样一个框架下,我们就可以通过递归下降的解析方法对
输入的表达式进行解析。除了解析读入的表达式之外,我们还有一个任务就是对表达式进行化简,去除表达式中的括号,最后得到一个可以输出的表达式。综合以上分析,我将第一次作业的任务分为了三个部分,解析(读入)表达式 – 化简表达式 – 输出表达式。
架构设计
这次作业的实现架构,我主要参考了训练中的架构,在其基础上加以修改完善。第一次作业的 UML 图如下:
重点说一下 Expr
和 Term
两个类,其他类和接口的解释图中都做了说明。
Expr
: 在本次作业中,表达式同时需要作为可能出现的因子来处理,因此将Expr
作为Factor
的一个实现。按照前文所说的“解析 – 化简 – 输出” 三个步骤,在通过 parser 解析表达式之后,顶层的 expr 对象已经等价于输入的表达式,下一步就是化简,我在方法simplify()
中实现。而作为因子出现时,它也需要返回自身,为了实现嵌套表达式也能化简,在toFactors()
中对每一项进行化简后再返回。Term
: 在表达式的化简中,项是最为重要的一部分。在本次作业中,项最终都可以化简为 系数 * x*(x指数) * y*(y指数) * z**(z指数)。因此增加 out 属性作为其最终的化简形态 ----- HashMap 。其余实现不在此过多赘述。
第二次作业
迭代要求与分析
这次作业的迭代内容为,支持嵌套多层括号,支持三角函数因子,支持自定义函数。
在 hw1 的实现中,使用递归下降的方式就已经实现了多层括号,因此不需要考虑。第二次作业迭代的内容主要为三角函数和自定义函数。三角函数实现的难点在于,如何将其与其他因子相乘,如何把它作为项的一部分进行输出。而自定义函数需要考虑的则更多,需要考虑如何进行实参和形参的替换,多层自定义函数的实现等。从实现难度上来看,我认为第二次作业是第一单元作业中最难的一次。
架构设计
本次作业的 UML 图如下:
为了完成 hw2 的迭代开发,我新增了两个类:
Trigonometry
: 在对表达式进行化简时,为了保证每一个三角函数在比较的时候都能拥有最简形态,在实现toFactors()
方法中需要加入对属性 expr 的化简,在这之后再对三角函数进行比较。为了实现三角函数的输出,我在 ~ 类中加入了由三角函数因子与其指数组成的 triangles,方便在化简时进行合并与项的比较。Function
: 实现自定义函数,需要解析自定义函数,包括形参的顺序和自定义函数对应的表达式。为了不让Function
类中的属性过于繁杂,我选择在解析表达式的时候若遇到自定义函数,就将该自定义函数对应的表达式传入Function
类的对象中,再在Function
类中实现将其用实参替换为表达式返回。其中涉及到深克隆与浅克隆的问题,如果采用浅克隆,用实参替换自定义函数的表达式会使得同种自定义函数的表达式与预期不同,比如 f(f(x)) 这种情况。因此需要再所有Factor
的实现类和Term
类中实现替换 (replace) 和 深克隆 (clone) 两个方法。
第三次作业
迭代要求与分析
这次作业的迭代内容为,支持求导算子(求导因子),自定义函数定义时可以调用已经定义的自定义函数(不会调用自身)。
其实在完成 hw2 时,我通过往 parser 中传入自定义函数的方式实现解析自定义函数,因此在解析自定义函数定义的时候就可以完成对于前面已经定义过的自定义函数的解析,所以第二条任务可以不作其他考虑。求导所需要考虑到的问题有:求导因子出现在自定义函数调用的实参中,求导因子出现在自定义函数定义中,三角函数有关的求导和,三角函数内部出现求导因子等等情况。在进行求导时,如果已经完成了对 expr 的化简,则只需要对其中的每一个 term 进行分别求导,最后合并即可。
架构设计
本次作业的 UML 类图如下:
- 这次作业只有一个新增的类:
Diff
。Diff
类的设计,只是为了在对它进行toFactors()
时返回一个求导过后表达式。主要的求导还是在Term
中增加的方法diff()
,主要思路就是将 Const(系数) * x *(x指数) * y *(y指数) * z **(z指数) 和 triangles 各三角函数分开,设共有 n 个,则求导后的结果也有 n 个项,分别对每个部分进行求导,再将剩余部分放入该项,就能得到个Term
组成的返回值,也就是该项的求导结果。
度量分析
名词解释
- OCavg:类的方法的平均循环复杂度
- OCmax:类的方法的最高循环复杂度
- WMC:类的总循环复杂度
- Cogc:认知复杂度
- ev(G):基本复杂度,用于衡量程序的非结构化程度
- iv(G):模块设计复杂度,用于衡量模块判定结构
- v(G):独立路径的条数,用于衡量模块判定结构的复杂程度
类分析
class | OCavg | OCmax | WMC |
---|---|---|---|
Const | 1.0 | 1.0 | 7.0 |
Diff | 1.0 | 1.0 | 7.0 |
Expr | 2.73 | 7.0 | 41.0 |
Function | 1.0 | 1.0 | 2.0 |
Lexer | 4.0 | 13.0 | 20.0 |
MainClass | 9.0 | 9.0 | 9.0 |
Parser | 4.6 | 12.0 | 23.0 |
Pow | 1.375 | 3.0 | 11.0 |
Term | 5.17 | 20.0 | 93.0 |
Trigonometry | 1.18 | 2.0 | 13.0 |
Var | 1.33 | 3.0 | 8.0 |
Average | 2.75 | 6.55 | 21.27 |
从分析结果的表格可以看出,Term 是复杂度最高的类,一个原因是我将大部分的输出任务都放在 Term
类中,除此之外,我也在其中定义了 terms 与 terms 的乘法,这也是循环复杂度高的来源。Expr
也是类循环复杂度比较高的类,主要因为它同时还是 Factor
的实现类,在三角函数、幂函数、自定义函数等类中都有出现。
方法分析
由于完整的方法分析结果过长,将部分复杂度很低的方法略去后的结果如下:
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Expr.clone() | 1.0 | 1.0 | 2.0 | 2.0 |
Expr.compareTo(Expr) | 15.0 | 6.0 | 4.0 | 6.0 |
Expr.diff(String) | 1.0 | 1.0 | 2.0 | 2.0 |
Expr.opposite(Expr) | 16.0 | 6.0 | 5.0 | 7.0 |
Expr.optimOppo() | 1.0 | 1.0 | 2.0 | 2.0 |
Expr.replace(ArrayList, ArrayList) | 1.0 | 1.0 | 2.0 | 2.0 |
Expr.simplify() | 9.0 | 1.0 | 6.0 | 6.0 |
Expr.toFactors() | 1.0 | 1.0 | 2.0 | 2.0 |
Expr.toString() | 11.0 | 4.0 | 6.0 | 7.0 |
Lexer.next() | 24.0 | 2.0 | 11.0 | 24.0 |
Lexer.peek() | 0.0 | 1.0 | 1.0 | 1.0 |
MainClass.main(String[]) | 11.0 | 4.0 | 5.0 | 8.0 |
Parser.parseExpr() | 4.0 | 1.0 | 4.0 | 4.0 |
Parser.parseFactor() | 21.0 | 1.0 | 15.0 | 17.0 |
Term.compareTo(Term) | 17.0 | 6.0 | 8.0 | 10.0 |
Term.diff(String) | 31.0 | 1.0 | 13.0 | 14.0 |
Term.equals(Term) | 17.0 | 6.0 | 9.0 | 11.0 |
Term.optimOppo() | 18.0 | 3.0 | 7.0 | 8.0 |
Term.replace(ArrayList, ArrayList) | 1.0 | 1.0 | 2.0 | 2.0 |
Term.simplify(ArrayList) | 6.0 | 1.0 | 4.0 | 4.0 |
Term.termsMultipy(ArrayList, ArrayList) | 38.0 | 1.0 | 13.0 | 13.0 |
Term.toString() | 47.0 | 2.0 | 26.0 | 27.0 |
Term.toTerms() | 15.0 | 1.0 | 7.0 | 11.0 |
Average | 3.91 | 1.42 | 2.76 | 3.188 |
从方法分析的结果表格中,可以看出复杂度极高的几个方法,termsMultipy()
,toString()
,equals()
,diff()
等基本都是 Term
类中的方法,从实现逻辑上来说,这几个方法都用到了多层循环的嵌套,为了追求思路上的连贯而导致复杂度过高,应该是可以进一步地优化。在架构不错的情况下,我的代码过于冗杂,有几个方法里面的实现逻辑都较为相似,这一点应该可以更加简化,降低复杂度。
Bug分享
在公测阶段,我对自己的代码主要是通过自主创造数据(手搓)和借用他人评测机(白嫖)进行测试,发现过的 Bug 包括但不限于:
-
hw2 中三角函数嵌套解析时,如 s i n ( c o s ( x ) ) sin(cos(x)) sin(cos(x)) ,会变成 s i n ( s i n ( x ) ) sin(sin(x)) sin(sin(x)),这是因为我一开始使用 static final 修饰
Trigonometry
类中的 kind 属性,导致其成为静态变量,在第一个 sin 的初始化后 kind 固定为 1 (代表三角函数为 1 )。 -
hw3 中在求导的过程中,对如 s i n ( x ) ∗ c o s ( x ) sin(x)*cos(x) sin(x)∗cos(x) 这种表达式应该求导为 c o s ( x ) 2 + s i n ( x ) 2 cos(x)^2+ sin(x)^2 cos(x)2+sin(x)2 ,但是由于我在对三角函数求导时判断其中因子的方法实现有问题,导致会在判断到因子一致时的三角函数合并存在 bug ,会出现 RE 的情况。
在强测和互测中,我都没有出现错误的点或者被人 hack,个人认为是设计的思路比较清晰,而且没有过多地进行特判的功劳。同时,因为我对于各种边界样例的认识不足,也没有使用这些数据去测试别人的代码,最终在这一单元中也没能 hack 到他人。
心得体会
初见 OO 之时,我被 hw1 所震惊,迟迟不知道如何着手开始 coding,在完成了 training 后有了思路然后“一路狂奔”,将一周中的大部分时间都放在了完成这 OO 第一次作业上。见到 hw2 和 hw3 时,确是还有些迷惑,但却不似初见时的彷徨,能“有条不紊”地逐步实现新要求…然后开始 debug 。最终看到提交的代码通过了所有评测的数据之后,心中也会感到充实和激动。从这次作业中,我学习到了面向对象中接口的实现与用法,也了解了递归下降的功能,将这些实际地运用于自己的代码中确实是让我获益良多。
在这单元的作业中,虽说要求基本实现,但还是有些许遗憾。首先就是输出表达式的化简不够彻底,许多可以实现的化简,如 s i n ( x ) 2 + c o s ( x ) 2 = 1 sin(x)^2 + cos(x)^2 = 1 sin(x)2+cos(x)2=1 , 2 ∗ s i n ( x ) ∗ c o s ( x ) = s i n ( ( 2 ∗ x ) ) 2*sin(x)*cos(x) = sin((2*x)) 2∗sin(x)∗cos(x)=sin((2∗x)) 等都没有花时间去写。除此之外,还有评测相关,没有利用剩余的时间搭建评测机对自己的代码(还有 hack 别人时)进行测试。希望能在下一单元的作业中有所精进。