Part0 前言
江山留胜迹,谁人复登临?当我在脑海中构想这篇博客时,并不完全把它当成一次作业,而是当成为后来人提供便利的记录。诚如Goths学长所言:
“写博客是一种非常好的知识输出方式,而知识输出不仅能够巩固自己的知识,同时很重要的一点是知识输出后可以创造更多的价值,即,可以供后来者在去除糟粕后参考学习。”
故而此篇中,我将以面向求知者的口吻,讲述我在OO第一单元的故事。应作业要求,笔者需要介绍自己并不那么完美的架构特点。除此之外,一些迭代过程中的亲身经历和思考结果,是我更想分享的。
Part1 度量分析:自我检讨环节
芝士两张展示factor和token的图,似乎大家都在追求减少类数。我上述这些类的绝大部分仅作为占位符用,只是图个结构统一,但单纯为了区别类别用属性标记即可,实在不必要弄这么多类,后来者莫要学我。
下面看看有关主要架构的类:
经过预处理器,词法分析与语法分析器处理,表达式字符串被转化为token,最后存储为表达式对象。translator
先将对函数调用进行处理,再调用multiplier
进行去括号时计算。
同时,将“命令”设计为了对象自己的方法。比如表达式求导,合并同类项等,都设计为了每种对象自己的方法。虽然没有调用其他的类,但expr,term的复杂度就增加了。
复杂度分析:
名词解释:
OCavg:平均操作复杂度
OCmax:最大操作复杂度
WMC:加权方法复杂度
Demo | 2.0 | 3.0 | 6.0 |
---|---|---|---|
Exp | 1.8 | 6.0 | 36.0 |
Expression | 2.8 | 8.0 | 84.0 |
ExprFactor | 1.1818181818181819 | 3.0 | 13.0 |
Factor | 1.8571428571428572 | 7.0 | 13.0 |
Fun | 1.3333333333333333 | 3.0 | 12.0 |
Lexer | 6.0 | 14.0 | 18.0 |
Main | 1.0 | 1.0 | 1.0 |
Multiplier | 2.6666666666666665 | 4.0 | 8.0 |
Parser | 1.0 | 1.0 | 2.0 |
Preprocess | 7.0 | 13.0 | 14.0 |
Signalnum | 1.4444444444444444 | 5.0 | 13.0 |
Term | 3.1944444444444446 | 12.0 | 115.0 |
token.Comma | 1.0 | 1.0 | 1.0 |
token.Der | 1.0 | 1.0 | 1.0 |
token.Exptoken | 1.0 | 1.0 | 1.0 |
token.Funtoken | 1.0 | 1.0 | 2.0 |
token.Num | 1.0 | 1.0 | 2.0 |
token.Opd | 1.0 | 1.0 | 1.0 |
token.OpMul | 1.0 | 1.0 | 1.0 |
token.Opt | 1.0 | 1.0 | 2.0 |
token.Para | 1.0 | 1.0 | 2.0 |
token.Tail | 1.0 | 1.0 | 1.0 |
token.Token | 1.0 | 1.0 | 1.0 |
token.Var | 1.0 | 1.0 | 2.0 |
Tokens | 1.0 | 1.0 | 6.0 |
Translator | 1.2 | 2.0 | 6.0 |
VarFactor | 1.6 | 4.0 | 16.0 |
我大量的操作都集成在了Term中。在进行层次化操作时,expr的动作是每个项执行动作;每个项的动作设计多个因子之间的联动,故而复杂度高于expr。其实term写的如此复杂仍然是因为将一些可以在factor中完成的功能拿到term中越级执行,细节的封装仍然有待提高。
方法衡量指标:
-
CogC(认知复杂度):衡量一个方法的控制流程有多困难去理解。具有高认知复杂度的方法将难以维护。sonar要求复杂度要在15以下。计算的大致思路是统计方法中控制流程语句的个数
-
ev(G):方法的基本圈复杂度,衡量程序非结构化程度的。
-
iv(G) :设计复杂度
-
v(G):方法的圈复杂度,衡量判断模块的复杂度。数值越高说明独立路径越多,测试完备的难度越大。
Expression.append(Term) | 0.0 | 1.0 | 1.0 | 1.0 |
---|---|---|---|---|
Expression.changeDegree(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
Expression.changePnc(int) | 0.0 | 1.0 | 1.0 | 1.0 |
Expression.checkDer() | 0.0 | 1.0 | 1.0 | 1.0 |
Expression.construct() | 6.0 | 4.0 | 3.0 | 4.0 |
Expression.deepClone() | 2.0 | 1.0 | 3.0 | 3.0 |
Expression.deepSimplify() | 15.0 | 1.0 | 8.0 | 8.0 |
Expression.derGen() | 11.0 | 2.0 | 6.0 | 6.0 |
Expression.derTranslate() | 1.0 | 1.0 | 2.0 | 2.0 |
Expression.divCoe(BigInteger) | 1.0 | 1.0 | 2.0 | 2.0 |
Expression.expEqual(Expression) | 11.0 | 7.0 | 5.0 | 7.0 |
Expression.expMerge() | 1.0 | 1.0 | 2.0 | 2.0 |
Expression.expReshape() | 1.0 | 1.0 | 2.0 | 2.0 |
Expression.Expression() | 0.0 | 1.0 | 1.0 | 1.0 |
Expression.generate(ArrayList, ArrayList) | 1.0 | 1.0 | 2.0 | 2.0 |
Expression.getDegree() | 0.0 | 1.0 | 1.0 | 1.0 |
Expression.getgcd() | 13.0 | 4.0 | 6.0 | 7.0 |
Expression.getTerm(int) | 0.0 | 1.0 | 1.0 | 1.0 |
Expression.getTermNum() | 0.0 | 1.0 | 1.0 | 1.0 |
Expression.getTerms() | 0.0 | 1.0 | 1.0 | 1.0 |
Expression.isSimFactor() | 2.0 | 2.0 | 2.0 | 2.0 |
Expression.isSimSignalNum() | 2.0 | 2.0 | 2.0 | 2.0 |
Expression.merge(Expression) | 0.0 | 1.0 | 1.0 | 1.0 |
Expression.mulCoe(BigInteger) | 4.0 | 1.0 | 3.0 | 3.0 |
Expression.pullCoe() | 2.0 | 2.0 | 2.0 | 2.0 |
Expression.putout() | 4.0 | 1.0 | 4.0 | 4.0 |
Expression.setDer() | 0.0 | 1.0 | 1.0 | 1.0 |
Expression.shallowSimplify() | 11.0 | 4.0 | 7.0 | 7.0 |
Expression.show() | 12.0 | 6.0 | 6.0 | 7.0 |
Expression.translate() | 1.0 | 1.0 | 2.0 | 2.0 |
Term.build_sim(BigInteger, BigInteger, BigInteger, BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
---|---|---|---|---|
Term.construct() | 12.0 | 4.0 | 5.0 | 8.0 |
Term.deepClone() | 3.0 | 1.0 | 4.0 | 4.0 |
Term.deepSimplify() | 1.0 | 1.0 | 2.0 | 2.0 |
Term.derGen() | 19.0 | 2.0 | 11.0 | 12.0 |
Term.derTranslate() | 6.0 | 1.0 | 6.0 | 6.0 |
Term.divCoe(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
Term.expEqual(Term) | 8.0 | 4.0 | 4.0 | 6.0 |
Term.expMerge() | 4.0 | 1.0 | 4.0 | 4.0 |
Term.expPutout() | 1.0 | 1.0 | 2.0 | 2.0 |
Term.expReshape() | 2.0 | 1.0 | 3.0 | 3.0 |
Term.expShow() | 14.0 | 1.0 | 7.0 | 7.0 |
Term.firstShow() | 3.0 | 1.0 | 3.0 | 3.0 |
Term.generate(ArrayList, ArrayList) | 11.0 | 1.0 | 9.0 | 9.0 |
Term.getCoefficient() | 0.0 | 1.0 | 1.0 | 1.0 |
Term.getD_x() | 0.0 | 1.0 | 1.0 | 1.0 |
Term.getD_y() | 0.0 | 1.0 | 1.0 | 1.0 |
Term.getD_z() | 0.0 | 1.0 | 1.0 | 1.0 |
Term.getExp(int) | 0.0 | 1.0 | 1.0 | 1.0 |
Term.getExpNum() | 0.0 | 1.0 | 1.0 | 1.0 |
Term.getPnc() | 0.0 | 1.0 | 1.0 | 1.0 |
Term.getSubExprNum() | 0.0 | 1.0 | 1.0 | 1.0 |
Term.isSimFactor() | 10.0 | 5.0 | 10.0 | 10.0 |
Term.isSimSignalNum() | 1.0 | 1.0 | 2.0 | 2.0 |
Term.make(Factor) | 9.0 | 1.0 | 9.0 | 9.0 |
Term.mulCoe(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
Term.noExpShow() | 10.0 | 1.0 | 6.0 | 6.0 |
Term.plus(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
Term.pullCoe() | 2.0 | 2.0 | 2.0 | 2.0 |
Term.putout() | 2.0 | 1.0 | 3.0 | 3.0 |
Term.selfMul() | 6.0 | 1.0 | 4.0 | 4.0 |
Term.show() | 2.0 | 1.0 | 3.0 | 3.0 |
Term.simply() | 1.0 | 1.0 | 2.0 | 2.0 |
Term.subExprMul() | 3.0 | 1.0 | 3.0 | 3.0 |
Term.Term(int) | 0.0 | 1.0 | 1.0 | 1.0 |
Term.translate() | 3.0 | 1.0 | 4.0 | 4.0 |
这里列举我expr和term中的两个方法为例,term中的一些复杂度还是太高了,这点我编程时也深有体会。其实为了执行一定的功能,方法中各类的复杂度总水平应该是相当的,出现复杂度过高的情况,除少数的架构问题外,大多是因为协作的类之间分工不合理所导致。
Part2 架构设计
在第一次作业之前,我认真学习了递归下降思想,提前实现了括号嵌套的处理方法。感觉学习这个思想有一个要点,就是在体会表达式拆分的过程中,要尽力理解每种结构的特点与采用某种设计的原因。课程组的训练代码中,将表达式的每个部分用加、减、乘号连接,大家要注意,这种抽象方法实际将表达式中任意部分视为等效的个体,脱离了项与因子的限制,其思想与语法树高度重合,个 人认为这是最优美简介的实现,有能力的同学一定要多加探索实践。
在第二次作业中增加了自定义函数与exp因子,添加的过程中我进行了很多痛苦的修改,最终无法继续推进,只能“伪”重构。之所以说是伪重构,是因为不是调整了主要架构,而是把一些接口设计的更加规范,能在底层结构中实现的方法绝不拿到上层结构中代劳,增强统一性。在思考的过程中,我领悟了一个接口设计诀窍:命令-查询分离原则,堪称我第一单元最大收获。
相关内容有幸被老师挂在了课件上嘿嘿~浅浅炫耀下
第三次作业中,我鉴于第二次作业中新增因子的经历,没有轻易增加因子,而是针对求导因子与表达式因子结构极像的特点,为表达式因子增加了求导计算方法,也做到了把分离求导过程,避免修改原有代码。我觉的这个方法还算精巧,不知道有无偷懒之嫌。
满足上述接口设计要求的架构是否拥有优秀的可扩展性呢,让我们构思一下。
假设现在要求新增新的运算符,比如%
,规定后接符号数因子,只需在词法分析部分增加取模运算的token。在语法分析时识别到取模符号既进行运算。
而表达式的结构基础不变,式-项-因子的构成足以覆盖所有变化,新增的内容只需新增因子与对应的计算方法。由于我们遵循命令-查询分离原则,新因子的计算只需要在自己新增方法内进行计算,无需更改已写好的代码。
Part3 Bug分析
三次作业中,本人十分侥幸的没有在强测和互测环节出现bug。但是第二次作业中,我因为出现了一个意向不到的bug,导致整段程序所有的部分都出现了问题(de不完的bug啊啊啊啊),最后发现是架构上的问题时整个人只剩半条命了。
在对exp因子进行化简时,我意识到必须先将exp外的指数提入内部因子,不然时间复杂度很高。但这里不能简单的将内部表达式的每一项乘以系数,因为内部的表达式可能并不是一次幂的。也就是说,我对表达式进行相乘、计算、函数翻译等工作时,都只是对其中各项执行对应命令,却忽略了可能挂在右上角的幂数。这是因为在第一次作业中,所有对表达式的计算都是在表达式转换为多项式之后进行的。而第二次作业中却没有这个信息,那么之前的方法调用逻辑便也不再适用,由此引发了极为痛苦的代码补正工作。
其实这是一个方法前置信息的问题。在我们的程序中,有些方法的正确执行依赖一些额外的条件或“信息”。比如向表达式乘一个系数,如果我们可以执行为表达式中每个项都乘该系数,必须建立在表达式已经转化为多项式的基础上。在实际情况中,这个条件可能是由我们在demo
中调用方法顺序保证的,但是一旦我们增加一些方法,在方法中进行调用时,就破坏了这个条件保证。
我的建议是,设计方法时一定要为各种情况设计分支,即使你明确某些分支不会进入。这样不仅使得程序处理各种情况的方法更加完备,避免了上述问题;也便于我们进行调试,如果出现了上述的顺序破坏问题,我们便能立刻意识到。
而bug出现的表达式相乘、项的求导等方法不出意外的也是复杂度较大的方法。其实一个方法在设计时,程序员有一定能力可以注意应用场景中的细节,但当细节过多,复杂度过大时,程序员难免顾此失彼。所以简化方法复杂度也很有必要,方法执行逻辑清晰,程序员犯错的几率也会大大减小。
Part4 Hack策略
三次作业中我采取的主要策略是随机强测+手动构造特殊样例。
随机强测即使搭建随机数据生成器,不停运行程序以找出程序中的bug。需要强调的是数据生成器部分一定要保证数据生成的随机性和分支全覆盖性,强度不够的数据贻害无穷。更不能想当然的认为数据中的某个点一定不会有人·出错而忽略,你永远不知道别人怎样实现一个功能,又出现了什么Bug。
举笔者亲身经历为例,我与同学合作搭建评测机并在第二次作业中承担数据生成工作。当时我认为表达式中的空白符大家在预处理阶段一定全部清除掉了,所以新生成的数据不需要关注这一点。我生成的数据在自定义函数中不会添加空白符。有一位同学恰恰没处理自定义函数的空白符,通过我们评测机的检验后信心满满提交,结果强测惨不忍睹……
而手动构造样例主要有两个思路:测试边界条件与测试内存时间。
上一届有2888传奇,这一届有ltc神之一刀。
大多数人的思路都停留在表达式嵌套,然后被cost限制住了手脚。这位大神使用exp嵌套,而且卡着cost边缘构造,狼人楷模啊。
而边界条件就比如一些含有0,+-1的表达式,听说很多同学公测阶段第一法都被dx(0)打败了。
还有一个策略叫做回归测试,指的是用前一次的强测数据进行这一次的测试,听起来不可思议,却往往行之有效。部分同学的架构没有很好的实现对增加开放,对修改关闭,在不断修改中就可能引发一些新的错误。面对这样的程序,回归分析往往有奇效。
有些遗憾的是没有为了hack别人而认真的阅读代码。其实这是最锻炼OO能力的过程,在繁多甚至可能不那么优美的代码中分析架构,找出可能的漏洞,你会发现自己调试和设计代码的能力也会不断提高。
Part5 优化策略
本次优化我只做了判断exp内是否为单因子,以及提取公因数。判断单因子只需要分析exp内的多项式结构即可:如果只有一项,且项中只含有不包括系数的一种因子,就可以提出系数而省略掉一层括号。提公因数只需要遍历每一项系数,取公因数即可。注意要保证提出的公因数为正,不要把负号提取出来。
这两种优化方法都与处理过程分离,且遵循了命令-查询分离原则。实现简洁,不修改原有代码。
其实exp组合优化策略看似复杂,但我们可以只在项数较小时试图使用,这样TLE风险不会很高。但在第三次作业中有人挖掘出了更好的优化效果,大家可以自行探索。
Part6 心得体会
在进入OO的正式学习之前,我了解过一些设计模式的知识。学起来感觉就像镜花水月,总有些朦胧的感觉。对照教材上的事例可以有些理解,但换一个场景就不太能灵活运用,而且很容易遗忘。
而随着OO正式课程的学习,我在敲代码的过程中有了自己的体会,自己的尝试,改正自己犯的错误,修复自己代码里的烂码风。纸上得来终觉浅,绝知此事要躬行,多试,要多试。
而多试绝不意味着不加细致思考的码代码,OO设计最重思考,接口怎么设计,方法怎么实现,类与类之间如何协作,当整个系统都能正常运作时,我们才可以动手写代码。
Part7 未来方向
第一单元在训练过程中讲知识与架构介绍的比较详细,尤其是实验课的代码中。这确实有助于大家更好的掌握递归下降,写出更标准的实现,少走一些弯路。但如果给出的知识过于详细,大家的创造性就会降低,不利于大家进行更广泛的思考与尝试。我觉得未来的课程中对于递归下降的思想可以介绍的更加详细,也可以介绍在代码编写中怎么介绍递归下降,但是可以减少一些架构细节方面的提示,给大家更多的思考空间。