BUAA_2022面向对象_第一单元总结

BUAA_2022面向对象_第一单元总结

O、写在前面

总的来说,第一单元尽管涵盖了面向对象这个名词的大多数含义,但事实上其难度跨度并不大,更多的是给予我们一定的时间来适应这门课的一些特征。根据我自己的理解,这门课最为重要的两个特征为:合作共赢、崇尚设计。

合作共赢:与该词相对的是零和博弈,但实际上更贴切的是闭门造车。面对这样一门新的专业课,大部分人都不敢自信的说自己凌驾于课程之上,我们的知识面小而散,很多时候需要助教加以指导才能逐步搭建其自己的成果。那么合作在此扮演什么角色呢?积沙成塔,积流成河。也许每个人的知识面都只是一小部分,每个人的想法都只在某一个方面拥有亮眼之处,但是通过交流就能将点子汇聚成一个完整的设计,这对一个难以上手的任务来说是弥足珍贵的。

崇尚设计:当我们在强调层次架构的时候我们在强调什么?其本质,还是设计。面向对象以对象为单位,与之相随的有八大设计原则,其难点在此,但其魅力亦在此。如何减少耦合性,如何满足单一职责、开闭原则等设计理念是至关重要的,不论是漏洞查找,还是程序拓展,亦或是局部重构,都需要一个灵活明了的架构。往往从一开始的第一次作业,就决定了本单元的艰难与否,这与架构是分不开的。

一、关于三次作业

为何对上述两个特征印象如此深刻,那必定是我吃过这俩的亏。在第一次作业时,尝试采取递归下降的方式进行解析,但是由于“没想好就动手”,在倒数第一天遇到de不出来的bug,四处翻找也没能在杂乱的设计中找到一个合理的bug解释,无奈只能临时跳转预解析。另外,也是犯了闭门造车的老毛病,递归下降的算法没有理解透彻,写的时候只是根据浅显的认识加以模仿,导致后续debug的时候没有一个明确的思维指引。而之后,权衡其他课程的时间,终究没能选择重构。

1.1 存储方式选择

言归正传,既然走上预解析的不归路,那就暂且不谈解析方法,而专注于存储方式与化简技巧吧。下面附上最终三次作业的UML图。

对于存储方法,广泛使用的有两种:寻求一般形式、(递归)建树。下面对这两种进行详尽的分析。

1.1.1 寻求一般形式

这对于第一次作业是再简单不过了,没有sin和cos的干扰,我们需要考虑的范围仅仅是多项式环F[x]。在该范围中,一个多项式可以仅仅用指数、系数构成的节点链接而成,于是很容易想到利用一系列带两个属性:coefficient、exponent,的term来存储整个多项式结构。对于简单的合并操作,加法比较exponent然后操作coefficient,乘法coefficient相乘、exponent相加,几乎没有任何难点。

第二次作业增加了sin和cos,如果在寻求一般形式,则形式如下:

$$
f(x)=a*x^b*sin^{m_1}*sin^{m_2}*...*cos^{n_1}*cos^{n_2}*...
$$

仍可以采用单个term的形式存取这一个一般形式。其优点显而易见,即架构尤其简单,只需要将term进行相加即可;其缺点也非常明显,即在这一个term类中,需要实现x、sin、cos这三者的计算操作,这直接引起讨论的情况数大大增加,不论是代码量还是出bug概率都大幅提升。

如果拿单一职责与开闭原则来比对,由于一个类实现几乎全部计算,显然不满足单一职责;另外,三角函数和多项式项集合在一起,如果另加其他函数,则需要在每种分支结构中添加新的分支,显然也不满足开闭原则。因此,这样的构思是完全面向问题设计,如果再继续拓展下去,重构不可避免。

1.1.2 (递归)建树

至于为什么在递归外加括号,想必大家也很清楚。可以说,在第三次允许嵌套之后,递归存储的方法成为“首选”方法。运算式本身就是一个多层嵌套结构,其本质,也是一个类似b+树的结构:非叶子节点——多元运算符;叶子结点——基本运算单位。因此只要这个树建好了,就可以通过后序遍历,递归进行:获取操作对象(子节点),获取操作符(本节点),得到本节点的运算结果,最终就能在根节点获取全部的运算结果。

第一单元作业全部采用BNF形式化描述,其递归描述的方式为我们直接提供了递归存储的思路。于是很容易就能构建出上面UML图的架构:顶层formula表征只定义了加法的多项式,子类term表征只定义了乘法的多项式,而factor则表征乘法的单元对象,即a*x^b与三角函数,而在三角函数中又用content存储顶层的formula,由此构成递归存储结构。

这样存储的优点:具有灵活性和可拓展性,并且对化简友好。相较于一般形式的方法,递归存储方式将表达式结构划分为一个个小的要素,可以分别定义各要素之间的运算,这样为履行单一职责提供了天然条件;另外对要素进行了分类,因此添加其他运算单位时可以作为新的类,并继续定义此类与其他运算单元的交互方法(运算)。有关化简下面会谈到。

1.2 复杂度分析

下面是使用idea圈复杂度分析插件MatricsReloader的分析结果

aim.Formula4.38461538461538510.057.0
aim.Term 3.8461538461538463 13.0 50.0
aim.Parser 3.75 12.0 30.0
aim.Variable 2.1 8.0 21.0
Main 2.0 2.0 2.0
aim.Triger 1.8181818181818181 6.0 20.0
aim.Factor 1.1666666666666667 3.0 14.0
Total     194.0
Average 2.8529411764705883 7.714285714285714 27.714285714285715

这是排序后的类复杂度评估结果,可见,Formula类由于几乎所有运算都需要从该类开始,并且下述的几个主要方法都以该类为顶层展开,Term紧跟其后,可以说,有这样的结果是不言而喻的。

aim.Term.toString()38.05.016.016.0
aim.Formula.simplify() 21.0 1.0 9.0 10.0
aim.Parser.parser() 15.0 1.0 8.0 12.0
aim.Term.trigerCompose(Term) 14.0 1.0 10.0 10.0
aim.Formula.add(Formula) 13.0 5.0 8.0 9.0
aim.Formula.equals(Formula) 12.0 7.0 5.0 8.0
aim.Term.addAbility(Term) 12.0 7.0 4.0 8.0
aim.Formula.need() 11.0 6.0 8.0 10.0
aim.Triger.toString() 11.0 3.0 6.0 6.0
aim.Variable.toString() 11.0 4.0 7.0 8.0
aim.Formula.add(Term) 10.0 4.0 6.0 6.0
aim.Term.simplify() 9.0 3.0 7.0 7.0
aim.Parser.isTerm(String) 8.0 5.0 6.0 7.0
aim.Term.multiply(Term) 8.0 4.0 5.0 5.0
aim.Factor.equals(Factor) 7.0 3.0 3.0 5.0
aim.Formula.multiply(Formula) 7.0 2.0 5.0 6.0
aim.Triger.multiAbility(Factor) 6.0 4.0 4.0 4.0
aim.Formula.toString() 5.0 4.0 5.0 5.0
aim.Formula.addUp() 4.0 1.0 4.0 4.0
aim.Parser.getTwoOperator(String[]) 4.0 1.0 5.0 5.0
aim.Triger.composeAbility(Factor) 4.0 1.0 7.0 7.0
aim.Variable.tackle(String) 4.0 1.0 4.0 4.0
aim.Term.hasTriger() 3.0 3.0 1.0 3.0
aim.Triger.equals(Factor) 3.0 2.0 5.0 5.0
aim.Variable.equals(Factor) 3.0 2.0 3.0 3.0
aim.Parser.computeNeg(String[]) 2.0 1.0 2.0 2.0
aim.Parser.computePos(String[]) 2.0 1.0 2.0 2.0
aim.Parser.computeTriger(String[]) 2.0 1.0 2.0 2.0
Main.main(String[]) 1.0 1.0 2.0 2.0
aim.Factor.myClone() 1.0 1.0 2.0 2.0
aim.Formula.myClone() 1.0 1.0 2.0 2.0
aim.Formula.neg() 1.0 1.0 2.0 2.0
aim.Term.equals(Term) 1.0 1.0 2.0 2.0
aim.Term.myClone() 1.0 1.0 2.0 2.0
aim.Factor.Factor() 0.0 1.0 1.0 1.0
aim.Factor.composeAbility(Factor) 0.0 1.0 1.0 1.0
aim.Factor.getCoefficient() 0.0 1.0 1.0 1.0
aim.Factor.getContent() 0.0 1.0 1.0 1.0
aim.Factor.getExponent() 0.0 1.0 1.0 1.0
aim.Factor.getType() 0.0 1.0 1.0 1.0
aim.Factor.multiAbility(Factor) 0.0 1.0 1.0 1.0
aim.Factor.setCoefficient(BigInteger) 0.0 1.0 1.0 1.0
aim.Factor.setExponent(BigInteger) 0.0 1.0 1.0 1.0
aim.Factor.toString() 0.0 1.0 1.0 1.0
aim.Formula.Formula() 0.0 1.0 1.0 1.0
aim.Formula.addTerm(Term) 0.0 1.0 1.0 1.0
aim.Formula.peek() 0.0 1.0 1.0 1.0
aim.Parser.Parser(List) 0.0 1.0 1.0 1.0
aim.Parser.makeFormula(String[]) 0.0 1.0 1.0 1.0
aim.Term.Term() 0.0 1.0 1.0 1.0
aim.Term.addFactor(Factor) 0.0 1.0 1.0 1.0
aim.Term.getCoefficient() 0.0 1.0 1.0 1.0
aim.Term.getFactors() 0.0 1.0 1.0 1.0
aim.Term.setCoefficient(BigInteger) 0.0 1.0 1.0 1.0
aim.Triger.Triger(String, Formula) 0.0 1.0 1.0 1.0
aim.Triger.getCoefficient() 0.0 1.0 1.0 1.0
aim.Triger.getContent() 0.0 1.0 1.0 1.0
aim.Triger.getExponent() 0.0 1.0 1.0 1.0
aim.Triger.getType() 0.0 1.0 1.0 1.0
aim.Triger.setCoefficient(BigInteger) 0.0 1.0 1.0 1.0
aim.Triger.setExponent(BigInteger) 0.0 1.0 1.0 1.0
aim.Variable.Variable(BigInteger, BigInteger) 0.0 1.0 1.0 1.0
aim.Variable.Variable(String) 0.0 1.0 1.0 1.0
aim.Variable.getCoefficient() 0.0 1.0 1.0 1.0
aim.Variable.getExponent() 0.0 1.0 1.0 1.0
aim.Variable.multiAbility(Factor) 0.0 1.0 1.0 1.0
aim.Variable.setCoefficient(BigInteger) 0.0 1.0 1.0 1.0
aim.Variable.setExponent(BigInteger) 0.0 1.0 1.0 1.0
Total 255.0 123.0 203.0 225.0
Average 3.75 1.8088235294117647 2.985294117647059 3.3088235294117645

经过排序可以发现,复杂度最高的一个是term的toString方法,这是由于优化输出的需要,必定会在toString中进行完备的讨论。譬如,系数为0或1的情况,指数为0或1的情况,有三角函数或没三角函数等等,这样一来,嵌套if的层数增多,加上factor的复杂度,才导致如上结果。另外,我每次调用化简方法simplify都是在toString前调用,因此也会增加其复杂度。排名第二的formula的simplify(化简)方法,也是由于多次遍历元素导致的复杂度上升。除此之外,圈复杂度高的方法基本都是“判断类”方法,这类方法需要进行大量比较,对于第二种存储方式,由于其“深度”加深,因此在遍历比较的时候需要多层嵌套的循环,这自然也是其复杂度高的原因。

因此在设计的时候尽可能减少if嵌套与循环嵌套是减少圈复杂度的关键方法。

1.3 bug分析

按理来说,预解析是不该有bug的,但是这动手比脑子块的老毛病让我在写代码的时候总是“先下手为强”,最终不得不“拆东墙补西墙”,下次一定多多注意。

前两次作业我确实采取第一种存储方式,第一次由于提交匆忙,有一处的String转Biginteger仍采用的是long过渡的形式,导致这一个bug被六个人围攻,着实不该,此处姑且不谈。着重谈谈第二次作业遇到的天坑。

1.2.1 浅克隆与深克隆

惭愧的是,在我写第二次作业之前,就有了解讨论区吴佬发起的讨论,但是在实际运用的时候,还是由于理解不深导致致命bug。沿用了第一次的架构,我一直都没有考虑这个问题。加法乘法运算,我一开始采用的是自存储的方式,即

$$
A.add(B),\ A.multiply
$$

这两种计算的结构都是直接修改A,之前在作业一中,由于Biginteger的不可变特性,这个漏洞没有显现,但是在作业二中操作对象变为定义的类,因此在多次进行上述操作的时候,这致命的bug才会逐渐浮现。可以看出,这个bug的关键在于,如果A存储结果,那么B必须不可变,即除了方法内不能更改B,上述运算形式还必须具有方向性,绝对不可以B.multiply(A)

针对这个bug,我在第三次作业重构中采取:每种运算都用一个新的对象来存储。这对加法而言好说,但是乘法还是自存储更为方便(毕竟依据一个个factor重新设置term着实没必要),因此我采用先将A进行克隆,然后再与B进行乘法,将结果加入到另一个预先设定好的容器中。对于clone,我是通过在各类中统一定义了myClone方法,利用Serializable接口利用序列化克隆实现的。

1.2.2 toString

如果采用第一种存储方式,那么toString一定需要小心,因为如果只定义了term、并且还有化简想法的我们,就会如上面复杂度分析那般所言。众多if嵌套就必须考虑全面,否则就会出错。另外一个bug集中的地方是化简的部分,提交最后一版的时候由于化简算法不够成熟,存在些许bug而最终选择简单化简的一版。从这里可以看出,复杂度更高的模块,其出bug的概率也越大,上面分析到的其很大一部分原因是由于大量if嵌套与循环嵌套,这正是我们debug需要注意的地方。

在某些方面来看,在term中toString,不如分散到factor中进行toString,这也是第三次作业重构的一大原因。

1.4 化简分析

第三次作业着重化简,我的架构在上面的UML图中十分显然,之前也说过这样的架构对化简友好,这里就详细谈谈其优点。

化简的关键点有二:能否化简、如何化简。

我的设计中将二者分开实现,“能否化简”用\#Ability的形式定义方法,而化简操作,则直接在toString中调用。

1.4.1 简单长度化简

对于基本的长度化简此处做简单概括。

首先是合并操作,加法的系数合并,乘法的指数合并,以及有0的时候的消除合并操作。当然,对于x**2转化为x*x就不必多说了。需要说明,这里需要考虑到消除合并时,子类列表为空的情况,譬如formula的terms列表里,由于系数全消为0而全被移除列表,导致terms.size()==0,这种情况需要特判。

其次是顺序移动操作,讲正数提到表达式开头可以省去一个符号的长度。

最后是三角函数括号的化简,对于有符号整数与x幂次的情况,sin和cos中不需要两层括号的,这可以通过一个need函数判断是否要加括号来实现。

1.4.2 三角函数合并化简

三角函数的平方和:我的做法是,在formula层级遍历term,寻找每对term中可能可以相加的指数为2的三角函数,利用一个composeAbility方法进行判断,如果可以,则对sin边消除sin^2成分,而对cos这一边,系数需要减去sin那一边的系数。

二倍角操作:在单个term中进行搜索,同样记录可能可以合并的三角函数,利用doubleAbility方法进行判断,如果可以,则消除cos成分,并令系数除以2(前提是系数为2的倍数,因为本次作业不支持小数),令sin内content的系数double。

二、关于hack经历

只能说从提心吊胆到心平气和。最初对自己的设计确实有所不自信,一方面来源于对java语言本身的不熟悉,担心有语言特性上的bug,另一方面还是自己架构的设计问题,缺乏此类经验,没有想得特别清楚。从最开始被hack5、6次,到最后0/20的hack结果,不仅体现了代码架构逻辑的进步,也说明了我的设计方法信心的积攒。

对于我hack别人,第一次兢兢业业下载别人的代码进行阅读,用数据驱动来发现存在的bug,然后针对bug进行hack。之后两次的hack,都是借助数据生成器,初此之外,加上自己能想到的边界数据来进行测试。只能说,星期天需要处理的作业有点多,留给hack的时间着实不多。。。

总体而言,所遇见的bug集中在两个方面:

  1. 扩展后的成分没有考虑到老的特殊情况。譬如前导零、带符号的指数、数据范围(未用biginteger)等等,以及最终结果为0或1的输出情况处理。

  2. 扩展后的乘法,即sin、cos分别存在的两个多项式之间的乘法或乘方。一般来说这是由于合并的结果。

基本上,如果提交前在这几个方面多多测试,在配以几个较为复杂的测试样例,在hack途中可能出现bug的概率不大(前提是架构明确。。。)

三、心得体会

在小组讨论过程中,我们组尤其讨论了撰写博客的意义所在,其中一大讨论结果就是“积累经验”。经验一次看似一笔带过般简单,但事实上却浓缩着整个大脑的抽象工作。它负责将我们所学的知识串联起来,并抽象成一块块有关系有结构的树型知识链,尽管日后我们不一定记得详细的知识点,但是却能联想到这方面的主要思想和易出错点,知识点忘了可以重新查找,但是“经验”在未来做项目时扮演者极其重要的角色。当然,这样说并不意味着知识点不重要,能记住知识点固然更好,只不过我认为经验更为重要。

经过这三周的学习,最大的收获还是文前所言的两个课程特征,这也是课程对我们提出的要求。如何能在一周为数不多的大段时间内,从设计到实现一个解决问题的方案,不仅考验知识,更是考验能力。我们需要合作,我们不仅要有自己的想法,更要善于学习他人想法,并在此过程中改善自己的想法,这样才能锻炼出短时间内得出最佳方案的能力;我们需要想清楚再动手,这个阶段才是最耗费时间的,如果将架构设计好之后,bug会少,重构会少,可谓一劳永逸。

希望下一单元能搞定这两方面的问题!

 

 

posted @ 2022-03-26 15:06  tsyhahaha  阅读(29)  评论(1编辑  收藏  举报