关于第一单元的总结与反思 - Coekjan
写在前面
本篇博客重点在于复盘, 反思与自我批判, 提出一些优化的可能, 同时满足作业要求.
笔者的 另一篇博客 则重点介绍架构与测试, 并不满足作业要求, 因此不在本站发布.
架构总体分析
架构总体分为:
这两部分基本解耦.
核心架构
在第一次作业时, 笔者花费了不少心思设计求导核心框架, 最终使得这一框架完全胜任三次作业, 没有重构.
首先是注意到文法中提及的函数, 明确指出只有幂函数. 但是笔者认为, 迭代开发过程中很有可能出现多类函数组合的问题, 因此提出了抽象 Function
架构.
这样, 每一个类别均只需要关注自身的求导, 字符串化, 简化方式, 不需要迁就其他类别, 每一个类别之间都是解耦的.
第二第三次作业的核心架构没有改变:
这一架构的优势是明显的, 笔者在第二次作业迭代到第三次作业过程中, 仅对代码修改了几十行就通过了中测.
仅以 CosineFunction
的求导写法展示架构的优秀:
// inside CosineFunction
@Override
public Function diff() {
return new Multiplier(
Constant.NEG_ONE,
new SineFunction(function),
function.diff()
);
}
解析架构
第一次作业时, 观察到形式化文法可以无递归嵌套, 因此可以单靠正则表达式识别完成, 因此采用了 Matcher
类的 find
方法循环完成:
Matcher m = // regex
while (m.find()) {
// identify & construct
}
第二次作业时, 注意到文法出现嵌套, 因此学习了递归下降的方法来解析字符串, 这一做法极大方便了第三次作业时的格式检查.
值得一提的是, 递归下降分析前, 需要提取
token
流. 这一部分是采用了enum
类型与正则识别完成的, 相当简洁.
基于度量的程序结构分析
采用的插件为 MetricsReload .
第一次作业
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
io.InputParser.parse(String) |
23.0 | 1.0 | 6.0 | 10.0 |
kernel.Multiplier.simplify() |
20.0 | 6.0 | 7.0 | 8.0 |
kernel.Adder.simplify() |
14.0 | 3.0 | 3.0 | 8.0 |
kernel.Adder.compareTo(Function) |
9.0 | 5.0 | 5.0 | 7.0 |
kernel.Multiplier.compareTo(Function) |
7.0 | 4.0 | 4.0 | 4.0 |
kernel.PowerFunction.compareTo(Function) |
7.0 | 4.0 | 4.0 | 5.0 |
Derive.main(String[]) |
6.0 | 3.0 | 2.0 | 4.0 |
kernel.Multiplier.diff() |
6.0 | 1.0 | 4.0 | 4.0 |
kernel.Function.getType(Function) |
5.0 | 5.0 | 1.0 | 5.0 |
kernel.PowerFunction.stringify() |
4.0 | 2.0 | 3.0 | 3.0 |
kernel.Function.equals(Object) |
3.0 | 3.0 | 2.0 | 3.0 |
kernel.Multiplier.getFirstConst() |
3.0 | 3.0 | 1.0 | 3.0 |
kernel.Multiplier.getFirstPower() |
3.0 | 3.0 | 1.0 | 3.0 |
kernel.PowerFunction.diff() |
3.0 | 3.0 | 3.0 | 3.0 |
kernel.Variable.compareTo(Function) |
3.0 | 3.0 | 1.0 | 3.0 |
kernel.Adder.add(Function) |
2.0 | 1.0 | 2.0 | 2.0 |
kernel.Constant.compareTo(Function) |
2.0 | 2.0 | 2.0 | 2.0 |
kernel.Multiplier.multiply(Function) |
2.0 | 1.0 | 2.0 | 2.0 |
kernel.PowerFunction.equals(Object) |
2.0 | 3.0 | 2.0 | 3.0 |
kernel.PowerFunction.simplify() |
2.0 | 2.0 | 1.0 | 2.0 |
kernel.Adder.diff() |
1.0 | 1.0 | 2.0 | 2.0 |
kernel.Function.fullySimplify() |
1.0 | 1.0 | 2.0 | 2.0 |
... |
0.0 | 1.0 | 1.0 | 1.0 |
Total | 128.0 | 94.0 | 94.0 | 122.0 |
Average | 2.28 | 1.68 | 1.68 | 2.18 |
- 输入解析部分的CogC(认知复杂度)最高, 可能是使用正则表达式, 多层循环与分支导致的.
- 用于简化表达式的方法复杂度也很高, 主要原因是内部具体逻辑还是面向过程, 笔者在这一方面的架构是不良的.
- 用于比较两函数的方法复杂度也不低, 主要原因还是内部逻辑是不断枚举被比较函数的类型, 笔者在这方面的架构也是不良的.
第二与第三次作业
第二与第三次差距不大, 下面仅分析第三次作业情况.
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
kernel.Multiplier.simplify() |
23.0 | 6.0 | 9.0 | 12.0 |
kernel.Adder.compareTo(Function) |
15.0 | 7.0 | 7.0 | 9.0 |
kernel.Multiplier.compareTo(Function) |
13.0 | 6.0 | 6.0 | 6.0 |
kernel.Multiplier.functionSimplifyIterated(List<Function>) |
12.0 | 1.0 | 4.0 | 6.0 |
kernel.Adder.combineLikeTerm(List<Function>) |
11.0 | 4.0 | 6.0 | 10.0 |
kernel.PowerFunction.compareTo(Function) |
9.0 | 5.0 | 5.0 | 7.0 |
kernel.FunctionParser.parseCosine() |
8.0 | 6.0 | 3.0 | 7.0 |
kernel.FunctionParser.parseSine() |
8.0 | 6.0 | 3.0 | 7.0 |
kernel.Adder.functionSimplifyIterated(List<Function>) |
7.0 | 1.0 | 4.0 | 4.0 |
kernel.Function.getType(Function) |
7.0 | 7.0 | 1.0 | 7.0 |
kernel.FunctionTokenStream.detectTokens() |
7.0 | 4.0 | 4.0 | 4.0 |
kernel.PowerFunction.toString() |
7.0 | 2.0 | 4.0 | 5.0 |
kernel.FunctionParser.parsePower() |
6.0 | 3.0 | 3.0 | 5.0 |
kernel.Multiplier.diff() |
6.0 | 1.0 | 4.0 | 4.0 |
Derive.main(String[]) |
5.0 | 3.0 | 2.0 | 4.0 |
kernel.Multiplier.toString() |
5.0 | 1.0 | 4.0 | 4.0 |
kernel.CosineFunction.compareTo(Function) |
4.0 | 3.0 | 3.0 | 6.0 |
kernel.Function.fullySimplify() |
4.0 | 3.0 | 2.0 | 3.0 |
kernel.FunctionParser.parseExpressionAppendix() |
4.0 | 2.0 | 2.0 | 4.0 |
kernel.FunctionParser.parseFactor() |
4.0 | 6.0 | 4.0 | 7.0 |
kernel.FunctionParser.parseTermAppendix() |
4.0 | 3.0 | 3.0 | 3.0 |
kernel.Multiplier.coefficient() |
4.0 | 1.0 | 3.0 | 3.0 |
kernel.PowerFunction.simplify() |
4.0 | 4.0 | 4.0 | 4.0 |
kernel.SineFunction.compareTo(Function) |
4.0 | 3.0 | 3.0 | 5.0 |
kernel.CosineFunction.toString() |
3.0 | 2.0 | 2.0 | 3.0 |
kernel.Function.equals(Object) |
3.0 | 3.0 | 2.0 | 3.0 |
kernel.FunctionParser.parseExp() |
3.0 | 4.0 | 1.0 | 4.0 |
kernel.PowerFunction.diff() |
3.0 | 3.0 | 3.0 | 3.0 |
kernel.SineFunction.toString() |
3.0 | 2.0 | 2.0 | 3.0 |
kernel.Variable.compareTo(Function) |
3.0 | 3.0 | 1.0 | 3.0 |
kernel.Adder.add(Function) |
2.0 | 1.0 | 2.0 | 2.0 |
kernel.Adder.simplify() |
2.0 | 2.0 | 2.0 | 2.0 |
kernel.Constant.compareTo(Function) |
2.0 | 2.0 | 2.0 | 2.0 |
kernel.CosineFunction.simplify() |
2.0 | 2.0 | 2.0 | 2.0 |
kernel.FunctionParser.parseEmpty() |
2.0 | 1.0 | 3.0 | 3.0 |
kernel.FunctionParser.parseExpression() |
2.0 | 2.0 | 1.0 | 4.0 |
kernel.FunctionParser.parseSigned() |
2.0 | 2.0 | 1.0 | 4.0 |
kernel.FunctionParser.parseTerm() |
2.0 | 2.0 | 3.0 | 4.0 |
kernel.FunctionTokenStream.current() |
2.0 | 2.0 | 2.0 | 2.0 |
kernel.Multiplier.multiply(Function) |
2.0 | 1.0 | 2.0 | 2.0 |
kernel.PowerFunction.multPower(PowerFunction) |
2.0 | 2.0 | 2.0 | 2.0 |
kernel.SineFunction.simplify() |
2.0 | 2.0 | 2.0 | 2.0 |
kernel.Adder.diff() |
1.0 | 1.0 | 2.0 | 2.0 |
kernel.FunctionParser.evaluate() |
1.0 | 2.0 | 1.0 | 2.0 |
kernel.FunctionParser.parseConstant() |
1.0 | 2.0 | 1.0 | 2.0 |
... |
0.0 | 1.0 | 1.0 | 1.0 |
Total | 226.0 | 184.0 | 185.0 | 245.0 |
Average | 2.24 | 1.88 | 1.89 | 2.50 |
笔者由于没有把化简逻辑进一步面向对象化, 导致了复杂度与耦合程度高. 在本地进行随机样例测试时, 发现的bug也通常出自化简部分.
BUG分析
自主测试中发现的BUG
自主测试中, 笔者仅发现1个bug. 出自第一次作业的表达式化简, 具体为乘法化简时的面向过程逻辑混乱, 出现死循环的情况. 后续优化了写法, 使其更加面向对象化, 该问题就消除了.
圈复杂度分析中, 出现bug的代码段呈现极高的情况:
- CogC : 24
- ev(G) : 9
- iv(G) : 10
- v(G) : 11
- codeline : 50+
修改后, 代码段的圈复杂度有所下降, 但由于整体来讲没有跳出面向过程的模式, 复杂度仍然不良:
- CogC : 20
- ev(G) : 6
- iv(G) : 7
- v(G) : 8
- codeline : 34
可能是重新整理后, 逻辑上相对清晰了, 没有在这一方向上再出现bug. 但这也启示我: 以后写代码, 如果遇到 讨论 复杂情形时, 应当意识到自己陷入了面向过程的模式.
强测与互测
三次强测与互测中均未被测试出bug.
HACK策略
本地搭建了基于 SymPy
的评测机, 与基于形式化文法的随机数据生成机. 这一本地评测平台帮助我找到了自己的1个bug, 同时也找到了其他同屋者的bug.
互测中, 笔者采用的思路是: 黑箱测试+人工构造. 首先是将程序投入评测机测试, 当某一程序被测出bug, 则进行人工剖析代码结构, 构造精准样例打击bug.
以第三次作业为例: 笔者将同屋者程序投入评测机后, 十分钟内发现了4位同屋者的程序漏洞. 笔者旋即分析他们的代码, 发现他们有一些共同的特征 - 枚举WRONG FORMAT!
- 即采用正则匹配的方法找格式错误. 因此笔者随着评测机给的提示, 找到了其正则表达式的漏洞, 构造样例, 完成hack.
互测中发现同屋者的问题
第一次作业:
- 解析字符串时无法解析类似
+++1
的串 - 解析字符串时无法解析
x**+1
的串
第二次作业:
- 常数求导时, 输出空串
- 嵌套函数的求导逻辑有误
第三次作业:
- 格式检查出错(正则表达式枚举错误, 导致正确格式也被识别为错误格式)
- 优化过程中将表达式因子外的负号失踪
重构经历总结
三次作业中, 核心架构并未重构, 唯一涉及重构的地方是输入串的解析.
第一次作业中, 这部分仅由一个类完成:
- | OCavg | OCmax | WMC |
---|---|---|---|
InputParser |
3.67 | 9 | 11 |
第二与第三次作业中, 这部分由两个类完成: 输入串转 token
流 & 递归下降分析 token
流:
- | OCavg | OCmax | WMC |
---|---|---|---|
FunctionTokenStream |
1.57 | 4 | 11 |
FunctionParser |
4.36 | 8 | 61 |
Parser
的复杂度略有提升, 这是由于递归下降分析过程中需要不断判定向前看的符号, 但是却并不能简单否定递归下降做法的优越性 - 鲁棒, 可扩展, 容易理解.
在这一点上, 我是对插件评估结果略有不服的.
一些遗憾
在结束第三次作业后的当晚, 我才恍然醒悟自己的同类项合并逻辑相当不良, 应当在系数提取, 指数提取等方面进行高层抽象. 这样会让代码看起来更加简洁易懂.
心得体会
由于笔者上学期学习《Java程序设计》前, 预习了Java的相关知识, 了解了JavaOOP的相关内容, 因此在上学期的Java大作业中我也采用了不少面向对象的做法: 封装, 继承, 多态, 工厂模式, 控制反转... 积累了不少经验. 因此本单元的学习过程中还是比较顺利的, 基本上没有什么坎坷(除了优化).
另一方面, 本单元的学习中笔者最大的收获是学习了形式化文法的递归下降分析, 这一内容让我对形式化语言产生了一些兴趣, 后续可能会深入了解相关知识.