OO第一单元总结

OO第一次单元总结

本单元作业的主题是表达式分析与求导,第一次作业涉及的求导规则有加法、减法、幂次、(因项可合并为一个因子所以没有乘法),涉及到的因子有带符号整数、幂函数。第二次作业难度骤增,求导规则引入了链式法则,因子引入了表达式因子和三角函数因子。第三次作业在前面的基础上,引入了嵌套表达。

基于度量的代码结构分析

  • 第一次作业
    • 类图分析
      image
        第一次作业难度较低,除Main类之外,我使用了三个类来完成功能,分别为表达式类,项类,因子类。
      • Expression

      表达式类。此类接受main函数传来的表达式串,负责以项为因子进行解析和求导,把具体对项解析的工作分配给了项类。

      • Terms

      项类。此类解析项的各因子,并计算因子合并后的指数,以方便表达式类利用HashMap进行求导。

      • Term

      因子类。在本次作业中,因子的形式比较简单,此类只负责求导和字符串输出。

    • 度量分析
      • 复杂度分析
        • Class metrics

        image
        表中的OCavg表示平均操作复杂度,WMC表示加权方法复杂度。由表中数据分析可知,Main类不够简洁,平均操作复杂度较高;Term类加权方法复杂度较高,原因是因子输出逻辑写的比较混乱,嵌套了很多if-else结构。

        • Method Merics

        image
        image
        image
        方法复杂度指标包含圈复杂度、基本复杂度、设计复杂度三个维度。由图可知:Term.toString方法在三个维度的复杂度都较高,原因为多分支结构和多返回语句;其次复杂的是Terms.mergeterms方法,在该方法中使用了大量正则表达式去匹配合适的因子,并对解析出的因子进行了合并操作。

    • 优缺点评价

    个人认为本次作业的优点是设计得比较有层次,各个对象之间互相协作,分工明确。每个类在解析和toString输出时只需要考虑抽象层次上的解析和输出,具体工作一级一级地向下分发。本次作业的缺点是部分方法设计得太过复杂,如Term.toString和Terms.mergeterms,前者没有仔细地设计输出逻辑,考虑了很多特判进行优化,后者则没有把功能块拆解为不同的函数。

  • 第二次作业
    • 类图分析

    image
    在第一次作业的基础上,由于题目要求的增多,正则表达式的数量也随之增多,因此我增加了TermPattern的类,用来集成所有的正则表达式,便于各个类对匹配模式的访问使用。与此同时,Term、Temrs类都统一地继承于表达式因子类,便于进行统一管理。最后做的比较大的改动是,我把项分成了两种——单个因子的项和多个因子的项。

    • 单因子项
    • 单个表达式因子
      遇到单个的表达式因子,将会直接调用表达式因子的求导函数,在内部进行拆分求导。
    • 单个的一般项
      在本次作业中,除表达式因子之外的因子可以表示成一个一般的形式,即coe*x**exp1*sin(x)**exp2*cos(x)**exp3,在Term的求导函数中已经预先将该一般形式的导函数形式求出,对一个一般项求导只需要代入其系数和三个指数,便能够直接计算,计算效率高且最终形式简洁。
    • 复合形式
        复合形式即由表达式因子和表达式因子,或表达式因子和一般项组成,在对复合形式进行求导时运用的是链式法则,每次都由第一个分割因子的乘号分割,对其前后的部分使用链式法则。
      * 度量分析
      *复杂度分析
      image
      由表格所示,复杂度集中在Expression的handle方法和Term的toString方法之中,原因在于handle方法是对项的拆解函数(本来应该放在Terms类中的),使用了大量正则进行逐个因子的匹配;Term的toString方法则考虑了一般形式的输出优化问题,加入了很多控制流结构。
      优缺点评价
      本次作业的代码总行数达到了628行,一些方法的位置没有安排好,存在很多比较重复的代码。另外,拆解分析上,过于拘泥于正则表达式的使用,导致了最后的失败。
  • 第三次作业
    • 类图分析
      image
      本次作业我把所有的因子都创建了相应的类,如常数类、正弦类、余弦类、幂函数类等。除此之外,还把乘法作为单独的类提取出来。针对题目对于WF的判断,很自然地使用了一个WF异常类。
      • 设计改动

      第二次作业失败后,本次作业进行了大规模重构,从纯正则表达式匹配转换到了类似于递归下降的分析方法——按照lambda表达式逐一地对语素进行寻找。

    • 复杂度分析
      image
      image

    由图所示,ExpressionUnit在所有类中的复杂性最高,原因在于递归下降的语法分析都在ExpressionUnit内,存在很多的递归调用。如果想削减单个类的复杂性,可以考虑新建不同的节点类,把寻找不同语法节点的工作分发给这些类。

    • 层次功能设计简述
      本次作业设计了求导接口,乘法类以及各种类型的因子类的父类Unit继承了这个接口,从而可以在求导的过程中把对因子使用多态进行统一管理。
    • 优缺点评价
      个人认为本次作业的可取之处是利用了类似递归下降分析的方法,较为清晰地对表达式进行分析,可以从容应对套娃的问题。在这一点上,使用正则是比较痛苦的。而本次作业的缺陷也很明显,完全抛弃了正则表达式的应用,加上把分析的方法都放在了同一个类里面,导致单个类的长度超过了400行。进一步优化的思路应该是对于正则表达式比较好写的匹配可以通过正则表达式解决,如幂函数、带符号整数,这样做可以简化部分过程。

分析自己程序产生的bug

  • 第一次作业

第一次作业比较简单,强测和互测都没有程序都没有被找出bug。不过互测也没有hack到别人。

  • 第二次作业

第二次作业继承第一次作业的大正则,在表达式因子匹配上出了很大的问题,由于java正则不能进行递归匹配,因此最初采取的是诸如

expressionPattern = Pattern.compile("([+-]{0,2})\\(.*?\\)");

的形式匹配表达式因子,仅以两头匹配的括号作为表达式因子的特征,在后续的debug过程中才逐渐发现其存在的巨大缺陷——对于(sin(x)+cos(x))这样的式子,我的匹配策略会在匹配表达式因子的时候将表达式因子匹配为(sin(x),在修复的过程中我写了checkPairBracket这个函数,用来判断匹配对象左括号和右括号数量是否相等且是否处于首尾位置,来达到准确判断表达式因子的效果。可惜的是,在项的匹配上,继承第一次作业的思想,我仍然使用的是大正则:

TermPattern = Pattern.compile("(" + unit + ")" + "(\\*" + unit + ")*");

最初目的是为了正则匹配的统一性,同时仍然保留了原有的表达式正则,而只把checkPairBracket作为判断的辅助手段。事实证明,这样的方法是不行的,尽管这次的修复保证了不会将错误的项加入到termArrayList里面,但正则匹配器仍然可以匹配错误的表达式因子字符串,从而导致了层出不叠的错误。直到作业截至,我都没有悬崖勒马,而是深陷正则的泥潭,最终导致了第二次作业的弱测都没有完全通过。

  • 第三次作业

这一次作业出现的比较典型的bug有两个。一个是三角函数的contents属性问题,一个是WrongFormat的问题。

  1. contents
    最开始为了省事,在三角函数截取括号内的字符串时取的是(4,contents.length()-1)的子串,如sin(x**2)就可以取出"x**2",然后在求导运用链式法则对里外依次求导。这样写造成的问题是当三角函数幂次不为1,如sin(x)**2时,依照该方法截取的内容便是"x)**",显然是不对劲的。最后采取的方法是
Cos(getContents().substring(4, getContents().lastIndexOf(")")))

来解决这个问题。当然,事后反思的时候意识到也可以将三角函数的contents属性专门用来存储其内部字符串,这样就可以直接在构造函数中就传入括号内的字符串,省去了之后每次取子串容易出bug的麻烦。
2. WrongFormat
这个bug比较低级,直接导致强测中掉了一个点,罪魁祸首竟在于初始写判断WF异常时使用的是print语句,之后才采取抛出异常的方式,导致一部分的异常在检测到之后可以输出错误信息,但程序仍然继续运行,造成了RuntimeError。此bug修复时只需要将print语句替换成异常抛出语句。这种bug的出现反映了写程序时程序架构的重要性,在写具体的程序之前应该考虑好异常处理方式,否则在具体写代码时可能出现异常处理不当的情况。

总结找出别人bug采取的策略

由于本人太菜,这三次作业中只进过一次互测,因此在寻找别人bug时经验不足。在仅有的一次测试中,采取了以下的策略:

  1. 常数和0
    比较简单地试探别人的程序有没有因为优化输出的0而导致的错误。
  2. 大指数
    这个主要是检测指数存储时使用的类型是否为BigInteger。
  3. 覆盖性测试
    针对题目给出的lambda表达式对每种典型的出现形式进行覆盖性测试。

重构经历总结

这3次作业中我进行了2次重构

  1. 第一次重构。第一次重构是由于原有的架构对于因子的统一性要求太高,没有实现乘法法则,而第二次作业不能避免乘法法则的使用,因此进行了第一次重构,把所有的项都抽象为表达式因子进行处理,并且还定义了一般项的形式,可惜由于依旧使用正则表达式处理表达式因子,最终没能达到要求
  2. 第二次重构。第二次重构的原因非常显然,第二次作业没有完成正确性,第三次作业又加了新的嵌套和格式判断,思考再三只能新建工程,从0开始使用递归下降进行分析。在本次重构的经历中收获还是挺大的,之前学长虽然分享过递归下降的分析方法,但出于对新知识的恐惧,迟迟没有决心进行尝试,而这一次在作业的压力下不得不来尝试这种方法。

心得体会

在本单元的学习中,我(被迫(x))去体会了面向对象的层次化设计思想,在做题之前,先想着怎么把整体的架构设计好。除此之外,在研讨课上学习到了大佬的技术分享,比如递归下降分析的思路和java核心类的使用。

posted @ 2021-03-30 18:36  弈~忆  阅读(84)  评论(0)    收藏  举报