北航OO课程 第一单元总结

前言

由于笔者在上学期运气不佳,没有抢到OO的先导课程,在本学期面临难度更大,挑战性更强的OO课程时,笔者感到了一种深刻的力不从心。在第一单元的作业完成过程中,笔者经历了多次的重构危机和bug难题,第一次作业侥幸通过中测却因强测得分过低未进入互测,第二次作业中测十个点一点未过,笔者的信心一度受到打击。不过好在在同学的帮助下,笔者调整了自己的架构,并在第三周顺利完成了三次作业的迭代开发,在这里要首先感谢给予笔者帮助的各位同学。同时,如果发现文中出现什么错误,也感谢各位读者的批评指正。

第一单元的主题是表达式的恒等变形,主要内容是表达式的展开与化简,采用的主要方法是递归下降,下面笔者会对这三次作业的相关内容逐一进行分析。


第一次作业

设计要求

第一次作业要求读入一个包含加、减、乘、乘方以及至多一层括号的多变量表达式,输出恒等变形展开所有括号后的表达式。

类图
架构设计

第一次作业采用了推送中提供的递归下降法,利用Lexer词法解析器对输入进行处理,使得每次都能分解出所需要的一个数字或符号;利用Parser语法解析器对读入的字符依次进行处理,将其解析成表达式、项或者因子。Expr类、Term类、Variable类和Number类对读入的字符串进行分层管理,并用Factor接口集中,初步体现了层次化设计的思想。

一些不足
  • 笔者最初不熟悉String类中的一些基本方法(如预处理过程中的repalceAll方法),对字符串的处理大部分是降低到字符的阶段进行,由于测试数据相对复杂,笔者的字符处理方法常常会遗漏一些情况,导致许多bug的产生。

  • 笔者在预处理时没有考虑到"(+""(-"这样形式的输入,未对原始输入进行完全的处理。

  • 在输入的字符串解析完毕后,笔者采用最原始的多项式乘法的方式将括号展开,这种方法虽然思路简单,但在具体的实现过程中遇到了括号展开不完全,展开过程中出现空白项等诸多问题,同时也为笔者第二次作业的全面崩盘埋下了隐患。

  • 由于笔者输出过程中一直使用String类进行存储,字符串的化简自然成为一个难题,笔者最终放弃了字符串的化简,导致性能分无法获得。

基于度量的程序结构分析
  • 类复杂度分析

  • 方法复杂度分析

可以看到,复杂度较高的方法大部分为采用字符处理字符串的方法。

  • 代码规模分析


第二次作业

设计要求

在第一次作业基础上,第二次作业增加了以下几个要求。

  • 支持括号嵌套

  • 新增三角函数因子,三角函数括号内部包含任意因子

  • 新增自定义函数因子,但自定义函数的函数表达式中不会调用其他函数

类图

由于第二次作业的迭代开发过程中进行了重构,重构前的类图与第一次作业基本类似,重构后的类图与第三次作业基本类似,因此在此不放置第二次作业的类图,可以参考另外两次作业。

重构思考

在第二周,笔者在第一次作业的基础上进行迭代开发,最初想使用递归下降法处理自定义函数,但由于对这一方法的理解不够深入,笔者在这条路上行进地困难重重。最终,由于bug过多导致完全无从下手,笔者只得放弃这个方案,进而转向预处理阶段直接把自定义函数处理掉。在预处理阶段,笔者采用了套多层循环的方式处理嵌套调用的自定义函数。

while (input.indexOf('f') != -1 || input.indexOf('g') != -1 || input.indexOf('h') != -1) {
      input = funcReplace(input);
}

在自定义函数的问题解决之后,前文有提到由于笔者架构的问题,三角函数因子的处理出现了问题,三角函数因子内部的表达式可以是多样的,与单一的xyz不同,因此用相同的方法处理难度很大。由于第二周时间紧张,笔者最终败在了第二次作业的中测上,强测也仅拿到了可怜的4分。

在第三周,笔者在保持原有Lexer和Parser结构基本不变的基础上,通过学习同学的思路,采用Poly-Mono架构来取代原先的String架构。在重构后的设计中,笔者优化了以下几点:

  1. 在Lexer解析时将字符'-'单独处理,检测到其后为数字时,直接分解出一个负数。

  1. Number类中删除sign属性,将数字用String表示,避免了符号的影响。

  1. 将原来Term类中的加减号属性转化为乘+1或-1,归并到Factor容器当中。

  1. 将Term类也作为Factor接口的一个实现,方便若干数据类都能由Poly类统一处理。

  1. 新增Poly和Mono两个类,用于表达式的化简与输出。

在新架构的搭建过程中,笔者同样遇到了许多bug,但这次由于架构的清晰,bug的处理也就相对有迹可循。最终笔者也是借助重构后的代码通过了第二次作业强测的全部数据点,并以此为基础进行第三次作业的迭代开发。

重构的经历给笔者带来了很多的思考,让笔者认识到了架构在大体量程序开发过程中起到的重要作用,也让笔者对OO这门课程有了更加深刻的认识。

bug分析(重构后)
  • 新建的容器不等于null,将容器的用法和String类的用法搞混,进而导致不合法的合并同类项,将其改为trigList.size==0即可解决问题。

  • String类的substring方法用于截取一段字符串,delete方法用于删除一段字符串,在使用时由于对方法的不熟悉导致二者用混。

  • 当对容器中的内容进行修改时,必须采用深拷贝而非浅拷贝,否则会出现问题。

  • 在Parser解析过程中对三角函数的处理采用parseFactor()方法,根据笔者的架构这种方法无法处理三角函数后面紧跟的指数,改为调用parseExpr方法即可。

  • 在Term类中进行toPoly时,new的Poly必须有一个初始数据"1",否则会出现问题;而在Expr类中相同情况下,"0"则是不被需要的。

基于度量的程序结构对比
  • 类复杂度对比

重构前

重构后

重构后由于Poly类和Mono类的加入,多个类的复杂度明显降低。

  • 方法复杂度对比

重构前

重构后

重构后方法的总复杂度和平均复杂度都有着明显的降低。

  • 代码规模对比

重构前

重构后

重构前后的代码规模相差不大,但重构后的代码性能远优于重构前。


第三次作业

设计要求

在前两次作业的基础上,第三次作业新增了以下要求:

  • 支持求导操作,新增求导算子

  • 在保证不出现递归调用的前提下,函数表达式中支持调用其他“已定义的”函数

类图
架构设计

第三次作业采用了最普遍的递归下降法加Poly-Mono结构输出,每个数据类都是Factor接口的一个实现,Factor接口中有两个方法:toPoly()方法和第二次作业相同,用于将每个数据类的内容转换为一个多项式,便于后续的化简和输出;derive()方法是新加入的方法,用于第三次作业新增的求导运算,其中的参数String为被求导的自变量(x|y|z),通过在每个数据类当中加入求导方法,可以将复杂的求导运算进行层次化处理。至于自定义函数的重复调用,可以在对每个自定义函数读入时对其函数体调用一次funcReplace()方法解决。将求导算子和自定义函数处理掉之后,第三次作业便与第二次作业无异。

bug分析
  • 在处理重复调用的自定义函数时,应当采取和处理输入表达式一致的方法。笔者在最初处理时遗漏了函数代入后再次进行重复加减号处理的操作,导致中测最后一个点不通过。不过这是小bug,很容易就修复了。

while (funcBody.indexOf('f') != -1 || funcBody.indexOf('g') != -1 || funcBody.indexOf('h') != -1) {
      funcBody = funcReplace(funcBody);
}
funcBody = transform(funcBody); //这行最初被遗漏
funcBody = operate(funcBody);
  • 在Mono类的mulMono()方法中,两个单项式相乘,要求其三角函数部分相乘,遇到相同的三角函数时,需要将后一个删除,两个指数相加替换给前一个,在删除过程中出现了bug。各类容器的删除需要用到迭代器,直接使用remove()方法也需要调用迭代器,笔者最初使用remove()方法导致出错,在hack阶段因为这个bug被砍4刀,留下了惨痛的教训。

Iterator<String> iter = map2.keySet().iterator();
while (iter.hasNext()) {
      String key2 = iter.next();
      if (key1.equals(key2)) {
            int value0 = map2.get(key2);
            map1.replace(key1, value, value + value0);
            iter.remove();
      }
}
  • 在最初处理指数时,笔者采用的是根据指数的数值向Term中加入指定数量的对应Factor,对于三角函数和幂函数,将其带指数向Term中加入一次。在指数为0时,表达式因子可以变为1,但三角函数和幂函数由于转为Poly,而在Poly类中没有对于0次幂的处理,导致bug的产生。这一bug主要来源于笔者对所有情况考虑得不够全面,给了hack的人可乘之机。

public Mono(String base, int num) {
        if (num == 0) { coef = new BigInteger("1"); }
}
public void powAddFactor(Factor factor, int pow) {
        if (pow == 0) {
            Factor powerZero = new Number("1");
            this.factors.add(powerZero);
        }
}
基于度量的程序结构分析
  • 类复杂度分析

  • 方法复杂度分析

  • 代码规模分析

可以看到,在合理的架构下,第三次作业相较于第二次作业只新增了100余行的代码。


心得体会

首先要说句题外话,由于笔者仅在第三周参与了一次互测,在发现别人bug所采用的策略及其有效性方面实在是没有什么经验可谈,因此该博客中有关这部分的内容省略,敬请谅解。

下面是笔者个人对于OO第一单元的一些心得体会。

学OO,首先要端正心态。很早就听说过OO课程的难度很大,我起初对这一说法并不以为意,但在经过这一个单元的学习之后,我发现这门课的难度是真的大,难度远超过我曾经学过的任何一门课程。可以说,OO这门课程是我来到北航这所学校之后遇到的最大的挑战,我曾一度想放弃挣扎直接摆烂。但这是一门必修课,难度再大也要克服,就算是进入补给站或是重修,也早晚要战胜它。

接着谈回到OO课程本身。OO课程培养的是一种面向对象的思维而非简单的Java语言,面向对象的这种思维在今后的大规模程序开发乃至未来的实际工作中都发挥着举足轻重的作用,无法掌握这种思维,基本可以说无法在这个行业当中立足。掌握这种面向对象的思维,对个人今后的发展带来的益处是无限大的。因此OO课程很重要,学好OO课程很重要。

然后谈回到第一单元的作业。这一单元的作业带给我的最大感受就是架构的重要性。我曾经花费了巨量的时间在最初的架构上进行开发,但最终的程序完全无法运行,大部分的努力都成了无用功;而换成Poly-Mono架构之后,思路随之变得顺畅,bug也变得没有那么难发现。可以说,一个好的架构在代码的迭代开发过程中所能起到的作用要大于代码本身。

在完成第一单元作业的过程中,我也深刻感受到了自己和上过OO先导课的同学之间的差距。举一个最经典的例子,String类中的ReplaceAll方法可以直接实现表达式的预处理,而对此不清楚的同学还在花时间写函数写循环。不过没上过先导课不应该成为借口,认真学习Java基本的语法知识,在作业完成的过程中逐步提高自己的编码能力,这才是我们这些没有上过先导课的同学应该做的。

最后对未来做一个展望。在之后的OO作业的完成过程中,我会尽可能规范自己的架构,将思路梳理通畅之后再进行编码,争取减少自己写出的bug数量,争取在今后的互测中可以少挨几刀。拒绝摆烂,用力前进,希望未来会越变越好!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值