OO-第一单元作业总结

作业内容介绍

OO第一次作业的内容是实现一个支持自定义函数及三角函数、求和函数的多项式化简程序

UML类图

image

其中,各个类的含义如下:

.
├── computation 			(用于实现表达式记录及化简计算的包)
│   ├── Expression.java		(表达式类)
│   ├── Factor.java			(因子类)
│   └── Term.java			(项类)
└── parser					(用于解析输入的包)
│   ├── ExprParser.java		(解析输入表达式的包)
│   ├── FuncParser.java		(解析输入自定义函数的包)
│   └── TermParser.java		(解析一个子项的包)
├── Main.java				(主类)

此程序整体上由两个包组成,parser包和computation包。前者用于将输入的表达式转换为程序利用computation抽象描述的表达式,computation类中存储着三个“可运算”类,支持基本的加,乘法运算。parser类中对于输入表达式的解析,实际上是分别对输入表达式的逐个元素解析,之后按照输入调用computation中的不同计算方法,如乘法,加法,乘方,最终得到完整的表达式。

在computation阶段,实际上是有一些各个类之间的公共方法的,比如simplify方法,selfMul方法,selfAdd方法。但是在实现过程中,并没有用统一的抽象接口或者虚类描述。实际上在第一周的任务中,我确实采用了一个Computable的接口类,统一描述各个类对外的接口,但由于Computable类型过于多,对于每个类型,需要在每个方法中逐个判别之后针对实现方法,最终复杂度爆炸,难以维系。在后续反复思考中,意识到过于的强制强调类之间的相似性忽略差异,过度抽象是无益的。Factor的化简结果还会是Factor,Term的化简结果还是Term,Expression的化简结果还是Expression.如果我做一个接口,叫做Simplifiable接口,所有实现此接口的类都需要实现simplify方法并返回Simplifiable对象,那反而是将返回类型的信息过度模糊化了,因为Factor与Term还有Expression虽然统一为Simplifiable类,但在实现add方法,mul方法,pow方法的过程中却是千差万别的。如果我实现一个Addable类,此接口要求实现add(Addable other)这样一个方法,且Expr,Factor,Term都实现了Addable类,那在实现add方法时,就不得不分别考虑Addable是具体三个子类的情况,这就会导致一个方法需要实现非常多次。这种情况下,对于快速的开发,很难看出这些不必要的抽象有什么意义,故在我的代码中,也放弃了这种无法理解的无意义抽象,转而充分利用方法的重载,去利用语法特性完成不同类型之间的运算操作(add(Term) add(Expr) add(Factor))。

度量分析

Class OCavg OCmax WMC
computation.Expression 2.466666667 6 37
computation.Factor 2.090909091 4 23
computation.Term 2.421052632 7 46
Main 2 2 2
parser.ExprParser 6 6 6
parser.FuncParser 1 1 1
parser.TermParser 12 39 60

从度量分析的结果来看,代码整体复杂度较高,尤其是Parser部分。这部分复杂的主要原因是,对正则表达式的掌握不佳,不敢完全信任正则表达式,故解析部分完全使用字符的自动机完成。整个ExprParser 以及 TermParser实际上都是复杂的FSM,造成了条件判断以及循环很多,复杂度上升的情况。

个人bug分析

在第一次作业中,存在非常多由于浅拷贝造成的问题,后放弃代码,重构进行第二次作业,并没有进行问题的修复。

在第二次作业中,出现了若某项中常数为1,且输出的第一个因子为三角函数因子,第二个为变量因子时候,连接两个因子的乘号将会消失。造成这个问题的原因是在Factor类中的toString方法中,忘记了在输出三角函数的分支中,标志非第一项的flag值。

在第三次作业中,首先是对于sum函数中上下界超过int范围的情况无法处理。其次是对于cos((-sin(x)))这种情况,由于没有判定带符号的非常数因子不是因子,造成输出格式错误。

两、三次作业中出现问题的是Factor类的toString和Term类的toString,在度量分析中的复杂度,高于Term类以及Factor类中的平均水平,但均远小于parser中的TermParser。TermParser却没有出现问题,而toString问题频发。究其原因可能是,TermParser严格按照形式化描述去实现状态机,有设定好的实现目标。而toString实现则没有那么小心,相反比较随意,没有太在乎形式化描述的问题。

发现别人bug所采用的策略

实际上我没有通过任何方式去尝试发现自己或别人的bug

架构设计体验

架构设计确实非常的重要,好的设计可以事半功倍,大幅度节约需要的工程量,调试难度,甚至可以使得代码写一边不需要调试就能直接工作。

我在第一次作业中,过度的抽象了Expr、Term与Factor为Computable接口。并要求Computable接口需要实现了一系列运算函数如add sub mul pow simplify等。这种设计造成了实现Expr、Term、Factor类无比艰难(每个类的每个方法都需要考虑输入Computable类为Expr、Term、Factor)的情况,且由于架构设计阶段,对于浅拷贝,深拷贝没有严格明确的定义,带来了很多的问题,对于第一次作业,仅仅是去一个括号的简单任务,也完全没有必要实现成这样。Computable接口作为核心的第一次代码框架,最终使得代码不可维护,不得不重构。

在第二次作业中,放弃了没有意义的抽象,且规定了所有拷贝的地方采用深拷贝,重构代码,这次很快就解决了问题,也基本没有花费时间调试(没有测试也导致出现非常低级的错误)。

第三次作业沿用第二次代码,基本没有很大的改动,这一点倒确实能体现架构优势带来的可拓展性。

心得体会

  • Java的继承:说实话在此次作业中完全没有体会到继承的优势,相反由于第一次的问题架构,可能会对继承的使用保持谨慎和小心,仅仅在必要的时候才去抽象。
  • Java的封装多态:Java的这两个特点在此次作业中有实际的感受到。封装可以屏蔽不需要的信息,而多态可以使用到类型信息,省略一些麻烦的判断,方法选择逻辑。
  • 语法解析:这方面由于时间原因,并没有过多去涉及了解,但我觉得应该重点学习一下,会是非常实用有效的重要知识,也会成为后续编译课程学习的基础。
posted @ 2022-03-26 14:21  Dofingert  阅读(40)  评论(0编辑  收藏  举报