前言
首次写博客, 作为记录自己OO作业的一个载体, 这次是前三次作业的回顾, 具体的一些技术细节会之后发表其他文章进行记录.
这篇文章共有三部分, 依次为: 程序结构分析, bug分析与互测策略 以及 体会与总结.
程序结构分析:
-
开始之前, 简要陈述一些代码度量指标.
应用IDEA插件MetricsReloaded计算整个项目代码复杂度, 与此次作业相关的参数共有五个, 如下所示
- 圈复杂度(cyclomatic complexity), 对方法而言, 其度量了这一方法中所有可能的独立路径数目, 相当于测试覆盖到代码的每一处时, 需要的测试样例数目. 有专门的计算方法.
- 基本圈复杂度(essential cyclomatic complexity), 对方法而言, 其同样度量了代码的圈复杂度, 前提是去除方法中结构化的控制结构. 如果代码拥有较高的圈复杂度, 但是基本圈复杂度低, 说明这一方法可以分解为若干子方法, 有降低复杂度的空间; 如果基本圈复杂度较高, 通常说明方法难以理解, 维护成本高昂, 即代码质量堪忧.
- 设计复杂度(design complexity), 对方法而言, 表示一个方法和他所调用的其他方法的紧密程度
- WMC(weighted method complexity), 对类而言, 依据类中含有方法的CC度量, 显示了类的复杂度
- OCavg, 对类而言, 操作复杂度.
-
我的三次作业代码基本OO度量与类图如下: (仅展示方法代码的度量)
-
第一次作业:
methods ev(G) iv(G) v(G) Polynomial.constructPolynomial() 1.0 4.0 4.0 Polynomial.getDerivative() 1.0 5.0 5.0 Polynomial.main(String[]) 2.0 1.0 2.0 Polynomial.parsePolynomial() 1.0 4.0 8.0 Polynomial.Polynomial(String) 1.0 1.0 1.0 Polynomial.printAns() 1.0 17.0 18.0 Term.getCoefficient() 1.0 1.0 1.0 Term.getDegree() 1.0 1.0 1.0 Term.setCoefficient(BigInteger) 1.0 1.0 1.0 Term.setDegree(BigInteger) 1.0 1.0 1.0 Term.Term(BigInteger,BigInteger) 1.0 1.0 1.0 Total 13.0 41.0 51.0 Average 1.0833333333333333 3.4166666666666665 4.25 第一次题目思路清晰, 因此代码结构明了, 但是是明显的过程化代码,
printANS()
方法的复杂度过高是, 原因是我按照判定顺序扔了一串if-else
上去, 以最优化输出, 虽然dirty但有效.作为重点的读取部分, 我首先进行预处理, 去掉空白符并替换连续符号, 之后用单个正则表达式匹配, 通过命名捕获组获得有效数据.
uml图如下所示. 由于结构单一, 只是很好的适应了特化的第一次作业要求.
-
第二次作业:
methods ev(G) iv(G) v(G) Expression.addTermRaw(Term,BigInteger) 2.0 2.0 3.0 Expression.check4simplify(Term,Term) 8.0 5.0 10.0 Expression.getDerivativeX() 1.0 5.0 5.0 Expression.simplifyExpression() 7.0 9.0 9.0 Expression.toString() 4.0 18.0 23.0 Factory.createPerfectExpression(String) 1.0 2.0 2.0 MainClass.main(String[]) 1.0 3.0 3.0 Term.compareTo(Term) 3.0 3.0 5.0 Tools.checkFormat(String) 1.0 3.0 3.0 Tools.compactString(String) 1.0 3.0 3.0 Tools.parsePerfectExpression(String) 1.0 3.0 3.0 Tools.parseTerm(String) 2.0 16.0 19.0 Total 44.0 85.0 102.0 Average 1.9130434782608696 3.6956521739130435 4.434782608695652 第二次作业新增要求包含\(sin(x)\)与\(cos(x)\), 经过权衡决定延续第一次作业的思路, 即依据题目要求特化代码结构, 代码展示出无视可扩展性的任性.
我个人当然考虑过可扩展性的问题, 但是目前要求十分简单, 相当于是树根的位置, 若猜测扩展, 则不确定性大增, 依旧面临重构风险, 因此我决定特化代码, 减少由于程序结构导致的bug.
读取部分我仍旧是预处理之后用完整的正则表达式匹配, work but crude.
表格中黑体表示的数字, 显示了相应方法中代码的复杂性, 其中大部分是圈复杂度过高, 基本圈复杂度都在可以忍受的范围内, 说明这些方法有能力分解为更小的子方法, 以增强可读性, 减少复杂度. 方法复杂度高的直接原因是代码层次化羸弱, 这也是我给自己第三次作业所留下的坑.
类图如下:
-
Term类作为最基本的操作元素, 被Expression类包含, Tools类包含若干静态方法, 用于辅助Expression类进行读取, 计算等操作. Factory类只是用函数抽象了创建对象的过程, 与对象创建模式无关.
可以见得, 由于\(cos\), \(sin\)等函数的出现, 迫使我讲过大的类与方法分离, 但实际上代码的本质还是过程式的逻辑, 可以转化为很长的一个main()
来完成.
-
第三次作业:
代码度量结果如下, 为节省篇幅, 仅提供大于含有大于5的项
methods ev(G) iv(G) v(G) derivative.combination.AddiCombination.toString() 5.0 5.0 7.0 derivative.combination.MultiCombination.compact() 1.0 6.0 6.0 derivative.combination.MultiCombination.toString() 5.0 9.0 11.0 derivative.Tool.parseExpString(String) 9.0 9.0 13.0 derivative.Tool.replaceNest(String) 6.0 4.0 11.0 derivative.Tool.replaceParen(String) 7.0 2.0 8.0 Total 86.0 91.0 120.0 Average 1.8695652173913044 1.9782608695652173 2.608695652173913 类图如下:(我尝试过自己画, 但是没有这个好看, 虽然这个图有点歪)
这次作业新增了嵌套需求.
透过类图可以看见, 有个完全没必要的类
Tool
在干扰视线, 原因是在我还未醒悟到多态与层次化结构在这次作业中的意义时, 我已经写出了解析读入字符串的代码; 毕竟前期工作(周二那个晚上)重心全部在如何完美读入表达式并找出 WRONG FORMAT, 故匆匆设计每个类之间的包含关系, 没有继承公共的父类, 也没有实现任何接口.在后续完善求导工作时, 我意识到公共父类
Factor
对于化简代码的重要性, 基于多态的设计可以减少我们判断对象运行时类别的工作, 可以使主程序归一化的调用覆写的方法, 也赋予我统一管理的能力. 这一点在诸如三角函数求导的实现中十分重要, 因为其求导结果是多个不同因子的乘积, 我的设计将得到一个列表, 若有公共父类, 后续处理将十分简单.在化简表达式与覆写
toString()
时, 我意识到分层设计的合理性, 同求导一样, 化简与toString()
方法都将递归调用更底层的类, 甚至是循环调用, 因此良好的层次结构设计可以很大程度简化代码复杂度. 显然在读入字符串时同样遵循这一规则, 若将其分散到各个类中, 让每一模块各司其职进行解析, 程序的可读性与度量结果都将大大优化, 可惜时间已经不足以修改最复杂的读入部分. 这也是为什么在度量结果中, 会产生高达13的数字.在类图中, 可以看出我的所有类的结构都参考了
BigInteger
等不可变类的创建方式, 即能改变这个对象的唯一机会就是构造函数中传入的信息. 这个信息在我的程序中就是个ArrayList, 但ArrayList是可变的, 所以在构造函数里我复制了一份作为对象所包含的内容, 这份拷贝是无法获取的, 因此提升了安全性, 避免了隐秘的bug. 这个办法唯一的缺陷是对于内存的消耗, 不过我不甚了解GC机制, 不知道我的担心是否多余.重点也是难点的读取部分, 在替换掉最外层括号后, 我仍旧使用完整的正则表达式进行匹配, 并进一步匹配嵌套的, 被替换掉的表达式. 虽然现在看来十分简单, 但周二晚上真挺痛苦.
bug分析与互测策略
-
个人bug分析:
前两次作业在公测中遇到被卡的情况, 主要原因皆为遗漏数据限制说明, 在强测与互测中没有遇到障碍, 原因是我在写完每一块较为独立的代码后, 立即开始测试, 并且提交前会经历数据随机性足够的数据生成程序与简单的的测试脚本的洗礼.
第三次作业公测没有问题, 但是被强测搞了一个点, 这个bug也在互测时被特殊关照. 此bug简要描述: 在读入嵌套的单独的\(sin\)与\(cos\)的嵌套内容时, 若为负常数, 则只会读入-1, 如\((sin(-17)) \rightarrow sin(-1)\). 原因是在Tool类中创建常数因子时错误调用了化简方法, 只需注释两行即可解决. 由于这个bug难以察觉, 且被我自动生成的数据避开, 所以没有不甘心. 这提醒我思路混乱时写的代码要格外关注.
由于出问题的输入结构十分显眼, 故我将测试样例化简为\((sin(-17))*x\)的形式. 在输入模块就已经定位.
-
寻找其他参与者bug:
寻找其他代码的bug的过程是有时无趣的, 这是我构建自动测试程序与脚本的主要原因, 就像看同辈的文章或棋谱, 若没有先入为主的观念了解作者, 或作品本身含有挑拨心弦的内容, 就很难兴奋. 我知道这么说显得无知... 幸运的是我遇到了不止一份十分简洁精彩的代码, 其中应用内部类以及lambda表达式来压缩代码行数, 以及状态机的尝试等, 都给予了我诸多灵感.
不过相应的寻找策略我还是有的, 如果谋一份代码结构混乱, 那么就直接把它挂到后台测试, 否则浏览结构, 遇到不明白的地方手动构造相应的样例进行测试即可. 若手动测试无果便同样运行脚本进行自动测试. 对方代码的结构是我构造手动测试点的主要原因, 所谓结构主要是关于特殊情况的判断, 与具体细节的实现. 就我的三次互测来说, 这样的策略还是有用的.
下图是三次作业通用的自动评测文件的截图, 具体内容不详述了.
体会与总结:
-
在前两次作业中, 凭借平稳的心态无伤走过. 但是在第三次作业时栽了, 回想起来收获不小, 逐渐获得了面向对象的感觉, 并且察觉到了与过程式程序的不同. java这一语言在OO中的卓越表现, 也是令我感到欣喜的地方, 以前一直听闻java的各种, 但至少现在我感觉, 它足以支撑心无旁骛的OO学习.
我明白自己的不足之处, 日后需认真阅读理解设计模式, 并且努力寻找应用场景. 在对象的设计与其关系的处理上, 我属于未出茅庐. 同时, 我思考的能力需要从代码中脱离, 从对象的角度思考, 其次才是实现.
感谢老师与助教提供的OO平台, 做作业的体验很好, 作业结束会获得心理上和学习上的满足, 讨论区的各种分享对我很有帮助, 互测很有意思, 完全不像听说的那般, 希望之后皆是如此. 十分期待下一次