程序结构分析
类图
如图所示,整体思路为输入递归下降解析、表达式计算和展开。
本架构较为优异的地方在于扩展性以及关注最小单元。将输入转化为多项式以及求导的方法均在各Factor
子类中实现,Poly
类则承担同类项合并以及将多项式转化为String
形式的功能,使得当增添新的因子时较易扩展;而最小单元Unit
类型的设定,使得合并化简时较易比较。
待改进之处:
- 将
Factor
父类改为接口会更契合其功能,写的时候是由于对继承更熟悉所以选择了继承。 - 遵循讨论区中提及的命令-查询分离原则,以及研讨课时涉及的SOLID原则对代码进行调整。在第二次作业时,由于第一次作业优化时在一个有返回值的方法内部对对象进行了修改,从而导致了bug。
度量
代码规模与方法复杂度总览(使用Statistic
和MetricsReload
插件)
方法 | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Total | 189.0 | 102.0 | 210.0 | 220.0 |
Average | 2.3333333333333335 | 1.2592592592592593 | 2.5925925925925926 | 2.7160493827160495 |
以下节选高复杂度方法进行分析:
- 内部使用了大量的条件判断语句,如
switch
段、if…else
嵌套等,使得复杂度偏高 Unit.toString()
与Poly.toString()
方法相互调用频繁,Parser.parseFactor()
在一定条件下调用Parser.parseFactor()
和Parser.parseExpr()
,Lexer.Lexer(String)
在将信息储存于token流时需要反复调用Token
的方法,从而使得iv(G)
较高
方法 | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Unit.toString() | 13.0 | 1.0 | 13.0 | 13.0 |
Poly.toString() | 14.0 | 1.0 | 10.0 | 10.0 |
Parser.parseFactor() | 25.0 | 8.0 | 21.0 | 21.0 |
Lexer.processing(String) | 20.0 | 1.0 | 10.0 | 11.0 |
Lexer.Lexer(String) | 20.0 | 1.0 | 18.0 | 18.0 |
Definer.addFunc(String) | 7.0 | 4.0 | 4.0 | 4.0 |
架构设计体验
如图,在建立初始架构时参考了往届学长的博客,关注最小单元,也因此在迭代过程中没有进行重构,而只是进行了增添和细节的修改。
假设下一次迭代是增加SinFactor
和CosFactor
三角函数因子:
在Factor的子类中增加三角函数因子,实现三角函数的toPoly()
,derive()
与deepClone()
方法
将最小化单元设置为 Unit = Σ[cxªexp(Poly) ∏sin(Poly) ∏cos(Poly)]
,修改Poly
类中Unit
的equals
判定和hashCode
值,以及相乘法则和化简。
bug分析
在第二次作业强侧时,由于在将exp
内部转化为String
时,误将某一个分支的(
打为((
,从而引发bug。从上文度量分析中可以看出,出现bug的toString()
方法圈复杂度较高,约为平均值的5倍。由于化简需要进行较多的条件判断,暂时未想到较好的降低复杂度的方法,智能针对各分支加强对该方法的测试以避免bug。
互测时主要进行的hack策略:
- 查看源代码里面有关细节的处理,如预处理时对多个加减号的处理,是否使用BigInteger,函数解析时是否对exp和形参进行替换等
- 借助评测机运行,以及针对新增条件和上届博客中的常见bug自行构造数据
优化
性能的优化主要体现于第一次作业,对最小化单元系数为0,1,-1,指数为0,1的情况进行了化简,并在最终输出时将系数为正的项调前。
将正项调前的优化方式没能一直保持正确性,在第二次作业中由于增加了指数函数,使得指数函数括号内转化为字符串需要调用Poly.toString()
方法,在递归调用中引发了bug
bug代码:
public String toString() {
...
Unit item = iterator.next();
if (mark == 0 && item.ifPlus()) {
Unit cloned = item.deepClone();
ans = cloned.toString();
iterator.remove();
...
}
...
return ans;
}
由于先调用了toString()
,使得在转化Poly
类型时总会不断进行remove
致使输出残缺。代价较小的修改方式是交换ans = cloned.toString()
与iterator.remove()
的位置,但更好的方式是遵循高内聚,低耦合,参考讨论区中所述,将命令与查询分隔开,即不在有返回值的toString()
方法中修改对象(units
)。
第二、三次作业时,出于正确性和时间限制的考量,没有对性能进行进一步的优化。
心得体会
- 进一步巩固了java基础知识和设计模式等相关内容,虽然经历了OOPre,但过了几个月的时间记忆已经较为生疏。完成本单元实验和作业的过程中进行了重温,使得对基础知识点的记忆更为深刻,同时也增进了对递归下降的了解;
- 对于架构可扩展性和优化的权衡,“高内聚,低耦合”目标的追求。讨论区中的《命令-查询分离原则及其在接口设计中的应用》一文给予了我极大启发,那时刚好经历了相应的历程,在有返回值的函数中修改了对象属性引发bug,而其读写分离一词使我恍然大悟,一个功能为查询操作的函数不应产生任何副作用,而功能为修改对象状态的操作则不应返回任何值。如果违背,在相互调用时将可能出现意想不到的bug;
- 感谢OO课程组公众号,助教,往届博客,讨论区以及研讨课的分享!
未来方向
希望可以对exp部分的性能优化提供一些思路或相关资料(☆▽☆)